From a9cd40faeb64fb4858efdf09a2ac93233be0166e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 Jan 2021 11:19:44 -0300 Subject: [PATCH 1/4] Add transactions to methods in ForumManager --- .../briarproject/briar/api/forum/ForumManager.java | 11 +++++++++++ .../briarproject/briar/forum/ForumManagerImpl.java | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java index b8b9f5ca2..7d6bd42a9 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java @@ -74,6 +74,11 @@ public interface ForumManager { */ Collection getForums() throws DbException; + /** + * Returns all forums to which the user subscribes. + */ + Collection getForums(Transaction txn) throws DbException; + /** * Returns the text of the forum post with the given ID. */ @@ -92,8 +97,14 @@ public interface ForumManager { /** * Returns the group count for the given forum. */ + @Deprecated GroupCount getGroupCount(GroupId g) throws DbException; + /** + * Returns the group count for the given forum. + */ + GroupCount getGroupCount(Transaction txn, GroupId g) throws DbException; + /** * Marks a message as read or unread and updates the group count. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java index ee9c578ec..17935ab7b 100644 --- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java @@ -165,8 +165,12 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { @Override public Collection getForums() throws DbException { - Collection groups = db.transactionWithResult(true, txn -> - db.getGroups(txn, CLIENT_ID, MAJOR_VERSION)); + return db.transactionWithResult(true, this::getForums); + } + + @Override + public Collection getForums(Transaction txn) throws DbException { + Collection groups = db.getGroups(txn, CLIENT_ID, MAJOR_VERSION); try { List forums = new ArrayList<>(); for (Group g : groups) forums.add(parseForum(g)); @@ -235,6 +239,12 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { return messageTracker.getGroupCount(g); } + @Override + public GroupCount getGroupCount(Transaction txn, GroupId g) + throws DbException { + return messageTracker.getGroupCount(txn, g); + } + @Override public void setReadFlag(GroupId g, MessageId m, boolean read) throws DbException { From e2e67edbbe2ac40c7e09aeaa84802aca92a7501f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 Jan 2021 11:58:25 -0300 Subject: [PATCH 2/4] Introduce ForumListViewModel --- .../briarproject/briar/android/AppModule.java | 2 + .../briar/android/forum/ForumListAdapter.java | 131 ++--------- .../android/forum/ForumListFragment.java | 216 ++++-------------- .../briar/android/forum/ForumListItem.java | 46 +++- .../android/forum/ForumListViewModel.java | 187 +++++++++++++++ .../briar/android/forum/ForumModule.java | 12 + .../briar/android/forum/ForumViewHolder.java | 77 +++++++ 7 files changed, 382 insertions(+), 289 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 9257a2d26..3971df6e8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -28,6 +28,7 @@ import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory; import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.android.account.LockManagerImpl; +import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule; @@ -68,6 +69,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; ViewModelModule.class, DevReportModule.class, // below need to be within same scope as ViewModelProvider.Factory + ForumModule.BindsModule.class, GroupListModule.class, }) public class AppModule { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java index 77cebffac..c97b491ae 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListAdapter.java @@ -1,134 +1,47 @@ package org.briarproject.briar.android.forum; -import android.content.Context; -import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; -import org.briarproject.bramble.api.sync.GroupId; +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.android.view.TextAvatarView; -import org.briarproject.briar.api.forum.Forum; -import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.DiffUtil.ItemCallback; +import androidx.recyclerview.widget.ListAdapter; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; -import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME; +@NotNullByDefault +class ForumListAdapter extends ListAdapter { -class ForumListAdapter - extends BriarAdapter { - - ForumListAdapter(Context ctx) { - super(ctx, ForumListItem.class); + ForumListAdapter() { + super(new ForumListCallback()); } @Override public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(ctx).inflate( + View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_forum, parent, false); return new ForumViewHolder(v); } @Override - public void onBindViewHolder(ForumViewHolder ui, int position) { - ForumListItem item = getItemAt(position); - if (item == null) return; + public void onBindViewHolder(ForumViewHolder viewHolder, int position) { + viewHolder.bind(getItem(position)); + } - // Avatar - ui.avatar.setText(item.getForum().getName().substring(0, 1)); - ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes()); - ui.avatar.setUnreadCount(item.getUnreadCount()); - - // Forum Name - ui.name.setText(item.getForum().getName()); - - // Post Count - int postCount = item.getPostCount(); - if (postCount > 0) { - ui.postCount.setText(ctx.getResources() - .getQuantityString(R.plurals.posts, postCount, - postCount)); - } else { - ui.postCount.setText(ctx.getString(R.string.no_posts)); + @NotNullByDefault + private static class ForumListCallback extends ItemCallback { + @Override + public boolean areItemsTheSame(ForumListItem a, ForumListItem b) { + return a.equals(b); } - // Date - if (item.isEmpty()) { - ui.date.setVisibility(GONE); - } else { - long timestamp = item.getTimestamp(); - ui.date.setText(UiUtils.formatDate(ctx, timestamp)); - ui.date.setVisibility(VISIBLE); - } - - // Open Forum on Click - ui.layout.setOnClickListener(v -> { - Intent i = new Intent(ctx, ForumActivity.class); - Forum f = item.getForum(); - i.putExtra(GROUP_ID, f.getId().getBytes()); - i.putExtra(GROUP_NAME, f.getName()); - ctx.startActivity(i); - }); - } - - @Override - public int compare(ForumListItem a, ForumListItem b) { - if (a == b) return 0; - // The forum with the newest message comes first - long aTime = a.getTimestamp(), bTime = b.getTimestamp(); - if (aTime > bTime) return -1; - if (aTime < bTime) return 1; - // Break ties by forum name - String aName = a.getForum().getName(); - String bName = b.getForum().getName(); - return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); - } - - @Override - public boolean areContentsTheSame(ForumListItem a, ForumListItem b) { - return a.isEmpty() == b.isEmpty() && - a.getTimestamp() == b.getTimestamp() && - a.getUnreadCount() == b.getUnreadCount(); - } - - @Override - public boolean areItemsTheSame(ForumListItem a, ForumListItem b) { - return a.getForum().equals(b.getForum()); - } - - int findItemPosition(GroupId g) { - int count = getItemCount(); - for (int i = 0; i < count; i++) { - ForumListItem item = getItemAt(i); - if (item != null && item.getForum().getGroup().getId().equals(g)) - return i; - } - return INVALID_POSITION; // Not found - } - - static class ForumViewHolder extends RecyclerView.ViewHolder { - - private final ViewGroup layout; - private final TextAvatarView avatar; - private final TextView name; - private final TextView postCount; - private final TextView date; - - private ForumViewHolder(View v) { - super(v); - - layout = (ViewGroup) v; - avatar = v.findViewById(R.id.avatarView); - name = v.findViewById(R.id.forumNameView); - postCount = v.findViewById(R.id.postCountView); - date = v.findViewById(R.id.dateView); + @Override + public boolean areContentsTheSame(ForumListItem a, ForumListItem b) { + return a.isEmpty() == b.isEmpty() && + a.getTimestamp() == b.getTimestamp() && + a.getUnreadCount() == b.getUnreadCount(); } } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java index 8d7735133..093968a68 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListFragment.java @@ -12,80 +12,48 @@ import android.view.ViewGroup; import com.google.android.material.snackbar.Snackbar; -import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.NoSuchGroupException; -import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.event.GroupAddedEvent; -import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.fragment.BaseEventFragment; +import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.sharing.ForumInvitationActivity; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; -import org.briarproject.briar.api.forum.Forum; -import org.briarproject.briar.api.forum.ForumManager; -import org.briarproject.briar.api.forum.ForumPostHeader; -import org.briarproject.briar.api.forum.ForumSharingManager; -import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; -import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; -import androidx.annotation.UiThread; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE; -import static java.util.logging.Level.WARNING; -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.api.forum.ForumManager.CLIENT_ID; +import static java.util.Objects.requireNonNull; @MethodsNotNullByDefault @ParametersNotNullByDefault -public class ForumListFragment extends BaseEventFragment implements +public class ForumListFragment extends BaseFragment implements OnClickListener { public final static String TAG = ForumListFragment.class.getName(); - private final static Logger LOG = Logger.getLogger(TAG); + private ForumListViewModel viewModel; private BriarRecyclerView list; - private ForumListAdapter adapter; private Snackbar snackbar; + private final ForumListAdapter adapter = new ForumListAdapter(); @Inject - AndroidNotificationManager notificationManager; - - // Fields that are accessed from background threads must be volatile - @Inject - volatile ForumManager forumManager; - @Inject - volatile ForumSharingManager forumSharingManager; + ViewModelProvider.Factory viewModelFactory; public static ForumListFragment newInstance() { - - Bundle args = new Bundle(); - - ForumListFragment fragment = new ForumListFragment(); - fragment.setArguments(args); - return fragment; + return new ForumListFragment(); } @Override public void injectFragment(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(ForumListViewModel.class); } @Nullable @@ -93,24 +61,35 @@ public class ForumListFragment extends BaseEventFragment implements public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - requireActivity().setTitle(R.string.forums_button); - View contentView = - inflater.inflate(R.layout.fragment_forum_list, container, - false); + View v = inflater.inflate(R.layout.fragment_forum_list, container, + false); - adapter = new ForumListAdapter(getActivity()); - - list = contentView.findViewById(R.id.forumList); + list = v.findViewById(R.id.forumList); list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setAdapter(adapter); + viewModel.getForumListItems().observe(getViewLifecycleOwner(), result -> + result.onError(this::handleException).onSuccess(items -> { + adapter.submitList(items); + if (requireNonNull(items).size() == 0) list.showData(); + }) + ); snackbar = new BriarSnackbarBuilder() .setAction(R.string.show, this) .make(list, "", LENGTH_INDEFINITE); + viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> { + if (num == 0) { + snackbar.dismiss(); + } else { + snackbar.setText(getResources().getQuantityString( + R.plurals.forums_shared, num, num)); + if (!snackbar.isShownOrQueued()) snackbar.show(); + } + }); - return contentView; + return v; } @Override @@ -121,19 +100,23 @@ public class ForumListFragment extends BaseEventFragment implements @Override public void onStart() { super.onStart(); - // TODO block all forum post notifications as well - notificationManager.clearAllForumPostNotifications(); - loadForums(); - loadAvailableForums(); + viewModel.blockAllForumPostNotifications(); + viewModel.clearAllForumPostNotifications(); + // The attributes and sorting of the forums may have changed while we + // were stopped and we have no way finding out about them, so re-load + // e.g. less unread posts in a forum after viewing it. + viewModel.loadForums(); + // The number of invitations might have changed while we were stopped + // e.g. because of accepting an invitation which does not trigger event + viewModel.loadForumInvitations(); list.startPeriodicUpdate(); } @Override public void onStop() { super.onStop(); - adapter.clear(); - list.showProgressBar(); list.stopPeriodicUpdate(); + viewModel.unblockAllForumPostNotifications(); } @Override @@ -145,123 +128,12 @@ public class ForumListFragment extends BaseEventFragment implements @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_create_forum: - Intent intent = - new Intent(getContext(), CreateForumActivity.class); - startActivity(intent); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == R.id.action_create_forum) { + Intent intent = new Intent(getContext(), CreateForumActivity.class); + startActivity(intent); + return true; } - } - - private void loadForums() { - int revision = adapter.getRevision(); - listener.runOnDbThread(() -> { - try { - long start = now(); - Collection forums = new ArrayList<>(); - for (Forum f : forumManager.getForums()) { - try { - GroupCount count = - forumManager.getGroupCount(f.getId()); - forums.add(new ForumListItem(f, count)); - } catch (NoSuchGroupException e) { - // Continue - } - } - logDuration(LOG, "Full load", start); - displayForums(revision, forums); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - private void displayForums(int revision, Collection forums) { - runOnUiThreadUnlessDestroyed(() -> { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (forums.isEmpty()) list.showData(); - else adapter.replaceAll(forums); - } else { - LOG.info("Concurrent update, reloading"); - loadForums(); - } - }); - } - - private void loadAvailableForums() { - listener.runOnDbThread(() -> { - try { - long start = now(); - int available = forumSharingManager.getInvitations().size(); - logDuration(LOG, "Loading available", start); - displayAvailableForums(available); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - private void displayAvailableForums(int availableCount) { - runOnUiThreadUnlessDestroyed(() -> { - if (availableCount == 0) { - snackbar.dismiss(); - } else { - snackbar.setText(getResources().getQuantityString( - R.plurals.forums_shared, availableCount, - availableCount)); - if (!snackbar.isShownOrQueued()) snackbar.show(); - } - }); - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof ContactRemovedEvent) { - LOG.info("Contact removed, reloading available forums"); - loadAvailableForums(); - } else if (e instanceof GroupAddedEvent) { - GroupAddedEvent g = (GroupAddedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Forum added, reloading forums"); - loadForums(); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent g = (GroupRemovedEvent) e; - if (g.getGroup().getClientId().equals(CLIENT_ID)) { - LOG.info("Forum removed, removing from list"); - removeForum(g.getGroup().getId()); - } - } else if (e instanceof ForumPostReceivedEvent) { - ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; - LOG.info("Forum post added, updating item"); - updateItem(f.getGroupId(), f.getHeader()); - } else if (e instanceof ForumInvitationRequestReceivedEvent) { - LOG.info("Forum invitation received, reloading available forums"); - loadAvailableForums(); - } - } - - @UiThread - private void updateItem(GroupId g, ForumPostHeader m) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(g); - ForumListItem item = adapter.getItemAt(position); - if (item != null) { - item.addHeader(m); - adapter.updateItemAt(position, item); - } - } - - @UiThread - private void removeForum(GroupId g) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(g); - ForumListItem item = adapter.getItemAt(position); - if (item != null) adapter.remove(item); + return super.onOptionsItemSelected(item); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java index 5341d3f1f..19f16c018 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListItem.java @@ -4,12 +4,16 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.forum.Forum; import org.briarproject.briar.api.forum.ForumPostHeader; -// This class is NOT thread-safe -class ForumListItem { +import javax.annotation.concurrent.Immutable; + +import androidx.annotation.Nullable; + +@Immutable +class ForumListItem implements Comparable { private final Forum forum; - private int postCount, unread; - private long timestamp; + private final int postCount, unread; + private final long timestamp; ForumListItem(Forum forum, GroupCount count) { this.forum = forum; @@ -18,10 +22,11 @@ class ForumListItem { this.timestamp = count.getLatestMsgTime(); } - void addHeader(ForumPostHeader h) { - postCount++; - if (!h.isRead()) unread++; - if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); + ForumListItem(ForumListItem item, ForumPostHeader h) { + this.forum = item.forum; + this.postCount = item.postCount + 1; + this.unread = item.unread + (h.isRead() ? 0 : 1); + this.timestamp = Math.max(item.timestamp, h.getTimestamp()); } Forum getForum() { @@ -43,4 +48,29 @@ class ForumListItem { int getUnreadCount() { return unread; } + + @Override + public int hashCode() { + return forum.getId().hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof ForumListItem && getForum().equals( + ((ForumListItem) o).getForum()); + } + + @Override + public int compareTo(ForumListItem o) { + if (this == o) return 0; + // The forum with the newest message comes first + long aTime = getTimestamp(), bTime = o.getTimestamp(); + if (aTime > bTime) return -1; + if (aTime < bTime) return 1; + // Break ties by forum name + String aName = getForum().getName(); + String bName = o.getForum().getName(); + return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java new file mode 100644 index 000000000..4f98c823e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java @@ -0,0 +1,187 @@ +package org.briarproject.briar.android.forum; + +import android.app.Application; + +import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; +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.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.event.GroupAddedEvent; +import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.DbViewModel; +import org.briarproject.briar.android.viewmodel.LiveResult; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumManager; +import org.briarproject.briar.api.forum.ForumPostHeader; +import org.briarproject.briar.api.forum.ForumSharingManager; +import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent; +import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; + +import java.util.ArrayList; +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.UiThread; +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.api.forum.ForumManager.CLIENT_ID; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +class ForumListViewModel extends DbViewModel implements EventListener { + + private static final Logger LOG = + getLogger(ForumListViewModel.class.getName()); + + private final ForumManager forumManager; + private final ForumSharingManager forumSharingManager; + private final AndroidNotificationManager notificationManager; + private final EventBus eventBus; + + private final MutableLiveData>> forumItems = + new MutableLiveData<>(); + private final MutableLiveData numInvitations = + new MutableLiveData<>(); + + @Inject + ForumListViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + ForumManager forumManager, + ForumSharingManager forumSharingManager, + AndroidNotificationManager notificationManager, EventBus eventBus) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor); + this.forumManager = forumManager; + this.forumSharingManager = forumSharingManager; + this.notificationManager = notificationManager; + this.eventBus = eventBus; + this.eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + } + + void clearAllForumPostNotifications() { + notificationManager.clearAllForumPostNotifications(); + } + + void blockAllForumPostNotifications() { + // TODO + } + + void unblockAllForumPostNotifications() { + // TODO + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof ContactRemovedEvent) { + LOG.info("Contact removed, reloading available forums"); + loadForumInvitations(); + } else if (e instanceof ForumInvitationRequestReceivedEvent) { + LOG.info("Forum invitation received, reloading available forums"); + loadForumInvitations(); + } else if (e instanceof GroupAddedEvent) { + GroupAddedEvent g = (GroupAddedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Forum added, reloading forums"); + loadForums(); + } + } else if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent g = (GroupRemovedEvent) e; + if (g.getGroup().getClientId().equals(CLIENT_ID)) { + LOG.info("Forum removed, removing from list"); + onGroupRemoved(g.getGroup().getId()); + } + } else if (e instanceof ForumPostReceivedEvent) { + ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; + LOG.info("Forum post added, updating item"); + onForumPostReceived(f.getGroupId(), f.getHeader()); + } + } + + public void loadForums() { + loadList(this::loadForums, forumItems::setValue); + } + + @DatabaseExecutor + private List loadForums(Transaction txn) throws DbException { + long start = now(); + List forums = new ArrayList<>(); + for (Forum f : forumManager.getForums(txn)) { + GroupCount count = forumManager.getGroupCount(txn, f.getId()); + forums.add(new ForumListItem(f, count)); + } + Collections.sort(forums); + logDuration(LOG, "Loading forums", start); + return forums; + } + + @UiThread + private void onForumPostReceived(GroupId g, ForumPostHeader header) { + List list = updateListItems(forumItems, + itemToTest -> itemToTest.getForum().getId().equals(g), + itemToUpdate -> new ForumListItem(itemToUpdate, header)); + if (list == null) return; + // re-sort as the order of items may have changed + Collections.sort(list); + forumItems.setValue(new LiveResult<>(list)); + } + + @UiThread + private void onGroupRemoved(GroupId groupId) { + List list = removeListItems(forumItems, i -> + i.getForum().getId().equals(groupId) + ); + if (list == null) return; + forumItems.setValue(new LiveResult<>(list)); + } + + void loadForumInvitations() { + runOnDbThread(() -> { + try { + long start = now(); + int available = forumSharingManager.getInvitations().size(); + logDuration(LOG, "Loading available", start); + numInvitations.postValue(available); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData>> getForumListItems() { + return forumItems; + } + + LiveData getNumInvitations() { + return numInvitations; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java index 06ebbd1f9..62394c76d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java @@ -2,13 +2,25 @@ package org.briarproject.briar.android.forum; import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.viewmodel.ViewModelKey; +import androidx.lifecycle.ViewModel; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.IntoMap; @Module public class ForumModule { + @Module + public interface BindsModule { + @Binds + @IntoMap + @ViewModelKey(ForumListViewModel.class) + ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel); + } + @ActivityScope @Provides ForumController provideForumController(BaseActivity activity, diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java new file mode 100644 index 000000000..78129ffb6 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewHolder.java @@ -0,0 +1,77 @@ +package org.briarproject.briar.android.forum; + +import android.content.Context; +import android.content.Intent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.util.UiUtils; +import org.briarproject.briar.android.view.TextAvatarView; +import org.briarproject.briar.api.forum.Forum; + +import androidx.recyclerview.widget.RecyclerView; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME; + +class ForumViewHolder extends RecyclerView.ViewHolder { + + private final Context ctx; + private final ViewGroup layout; + private final TextAvatarView avatar; + private final TextView name; + private final TextView postCount; + private final TextView date; + + ForumViewHolder(View v) { + super(v); + ctx = v.getContext(); + layout = (ViewGroup) v; + avatar = v.findViewById(R.id.avatarView); + name = v.findViewById(R.id.forumNameView); + postCount = v.findViewById(R.id.postCountView); + date = v.findViewById(R.id.dateView); + } + + void bind(ForumListItem item) { + // Avatar + avatar.setText(item.getForum().getName().substring(0, 1)); + avatar.setBackgroundBytes(item.getForum().getId().getBytes()); + avatar.setUnreadCount(item.getUnreadCount()); + + // Forum Name + name.setText(item.getForum().getName()); + + // Post Count + int count = item.getPostCount(); + if (count > 0) { + postCount.setText(ctx.getResources() + .getQuantityString(R.plurals.posts, count, count)); + } else { + postCount.setText(ctx.getString(R.string.no_posts)); + } + + // Date + if (item.isEmpty()) { + date.setVisibility(GONE); + } else { + long timestamp = item.getTimestamp(); + date.setText(UiUtils.formatDate(ctx, timestamp)); + date.setVisibility(VISIBLE); + } + + // Open Forum on Click + layout.setOnClickListener(v -> { + Intent i = new Intent(ctx, ForumActivity.class); + Forum f = item.getForum(); + i.putExtra(GROUP_ID, f.getId().getBytes()); + i.putExtra(GROUP_NAME, f.getName()); + ctx.startActivity(i); + }); + } + +} From 3b02797639b94a6ad65e0984ba4be04ae415e462 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 Jan 2021 12:03:38 -0300 Subject: [PATCH 3/4] Block forum post notifications while viewing forum list --- .../android/AndroidNotificationManagerImpl.java | 15 +++++++++++++-- .../briar/android/forum/ForumListViewModel.java | 4 ++-- .../api/android/AndroidNotificationManager.java | 4 ++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java index 318153320..7bad14178 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java @@ -109,7 +109,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, @Nullable private GroupId blockedGroup = null; private boolean blockSignInReminder = false; - private boolean blockGroups = false, blockBlogs = false; + private boolean blockForums = false, blockGroups = false, + blockBlogs = false; private long lastSound = 0; private volatile Settings settings = new Settings(); @@ -453,6 +454,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, @UiThread private void showForumPostNotification(GroupId g) { + if (blockForums) return; if (g.equals(blockedGroup)) return; forumCounts.add(g); updateForumPostNotification(true); @@ -682,10 +684,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, }); } + @Override + public void blockAllForumPostNotifications() { + androidExecutor.runOnUiThread((Runnable) () -> blockForums = true); + } + + @Override + public void unblockAllForumPostNotifications() { + androidExecutor.runOnUiThread((Runnable) () -> blockForums = false); + } + @Override public void blockAllGroupMessageNotifications() { androidExecutor.runOnUiThread((Runnable) () -> blockGroups = true); - } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java index 4f98c823e..af86a9d95 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumListViewModel.java @@ -92,11 +92,11 @@ class ForumListViewModel extends DbViewModel implements EventListener { } void blockAllForumPostNotifications() { - // TODO + notificationManager.blockAllForumPostNotifications(); } void unblockAllForumPostNotifications() { - // TODO + notificationManager.unblockAllForumPostNotifications(); } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java b/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java index b2eeebd16..9e69808d5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java +++ b/briar-android/src/main/java/org/briarproject/briar/api/android/AndroidNotificationManager.java @@ -82,6 +82,10 @@ public interface AndroidNotificationManager { void unblockNotification(GroupId g); + void blockAllForumPostNotifications(); + + void unblockAllForumPostNotifications(); + void blockAllGroupMessageNotifications(); void unblockAllGroupMessageNotifications(); From 921e952b05849b82909000f4929c0b3497016652 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 5 Jan 2021 12:11:22 -0300 Subject: [PATCH 4/4] Rename ForumItem to ForumPostItem --- .../briar/android/forum/ForumActivity.java | 6 +-- .../briar/android/forum/ForumController.java | 4 +- .../android/forum/ForumControllerImpl.java | 12 ++--- .../{ForumItem.java => ForumPostItem.java} | 8 ++-- .../android/forum/ForumActivityTest.java | 45 +++++++++---------- .../android/forum/TestForumActivity.java | 2 +- 6 files changed, 38 insertions(+), 39 deletions(-) rename briar-android/src/main/java/org/briarproject/briar/android/forum/{ForumItem.java => ForumPostItem.java} (73%) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java index d8303da19..f77ac93e0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java @@ -38,7 +38,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEX @MethodsNotNullByDefault @ParametersNotNullByDefault public class ForumActivity extends - ThreadListActivity> + ThreadListActivity> implements ForumListener { @Inject @@ -50,7 +50,7 @@ public class ForumActivity extends } @Override - protected ThreadListController getController() { + protected ThreadListController getController() { return forumController; } @@ -82,7 +82,7 @@ public class ForumActivity extends } @Override - protected ThreadItemAdapter createAdapter( + protected ThreadItemAdapter createAdapter( LinearLayoutManager layoutManager) { return new ThreadItemAdapter<>(this, layoutManager); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java index 5f11f4e1d..6658e60fc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java @@ -8,9 +8,9 @@ import org.briarproject.briar.api.forum.Forum; import androidx.annotation.UiThread; @NotNullByDefault -interface ForumController extends ThreadListController { +interface ForumController extends ThreadListController { - interface ForumListener extends ThreadListListener { + interface ForumListener extends ThreadListListener { @UiThread void onForumLeft(ContactId c); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java index 75551ef93..6ad325d5b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java @@ -43,7 +43,7 @@ import static org.briarproject.bramble.util.LogUtils.logException; @NotNullByDefault class ForumControllerImpl extends - ThreadListControllerImpl + ThreadListControllerImpl implements ForumController { private static final Logger LOG = @@ -138,8 +138,8 @@ class ForumControllerImpl extends @Override public void createAndStoreMessage(String text, - @Nullable ForumItem parentItem, - ResultExceptionHandler handler) { + @Nullable ForumPostItem parentItem, + ResultExceptionHandler handler) { runOnDbThread(() -> { try { LocalAuthor author = identityManager.getLocalAuthor(); @@ -158,7 +158,7 @@ class ForumControllerImpl extends private void createMessage(String text, long timestamp, @Nullable MessageId parentId, LocalAuthor author, - ResultExceptionHandler handler) { + ResultExceptionHandler handler) { cryptoExecutor.execute(() -> { LOG.info("Creating forum post..."); ForumPost msg = forumManager.createLocalPost(getGroupId(), text, @@ -178,8 +178,8 @@ class ForumControllerImpl extends } @Override - protected ForumItem buildItem(ForumPostHeader header, String text) { - return new ForumItem(header, text); + protected ForumPostItem buildItem(ForumPostHeader header, String text) { + return new ForumPostItem(header, text); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumItem.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java similarity index 73% rename from briar-android/src/main/java/org/briarproject/briar/android/forum/ForumItem.java rename to briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java index 4dae7e7c1..03df1b0a0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java @@ -10,15 +10,15 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @NotThreadSafe -class ForumItem extends ThreadItem { +class ForumPostItem extends ThreadItem { - ForumItem(ForumPostHeader h, String text) { + ForumPostItem(ForumPostHeader h, String text) { super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(), h.getAuthorInfo(), h.isRead()); } - ForumItem(MessageId messageId, @Nullable MessageId parentId, String text, - long timestamp, Author author, AuthorInfo authorInfo) { + ForumPostItem(MessageId messageId, @Nullable MessageId parentId, + String text, long timestamp, Author author, AuthorInfo authorInfo) { super(messageId, parentId, text, timestamp, author, authorInfo, true); } diff --git a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java index 5a8892f9b..8c04d85b4 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java @@ -25,7 +25,6 @@ import org.robolectric.annotation.Config; import java.util.Arrays; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getRandomId; @@ -68,7 +67,7 @@ public class ForumActivityTest { private TestForumActivity forumActivity; @Captor - private ArgumentCaptor, DbException>> + private ArgumentCaptor, DbException>> rc; @Before @@ -80,42 +79,42 @@ public class ForumActivityTest { intent).create().start().resume().get(); } - private ThreadItemList getDummyData() { - ForumItem[] forumItems = new ForumItem[6]; - for (int i = 0; i < forumItems.length; i++) { + private ThreadItemList getDummyData() { + ForumPostItem[] forumPostItems = new ForumPostItem[6]; + for (int i = 0; i < forumPostItems.length; i++) { Author author = getAuthor(); String text = getRandomString(MAX_FORUM_POST_TEXT_LENGTH); - forumItems[i] = new ForumItem(MESSAGE_IDS[i], PARENT_IDS[i], + forumPostItems[i] = new ForumPostItem(MESSAGE_IDS[i], PARENT_IDS[i], text, System.currentTimeMillis(), author, new AuthorInfo(UNKNOWN)); - forumItems[i].setLevel(LEVELS[i]); + forumPostItems[i].setLevel(LEVELS[i]); } - ThreadItemList list = new ThreadItemListImpl<>(); - list.addAll(Arrays.asList(forumItems)); + ThreadItemList list = new ThreadItemListImpl<>(); + list.addAll(Arrays.asList(forumPostItems)); return list; } @Test public void testNestedEntries() { ForumController mc = forumActivity.getController(); - ThreadItemList dummyData = getDummyData(); + ThreadItemList dummyData = getDummyData(); verify(mc, times(1)).loadItems(rc.capture()); rc.getValue().onResult(dummyData); - ThreadItemAdapter adapter = forumActivity.getAdapter(); + ThreadItemAdapter adapter = forumActivity.getAdapter(); Assert.assertNotNull(adapter); assertEquals(6, adapter.getItemCount()); - assertTrue(dummyData.get(0).getText() - .equals(adapter.getItemAt(0).getText())); - assertTrue(dummyData.get(1).getText() - .equals(adapter.getItemAt(1).getText())); - assertTrue(dummyData.get(2).getText() - .equals(adapter.getItemAt(2).getText())); - assertTrue(dummyData.get(3).getText() - .equals(adapter.getItemAt(3).getText())); - assertTrue(dummyData.get(4).getText() - .equals(adapter.getItemAt(4).getText())); - assertTrue(dummyData.get(5).getText() - .equals(adapter.getItemAt(5).getText())); + assertEquals(dummyData.get(0).getText(), + adapter.getItemAt(0).getText()); + assertEquals(dummyData.get(1).getText(), + adapter.getItemAt(1).getText()); + assertEquals(dummyData.get(2).getText(), + adapter.getItemAt(2).getText()); + assertEquals(dummyData.get(3).getText(), + adapter.getItemAt(3).getText()); + assertEquals(dummyData.get(4).getText(), + adapter.getItemAt(4).getText()); + assertEquals(dummyData.get(5).getText(), + adapter.getItemAt(5).getText()); } } diff --git a/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java b/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java index 7a04b4517..dd71be55f 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java @@ -27,7 +27,7 @@ public class TestForumActivity extends ForumActivity { return forumController; } - public ThreadItemAdapter getAdapter() { + public ThreadItemAdapter getAdapter() { return adapter; }