From 1c107a851baa5b3c810d5eda6eb5fe9963719a6f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 8 Jan 2021 11:14:11 -0300 Subject: [PATCH] Move thread list events, fields and notification handling into ViewModels --- .../briar/android/forum/ForumActivity.java | 12 +- .../android/forum/ForumControllerImpl.java | 15 +- .../briar/android/forum/ForumViewModel.java | 16 +- .../conversation/GroupActivity.java | 78 +++------ .../conversation/GroupController.java | 9 -- .../conversation/GroupControllerImpl.java | 40 +---- .../conversation/GroupMessageAdapter.java | 8 +- .../conversation/GroupViewModel.java | 40 ++++- .../android/threaded/ThreadItemAdapter.java | 16 +- .../android/threaded/ThreadListActivity.java | 152 +++++++----------- .../threaded/ThreadListController.java | 19 +-- .../threaded/ThreadListControllerImpl.java | 11 -- .../android/threaded/ThreadListViewModel.java | 62 ++++++- 13 files changed, 215 insertions(+), 263 deletions(-) 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 f117dd61f..f3e7ecb87 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 @@ -26,7 +26,6 @@ import javax.inject.Inject; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM; @@ -63,6 +62,11 @@ public class ForumActivity extends return viewModel; } + @Override + protected ThreadItemAdapter createAdapter() { + return new ThreadItemAdapter<>(this); + } + @Override public void onCreate(@Nullable Bundle state) { super.onCreate(state); @@ -91,9 +95,9 @@ public class ForumActivity extends } @Override - protected ThreadItemAdapter createAdapter( - LinearLayoutManager layoutManager) { - return new ThreadItemAdapter<>(this, layoutManager); + public void onStart() { + super.onStart(); + viewModel.clearForumPostNotification(); } @Override 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 5228d8763..823c7ade0 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 @@ -17,10 +17,8 @@ import org.briarproject.briar.android.threaded.ThreadListControllerImpl; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.forum.ForumInvitationResponse; 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.ForumInvitationResponseReceivedEvent; -import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; import java.util.ArrayList; @@ -59,7 +57,6 @@ class ForumControllerImpl extends ThreadListControllerImpl @Override public void onActivityStart() { super.onActivityStart(); - notificationManager.clearForumPostNotification(getGroupId()); } @Override @@ -68,13 +65,7 @@ class ForumControllerImpl extends ThreadListControllerImpl ForumListener listener = (ForumListener) this.listener; - if (e instanceof ForumPostReceivedEvent) { - ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; - if (f.getGroupId().equals(getGroupId())) { - LOG.info("Forum post received, adding..."); - listener.onItemReceived(buildItem(f.getHeader(), f.getText())); - } - } else if (e instanceof ForumInvitationResponseReceivedEvent) { + if (e instanceof ForumInvitationResponseReceivedEvent) { ForumInvitationResponseReceivedEvent f = (ForumInvitationResponseReceivedEvent) e; ForumInvitationResponse r = f.getMessageHeader(); @@ -114,8 +105,4 @@ class ForumControllerImpl extends ThreadListControllerImpl }); } - private ForumPostItem buildItem(ForumPostHeader header, String text) { - return new ForumPostItem(header, text); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java index b6b24f790..281442cf3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java @@ -29,6 +29,7 @@ import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumPost; import org.briarproject.briar.api.forum.ForumPostHeader; import org.briarproject.briar.api.forum.ForumSharingManager; +import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; import java.util.List; import java.util.concurrent.Executor; @@ -80,7 +81,20 @@ class ForumViewModel extends ThreadListViewModel { @Override public void eventOccurred(Event e) { + if (e instanceof ForumPostReceivedEvent) { + ForumPostReceivedEvent f = (ForumPostReceivedEvent) e; + if (f.getGroupId().equals(groupId)) { + LOG.info("Forum post received, adding..."); + ForumPostItem item = buildItem(f.getHeader(), f.getText()); + addItem(item); + } + } else { + super.eventOccurred(e); + } + } + void clearForumPostNotification() { + notificationManager.clearForumPostNotification(groupId); } LiveData loadForum() { @@ -141,7 +155,7 @@ class ForumViewModel extends ThreadListViewModel { long start = now(); ForumPostHeader header = forumManager.addLocalPost(msg); textCache.put(msg.getMessage().getId(), text); - addItem(buildItem(header, text), true); + addItemAsync(buildItem(header, text)); logDuration(LOG, "Storing forum post", start); } catch (DbException e) { logException(LOG, WARNING, e); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java index fab1546cb..799a0c5b1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java @@ -7,13 +7,11 @@ import android.view.MenuInflater; import android.view.MenuItem; import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.identity.AuthorId; 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.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.privategroup.conversation.GroupController.GroupListener; import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity; import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity; @@ -29,7 +27,6 @@ import javax.inject.Inject; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -50,9 +47,6 @@ public class GroupActivity extends private GroupViewModel viewModel; - @Nullable - private Boolean isCreator = null; - private boolean isDissolved = false; private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem, dissolveMenuItem; @@ -73,6 +67,11 @@ public class GroupActivity extends return viewModel; } + @Override + protected GroupMessageAdapter createAdapter() { + return new GroupMessageAdapter(this); + } + @Override public void onCreate(@Nullable Bundle state) { super.onCreate(state); @@ -85,11 +84,7 @@ public class GroupActivity extends observeOnce(viewModel.getPrivateGroup(), this, privateGroup -> setTitle(privateGroup.getName()) ); - observeOnce(viewModel.isCreator(), this, isCreator -> { - this.isCreator = isCreator; // TODO remove field - adapter.setPerspective(isCreator); - showMenuItems(); - }); + observeOnce(viewModel.isCreator(), this, adapter::setIsCreator); // Open member list on Toolbar click if (toolbar != null) { @@ -101,25 +96,18 @@ public class GroupActivity extends }); } + // start with group disabled and enable when not dissolved setGroupEnabled(false); - controller.isDissolved( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Boolean isDissolved) { - setGroupEnabled(!isDissolved); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + viewModel.isDissolved().observe(this, dissolved -> { + setGroupEnabled(!dissolved); + if (dissolved) onGroupDissolved(); + }); } @Override - protected GroupMessageAdapter createAdapter( - LinearLayoutManager layoutManager) { - return new GroupMessageAdapter(this, layoutManager); + public void onStart() { + super.onStart(); + viewModel.clearGroupMessageNotifications(); } @Override @@ -139,8 +127,13 @@ public class GroupActivity extends leaveMenuItem.setVisible(false); dissolveMenuItem.setVisible(false); - // show items based on role - showMenuItems(); + // show items based on role (which will not change, so observe once) + observeOnce(viewModel.isCreator(), this, isCreator -> { + revealMenuItem.setVisible(!isCreator); + inviteMenuItem.setVisible(isCreator); + leaveMenuItem.setVisible(!isCreator); + dissolveMenuItem.setVisible(isCreator); + }); return super.onCreateOptionsMenu(menu); } @@ -154,26 +147,26 @@ public class GroupActivity extends startActivity(i1); return true; } else if (itemId == R.id.action_group_reveal) { - if (isCreator == null || isCreator) + if (viewModel.isCreator().getValue()) throw new IllegalStateException(); Intent i2 = new Intent(this, RevealContactsActivity.class); i2.putExtra(GROUP_ID, groupId.getBytes()); startActivity(i2); return true; } else if (itemId == R.id.action_group_invite) { - if (isCreator == null || !isCreator) + if (!viewModel.isCreator().getValue()) throw new IllegalStateException(); Intent i3 = new Intent(this, GroupInviteActivity.class); i3.putExtra(GROUP_ID, groupId.getBytes()); startActivityForResult(i3, REQUEST_GROUP_INVITE); return true; } else if (itemId == R.id.action_group_leave) { - if (isCreator == null || isCreator) + if (viewModel.isCreator().getValue()) throw new IllegalStateException(); showLeaveGroupDialog(); return true; } else if (itemId == R.id.action_group_dissolve) { - if (isCreator == null || !isCreator) + if (!viewModel.isCreator().getValue()) throw new IllegalStateException(); showDissolveGroupDialog(); @@ -190,14 +183,6 @@ public class GroupActivity extends } else super.onActivityResult(request, result, data); } - @Override - public void onItemReceived(GroupMessageItem item) { - super.onItemReceived(item); - if (item instanceof JoinMessageItem) { - if (((JoinMessageItem) item).isInitial()) loadSharingContacts(); - } - } - @Override protected int getMaxTextLength() { return MAX_GROUP_POST_TEXT_LENGTH; @@ -205,11 +190,10 @@ public class GroupActivity extends @Override public void onReplyClick(GroupMessageItem item) { - if (!isDissolved) super.onReplyClick(item); + if (!viewModel.isDissolved().getValue()) super.onReplyClick(item); } private void setGroupEnabled(boolean enabled) { - isDissolved = !enabled; sendController.setReady(enabled); list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f); @@ -221,15 +205,6 @@ public class GroupActivity extends } } - private void showMenuItems() { - // we need to have the menu items and know if we are the creator - if (leaveMenuItem == null || isCreator == null) return; - revealMenuItem.setVisible(!isCreator); - inviteMenuItem.setVisible(isCreator); - leaveMenuItem.setVisible(!isCreator); - dissolveMenuItem.setVisible(isCreator); - } - private void showLeaveGroupDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.BriarDialogTheme); @@ -268,7 +243,6 @@ public class GroupActivity extends sharingController.getOnlineCount()); } - @Override public void onGroupDissolved() { setGroupEnabled(false); AlertDialog.Builder builder = diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java index d777ce08f..210360ade 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java @@ -1,9 +1,7 @@ package org.briarproject.briar.android.privategroup.conversation; import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.android.threaded.ThreadListController; import org.briarproject.briar.api.privategroup.Visibility; @@ -12,17 +10,10 @@ import androidx.annotation.UiThread; public interface GroupController extends ThreadListController { - void isDissolved( - ResultExceptionHandler handler); - interface GroupListener extends ThreadListListener { - @UiThread void onContactRelationshipRevealed(AuthorId memberId, ContactId contactId, Visibility v); - - @UiThread - void onGroupDissolved(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java index d83cd9b53..e778cc01e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java @@ -16,13 +16,9 @@ import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.android.threaded.ThreadListControllerImpl; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.privategroup.GroupMember; -import org.briarproject.briar.api.privategroup.GroupMessageHeader; -import org.briarproject.briar.api.privategroup.JoinMessageHeader; import org.briarproject.briar.api.privategroup.PrivateGroupManager; import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent; -import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent; import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent; -import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import java.util.ArrayList; @@ -61,7 +57,6 @@ class GroupControllerImpl extends ThreadListControllerImpl @Override public void onActivityStart() { super.onActivityStart(); - notificationManager.clearGroupMessageNotification(getGroupId()); } @Override @@ -70,13 +65,7 @@ class GroupControllerImpl extends ThreadListControllerImpl GroupListener listener = (GroupListener) this.listener; - if (e instanceof GroupMessageAddedEvent) { - GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; - if (!g.isLocal() && g.getGroupId().equals(getGroupId())) { - LOG.info("Group message received, adding..."); - listener.onItemReceived(buildItem(g.getHeader(), g.getText())); - } - } else if (e instanceof ContactRelationshipRevealedEvent) { + if (e instanceof ContactRelationshipRevealedEvent) { ContactRelationshipRevealedEvent c = (ContactRelationshipRevealedEvent) e; if (getGroupId().equals(c.getGroupId())) { @@ -90,11 +79,6 @@ class GroupControllerImpl extends ThreadListControllerImpl if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) { listener.onInvitationAccepted(g.getContactId()); } - } else if (e instanceof GroupDissolvedEvent) { - GroupDissolvedEvent g = (GroupDissolvedEvent) e; - if (getGroupId().equals(g.getGroupId())) { - listener.onGroupDissolved(); - } } } @@ -123,26 +107,4 @@ class GroupControllerImpl extends ThreadListControllerImpl }); } - private GroupMessageItem buildItem(GroupMessageHeader header, String text) { - if (header instanceof JoinMessageHeader) { - return new JoinMessageItem((JoinMessageHeader) header, text); - } - return new GroupMessageItem(header, text); - } - - @Override - public void isDissolved( - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - boolean isDissolved = - privateGroupManager.isDissolved(getGroupId()); - handler.onResult(isDissolved); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java index 76c2dc564..4d88a22ea 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java @@ -14,7 +14,6 @@ import org.briarproject.briar.api.privategroup.Visibility; import androidx.annotation.LayoutRes; import androidx.annotation.UiThread; -import androidx.recyclerview.widget.LinearLayoutManager; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; @@ -24,9 +23,8 @@ class GroupMessageAdapter extends ThreadItemAdapter { private boolean isCreator = false; - GroupMessageAdapter(ThreadItemListener listener, - LinearLayoutManager layoutManager) { - super(listener, layoutManager); + GroupMessageAdapter(ThreadItemListener listener) { + super(listener); } @LayoutRes @@ -47,7 +45,7 @@ class GroupMessageAdapter extends ThreadItemAdapter { return new ThreadPostViewHolder<>(v); } - void setPerspective(boolean isCreator) { + void setIsCreator(boolean isCreator) { this.isCreator = isCreator; notifyDataSetChanged(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java index 4e0e33db5..b501c38eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java @@ -30,6 +30,8 @@ import org.briarproject.briar.api.privategroup.GroupMessageHeader; import org.briarproject.briar.api.privategroup.JoinMessageHeader; import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent; +import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent; import java.util.List; import java.util.concurrent.Executor; @@ -60,6 +62,8 @@ class GroupViewModel extends ThreadListViewModel { private final MutableLiveData privateGroup = new MutableLiveData<>(); private final MutableLiveData isCreator = new MutableLiveData<>(); + private final MutableLiveData isDissolved = + new MutableLiveData<>(); @Inject GroupViewModel(Application application, @@ -84,7 +88,26 @@ class GroupViewModel extends ThreadListViewModel { @Override public void eventOccurred(Event e) { - + if (e instanceof GroupMessageAddedEvent) { + GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; + // only act on non-local messages in this group + if (!g.isLocal() && g.getGroupId().equals(groupId)) { + LOG.info("Group message received, adding..."); + GroupMessageItem item = buildItem(g.getHeader(), g.getText()); + addItem(item); + if (item instanceof JoinMessageItem) { + // TODO +// if (((JoinMessageItem) item).isInitial()) loadSharingContacts(); + } + } + } else if (e instanceof GroupDissolvedEvent) { + GroupDissolvedEvent g = (GroupDissolvedEvent) e; + if (g.getGroupId().equals(groupId)) { + isDissolved.setValue(true); + } + } else { + super.eventOccurred(e); + } } @Override @@ -93,6 +116,10 @@ class GroupViewModel extends ThreadListViewModel { loadPrivateGroup(groupId); } + public void clearGroupMessageNotifications() { + notificationManager.clearGroupMessageNotification(groupId); + } + private void loadPrivateGroup(GroupId groupId) { runOnDbThread(() -> { try { @@ -109,7 +136,10 @@ class GroupViewModel extends ThreadListViewModel { @Override public void loadItems() { loadList(txn -> { - // TODO first check if group is dissolved + // check first if group is dissolved + isDissolved + .postValue(privateGroupManager.isDissolved(txn, groupId)); + // no continue to load the items long start = now(); List headers = privateGroupManager.getHeaders(txn, groupId); @@ -173,7 +203,7 @@ class GroupViewModel extends ThreadListViewModel { GroupMessageHeader header = privateGroupManager.addLocalMessage(msg); textCache.put(msg.getMessage().getId(), text); - addItem(buildItem(header, text), true); + addItemAsync(buildItem(header, text)); logDuration(LOG, "Storing group message", start); } catch (DbException e) { logException(LOG, WARNING, e); @@ -199,4 +229,8 @@ class GroupViewModel extends ThreadListViewModel { return isCreator; } + LiveData isDissolved() { + return isDissolved; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java index 73865d875..a66969592 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java @@ -30,10 +30,8 @@ public class ThreadItemAdapter static final int UNDEFINED = -1; private final ThreadItemListener listener; - private final LinearLayoutManager layoutManager; - public ThreadItemAdapter(ThreadItemListener listener, - LinearLayoutManager layoutManager) { + public ThreadItemAdapter(ThreadItemListener listener) { super(new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(I a, I b) { @@ -47,7 +45,6 @@ public class ThreadItemAdapter } }); this.listener = listener; - this.layoutManager = layoutManager; } @NonNull @@ -101,10 +98,17 @@ public class ThreadItemAdapter return null; } + @Nullable + MessageId getFirstVisibleMessageId(LinearLayoutManager layoutManager) { + int position = layoutManager.findFirstVisibleItemPosition(); + if (position == NO_POSITION) return null; + return getItemAt(position).getId(); + } + /** * Returns the position of the first unread item below the current viewport */ - int getVisibleUnreadPosBottom() { + int getVisibleUnreadPosBottom(LinearLayoutManager layoutManager) { int positionBottom = layoutManager.findLastVisibleItemPosition(); if (positionBottom == NO_POSITION) return NO_POSITION; for (int i = positionBottom + 1; i < getItemCount(); i++) { @@ -116,7 +120,7 @@ public class ThreadItemAdapter /** * Returns the position of the first unread item above the current viewport */ - int getVisibleUnreadPosTop() { + int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) { int positionTop = layoutManager.findFirstVisibleItemPosition(); int position = NO_POSITION; for (int i = 0; i < getItemCount(); i++) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index 9aaee4dc0..b3c6d34d1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -2,7 +2,6 @@ package org.briarproject.briar.android.threaded; import android.content.Intent; import android.os.Bundle; -import android.os.Parcelable; import android.view.MenuItem; import com.google.android.material.snackbar.Snackbar; @@ -19,7 +18,6 @@ import org.briarproject.briar.android.controller.SharingController; import org.briarproject.briar.android.controller.SharingController.SharingListener; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; -import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource; import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; @@ -31,7 +29,6 @@ import org.briarproject.briar.api.attachment.AttachmentHeader; import java.util.Collection; import java.util.List; -import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; @@ -42,7 +39,6 @@ import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.LinearLayoutManager; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; -import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @MethodsNotNullByDefault @@ -50,30 +46,22 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; public abstract class ThreadListActivity> extends BriarActivity implements ThreadListListener, SendListener, SharingListener, - ThreadItemListener, ThreadListDataSource { - - protected static final String KEY_REPLY_ID = "replyId"; - - private static final Logger LOG = - getLogger(ThreadListActivity.class.getName()); - - protected A adapter; + ThreadItemListener { + protected final A adapter = createAdapter(); private ThreadScrollListener scrollListener; protected BriarRecyclerView list; private LinearLayoutManager layoutManager; protected TextInputView textInput; protected TextSendController sendController; protected GroupId groupId; - @Nullable - private Parcelable layoutManagerState; - @Nullable - private MessageId replyId; protected abstract ThreadListController getController(); protected abstract ThreadListViewModel getViewModel(); + protected abstract A createAdapter(); + @Inject protected SharingController sharingController; @@ -103,59 +91,85 @@ public abstract class ThreadListActivity(adapter, getController(), upButton, downButton); list.getRecyclerView().addOnScrollListener(scrollListener); upButton.setOnClickListener(v -> { - int position = adapter.getVisibleUnreadPosTop(); + int position = adapter.getVisibleUnreadPosTop(layoutManager); if (position != NO_POSITION) { list.getRecyclerView().scrollToPosition(position); } }); downButton.setOnClickListener(v -> { - int position = adapter.getVisibleUnreadPosBottom(); + int position = adapter.getVisibleUnreadPosBottom(layoutManager); if (position != NO_POSITION) { list.getRecyclerView().scrollToPosition(position); } }); - if (state != null) { - byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID); - if (replyIdBytes != null) replyId = new MessageId(replyIdBytes); - } - getViewModel().getItems().observe(this, result -> result .onError(this::handleException) .onSuccess(this::displayItems) ); + getViewModel().getGroupRemoved().observe(this, removed -> { + if (removed) supportFinishAfterTransition(); + }); + sharingController.setSharingListener(this); loadSharingContacts(); } + @CallSuper + @Override + public void onStart() { + super.onStart(); + getViewModel().blockNotifications(); + sharingController.onStart(); + list.startPeriodicUpdate(); + } + + @CallSuper + @Override + public void onStop() { + super.onStop(); + getViewModel().unblockNotifications(); + sharingController.onStop(); + list.stopPeriodicUpdate(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + supportFinishAfterTransition(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + if (adapter.getHighlightedItem() != null) { + textInput.clearText(); + getViewModel().setReplyId(null); + updateTextInput(); + } else { + super.onBackPressed(); + } + } + @Override protected void onDestroy() { super.onDestroy(); - getViewModel().storeMessageId(getFirstVisibleMessageId()); - } - - @Override - @Nullable - public MessageId getFirstVisibleMessageId() { + // store list position, so we can restore it when coming back here if (layoutManager != null && adapter != null) { - int position = - layoutManager.findFirstVisibleItemPosition(); - I i = adapter.getItemAt(position); - return i == null ? null : i.getId(); + MessageId id = adapter.getFirstVisibleMessageId(layoutManager); + getViewModel().storeMessageId(id); } - return null; } - protected abstract A createAdapter(LinearLayoutManager layoutManager); - protected void displayItems(List items) { if (items.isEmpty()) { list.showData(); @@ -201,58 +215,9 @@ public abstract class ThreadListActivity void markItemsRead(Collection items); - interface ThreadListListener extends ThreadListDataSource { - - @UiThread - void onItemReceived(I item); - - @UiThread - void onGroupRemoved(); - + interface ThreadListListener { @UiThread void onInvitationAccepted(ContactId c); } - interface ThreadListDataSource { - - @UiThread - @Nullable - MessageId getFirstVisibleMessageId(); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java index f998cf701..01d015d17 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java @@ -14,7 +14,6 @@ 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.MessageId; -import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.android.controller.DbControllerImpl; import org.briarproject.briar.api.android.AndroidNotificationManager; @@ -78,14 +77,12 @@ public abstract class ThreadListControllerImpl @CallSuper @Override public void onActivityStart() { - notificationManager.blockNotification(getGroupId()); eventBus.addListener(this); } @CallSuper @Override public void onActivityStop() { - notificationManager.unblockNotification(getGroupId()); eventBus.removeListener(this); } @@ -94,16 +91,8 @@ public abstract class ThreadListControllerImpl } - @CallSuper @Override public void eventOccurred(Event e) { - if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent s = (GroupRemovedEvent) e; - if (s.getGroup().getId().equals(getGroupId())) { - LOG.info("Group removed"); - listener.onGroupRemoved(); - } - } } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java index 733f38b4b..c17f70e26 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java @@ -7,6 +7,7 @@ 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.identity.IdentityManager; @@ -15,6 +16,7 @@ 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.MessageId; +import org.briarproject.bramble.api.sync.event.GroupRemovedEvent; import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.android.viewmodel.DbViewModel; @@ -65,14 +67,18 @@ public abstract class ThreadListViewModel @DatabaseExecutor private final MessageTree messageTree = new MessageTreeImpl<>(); - protected final Map textCache = + protected final Map textCache = // TODO still needed? new ConcurrentHashMap<>(); private final MutableLiveData>> items = new MutableLiveData<>(); + private final MutableLiveData groupRemoved = + new MutableLiveData<>(); private final AtomicReference scrollToItem = new AtomicReference<>(); protected volatile GroupId groupId; + @Nullable + private MessageId replyId; private final AtomicReference storedMessageId = new AtomicReference<>(); @@ -114,6 +120,26 @@ public abstract class ThreadListViewModel loadItems(); } + public void blockNotifications() { + notificationManager.blockNotification(groupId); + } + + public void unblockNotifications() { + notificationManager.unblockNotification(groupId); + } + + @Override + @CallSuper + public void eventOccurred(Event e) { + if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent s = (GroupRemovedEvent) e; + if (s.getGroup().getId().equals(groupId)) { + LOG.info("Group removed"); + groupRemoved.setValue(true); + } + } + } + private void loadStoredMessageId() { runOnDbThread(() -> { try { @@ -161,9 +187,24 @@ public abstract class ThreadListViewModel return messageTree.depthFirstOrder(); } - protected void addItem(I item, boolean local) { + /** + * Add a remote item on the UI thread. + * The list will not scroll, but show an unread indicator. + */ + @UiThread + protected void addItem(I item) { messageTree.add(item); - if (local) scrollToItem.set(item.getId()); + items.setValue(new LiveResult<>(messageTree.depthFirstOrder())); + } + + /** + * Add a local item from the DB thread. + * The list will scroll to the new item. + */ + @DatabaseExecutor + protected void addItemAsync(I item) { + messageTree.add(item); + scrollToItem.set(item.getId()); items.postValue(new LiveResult<>(messageTree.depthFirstOrder())); } @@ -171,6 +212,17 @@ public abstract class ThreadListViewModel protected abstract String loadMessageText(Transaction txn, PostHeader header) throws DbException; + @UiThread + public void setReplyId(@Nullable MessageId id) { + replyId = id; + } + + @UiThread + @Nullable + public MessageId getReplyId() { + return replyId; + } + void storeMessageId(@Nullable MessageId messageId) { if (messageId != null) runOnDbThread(() -> { try { @@ -190,6 +242,10 @@ public abstract class ThreadListViewModel return items; } + LiveData getGroupRemoved() { + return groupRemoved; + } + @Nullable MessageId getAndResetScrollToItem() { return scrollToItem.getAndSet(null);