From 015ecb1d99b6175db15489b7e86d2ed42f7b5e71 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 17 Dec 2020 17:35:17 -0300 Subject: [PATCH] Migrate GroupListController to a ViewModel Use ListAdapter to calculate list diffs on a background thread --- .../briarproject/briar/android/AppModule.java | 5 +- .../android/activity/ActivityComponent.java | 2 - .../android/privategroup/list/GroupItem.java | 35 +++- .../privategroup/list/GroupListAdapter.java | 67 ++----- .../list/GroupListController.java | 60 ------ .../privategroup/list/GroupListFragment.java | 173 +++++------------- .../privategroup/list/GroupListModule.java | 19 +- ...ollerImpl.java => GroupListViewModel.java} | 149 ++++++++------- .../privategroup/list/GroupViewHolder.java | 6 +- 9 files changed, 195 insertions(+), 321 deletions(-) delete mode 100644 briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListController.java rename briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/{GroupListControllerImpl.java => GroupListViewModel.java} (64%) 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 30c510d80..451aac2b6 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 @@ -31,6 +31,7 @@ import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule; +import org.briarproject.briar.android.privategroup.list.GroupListModule; import org.briarproject.briar.android.reporting.DevReportModule; import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.api.android.AndroidNotificationManager; @@ -65,7 +66,9 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; LoginModule.class, NavDrawerModule.class, ViewModelModule.class, - DevReportModule.class + DevReportModule.class, + // below need to be within same scope as ViewModelProvider.Factory + GroupListModule.class, }) public class AppModule { 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 51e524319..0154b0469 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 @@ -60,7 +60,6 @@ import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment; import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity; import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule; import org.briarproject.briar.android.privategroup.list.GroupListFragment; -import org.briarproject.briar.android.privategroup.list.GroupListModule; import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity; import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule; import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule; @@ -94,7 +93,6 @@ import dagger.Component; ForumModule.class, GroupInvitationModule.class, GroupConversationModule.class, - GroupListModule.class, GroupMemberModule.class, GroupRevealModule.class, SharingModule.class diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupItem.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupItem.java index e4a69c366..a2e05c5dc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupItem.java @@ -8,9 +8,11 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.privategroup.GroupMessageHeader; import org.briarproject.briar.api.privategroup.PrivateGroup; +import androidx.annotation.Nullable; + // This class is not thread-safe @NotNullByDefault -class GroupItem { +class GroupItem implements Comparable { private final PrivateGroup privateGroup; private final AuthorInfo authorInfo; @@ -28,6 +30,15 @@ class GroupItem { this.dissolved = dissolved; } + GroupItem(GroupItem item) { + this.privateGroup = item.privateGroup; + this.authorInfo = item.authorInfo; + this.messageCount = item.messageCount; + this.unreadCount = item.unreadCount; + this.timestamp = item.timestamp; + this.dissolved = item.dissolved; + } + void addMessageHeader(GroupMessageHeader header) { messageCount++; if (header.getTimestamp() > timestamp) { @@ -38,10 +49,6 @@ class GroupItem { } } - PrivateGroup getPrivateGroup() { - return privateGroup; - } - GroupId getId() { return privateGroup.getId(); } @@ -82,4 +89,22 @@ class GroupItem { dissolved = true; } + @Override + public boolean equals(@Nullable Object o) { + return o instanceof GroupItem && + getId().equals(((GroupItem) o).getId()); + } + + @Override + public int compareTo(GroupItem o) { + if (this == o) return 0; + // The group with the latest message comes first + long aTime = getTimestamp(), bTime = o.getTimestamp(); + if (aTime > bTime) return -1; + if (aTime < bTime) return 1; + // Break ties by group name + String aName = getName(); + String bName = o.getName(); + return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListAdapter.java index d6fb8cc35..a2a4805bc 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListAdapter.java @@ -1,81 +1,52 @@ package org.briarproject.briar.android.privategroup.list; -import android.content.Context; import android.view.LayoutInflater; 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.bramble.api.sync.GroupId; import org.briarproject.briar.R; import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener; -import org.briarproject.briar.android.util.BriarAdapter; -import static androidx.recyclerview.widget.SortedList.INVALID_POSITION; +import androidx.recyclerview.widget.DiffUtil.ItemCallback; +import androidx.recyclerview.widget.ListAdapter; @MethodsNotNullByDefault @ParametersNotNullByDefault -class GroupListAdapter extends BriarAdapter { +class GroupListAdapter extends ListAdapter { private final OnGroupRemoveClickListener listener; - GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) { - super(ctx, GroupItem.class); + GroupListAdapter(OnGroupRemoveClickListener listener) { + super(new GroupItemCallback()); this.listener = listener; } @Override public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(ctx).inflate( - R.layout.list_item_group, parent, false); + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_item_group, parent, false); return new GroupViewHolder(v); } @Override public void onBindViewHolder(GroupViewHolder ui, int position) { - ui.bindView(ctx, items.get(position), listener); + ui.bindView(getItem(position), listener); } - @Override - public int compare(GroupItem a, GroupItem b) { - if (a == b) return 0; - // The group with the latest message comes first - long aTime = a.getTimestamp(), bTime = b.getTimestamp(); - if (aTime > bTime) return -1; - if (aTime < bTime) return 1; - // Break ties by group name - String aName = a.getName(); - String bName = b.getName(); - return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); - } - - @Override - public boolean areContentsTheSame(GroupItem a, GroupItem b) { - return a.getMessageCount() == b.getMessageCount() && - a.getTimestamp() == b.getTimestamp() && - a.getUnreadCount() == b.getUnreadCount() && - a.isDissolved() == b.isDissolved(); - } - - @Override - public boolean areItemsTheSame(GroupItem a, GroupItem b) { - return a.getId().equals(b.getId()); - } - - int findItemPosition(GroupId g) { - for (int i = 0; i < items.size(); i++) { - GroupItem item = items.get(i); - if (item.getId().equals(g)) { - return i; - } + private static class GroupItemCallback extends ItemCallback { + @Override + public boolean areItemsTheSame(GroupItem a, GroupItem b) { + return a.equals(b); } - return INVALID_POSITION; - } - void removeItem(GroupId groupId) { - int pos = findItemPosition(groupId); - if (pos != INVALID_POSITION) items.removeItemAt(pos); + @Override + public boolean areContentsTheSame(GroupItem a, GroupItem b) { + return a.getMessageCount() == b.getMessageCount() && + a.getTimestamp() == b.getTimestamp() && + a.getUnreadCount() == b.getUnreadCount() && + a.isDissolved() == b.isDissolved(); + } } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListController.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListController.java deleted file mode 100644 index 47763f9d8..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListController.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.briarproject.briar.android.privategroup.list; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.briar.android.controller.DbController; -import org.briarproject.briar.android.controller.handler.ExceptionHandler; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.privategroup.GroupMessageHeader; - -import java.util.Collection; - -import androidx.annotation.UiThread; - -@NotNullByDefault -interface GroupListController extends DbController { - - /** - * The listener must be set right after the controller was injected - */ - @UiThread - void setGroupListListener(GroupListListener listener); - - @UiThread - void unsetGroupListListener(GroupListListener listener); - - @UiThread - void onStart(); - - @UiThread - void onStop(); - - void loadGroups( - ResultExceptionHandler, DbException> result); - - void removeGroup(GroupId g, ExceptionHandler result); - - void loadAvailableGroups( - ResultExceptionHandler result); - - interface GroupListListener { - - @UiThread - void onGroupMessageAdded(GroupMessageHeader header); - - @UiThread - void onGroupInvitationReceived(); - - @UiThread - void onGroupAdded(GroupId groupId); - - @UiThread - void onGroupRemoved(GroupId groupId); - - @UiThread - void onGroupDissolved(GroupId groupId); - - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java index 98cb53f90..b973081ed 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListFragment.java @@ -15,54 +15,50 @@ import com.google.android.material.snackbar.Snackbar; import org.briarproject.bramble.api.db.DbException; 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.ActivityComponent; -import org.briarproject.briar.android.controller.handler.UiExceptionHandler; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity; import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity; -import org.briarproject.briar.android.privategroup.list.GroupListController.GroupListListener; import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; -import org.briarproject.briar.api.privategroup.GroupMessageHeader; -import java.util.Collection; -import java.util.logging.Logger; +import java.util.List; 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.Objects.requireNonNull; @MethodsNotNullByDefault @ParametersNotNullByDefault public class GroupListFragment extends BaseFragment implements - GroupListListener, OnGroupRemoveClickListener, OnClickListener { + OnGroupRemoveClickListener, OnClickListener { public final static String TAG = GroupListFragment.class.getName(); - private static final Logger LOG = Logger.getLogger(TAG); public static GroupListFragment newInstance() { return new GroupListFragment(); } @Inject - GroupListController controller; + ViewModelProvider.Factory viewModelFactory; + private GroupListViewModel viewModel; private BriarRecyclerView list; private GroupListAdapter adapter; - private Snackbar snackbar; @Override public void injectFragment(ActivityComponent component) { component.inject(this); - controller.setGroupListListener(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(GroupListViewModel.class); } @Nullable @@ -75,17 +71,35 @@ public class GroupListFragment extends BaseFragment implements View v = inflater.inflate(R.layout.list, container, false); - adapter = new GroupListAdapter(getActivity(), this); + adapter = new GroupListAdapter(this); list = v.findViewById(R.id.list); list.setEmptyImage(R.drawable.ic_empty_state_group_list); list.setEmptyText(R.string.groups_list_empty); list.setEmptyAction(R.string.groups_list_empty_action); list.setLayoutManager(new LinearLayoutManager(getContext())); list.setAdapter(adapter); + viewModel.getGroupItems().observe(getViewLifecycleOwner(), result -> { + List items = result.getResultOrNull(); + if (items == null && result.getException() instanceof DbException) { + handleDbException((DbException) result.getException()); + } else { + adapter.submitList(items); + if (requireNonNull(items).size() == 0) list.showData(); + } + }); - snackbar = new BriarSnackbarBuilder() + Snackbar 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.groups_invitations_open, num, num)); + if (!snackbar.isShownOrQueued()) snackbar.show(); + } + }); return v; } @@ -93,25 +107,22 @@ public class GroupListFragment extends BaseFragment implements @Override public void onStart() { super.onStart(); - controller.onStart(); + // TODO should we block all group message notifications as well? + viewModel.clearAllGroupMessageNotifications(); + // The attributes and sorting of the groups may have changed while we + // were stopped and we have no way finding out about them, so re-load + // e.g. less unread messages in a group after viewing it. + viewModel.loadGroups(); + // The number of invitations might have changed while we were stopped + // e.g. because of accepting an invitation which does not trigger event + viewModel.loadNumInvitations(); list.startPeriodicUpdate(); - loadGroups(); - loadAvailableGroups(); } @Override public void onStop() { super.onStop(); - controller.onStop(); list.stopPeriodicUpdate(); - adapter.clear(); - list.showProgressBar(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - controller.unsetGroupListListener(this); } @Override @@ -122,68 +133,18 @@ public class GroupListFragment extends BaseFragment implements @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add_group: - Intent i = new Intent(getContext(), CreateGroupActivity.class); - startActivity(i); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == R.id.action_add_group) { + Intent i = new Intent(getContext(), CreateGroupActivity.class); + startActivity(i); + return true; } + return super.onOptionsItemSelected(item); } @UiThread @Override public void onGroupRemoveClick(GroupItem item) { - controller.removeGroup(item.getId(), - new UiExceptionHandler(this) { - // result handled by GroupRemovedEvent and onGroupRemoved() - @Override - public void onExceptionUi(DbException exception) { - handleDbException(exception); - } - }); - } - - @UiThread - @Override - public void onGroupMessageAdded(GroupMessageHeader header) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(header.getGroupId()); - GroupItem item = adapter.getItemAt(position); - if (item != null) { - item.addMessageHeader(header); - adapter.updateItemAt(position, item); - } - } - - @Override - public void onGroupInvitationReceived() { - loadAvailableGroups(); - } - - @UiThread - @Override - public void onGroupAdded(GroupId groupId) { - loadGroups(); - } - - @UiThread - @Override - public void onGroupRemoved(GroupId groupId) { - adapter.incrementRevision(); - adapter.removeItem(groupId); - } - - @Override - public void onGroupDissolved(GroupId groupId) { - adapter.incrementRevision(); - int position = adapter.findItemPosition(groupId); - GroupItem item = adapter.getItemAt(position); - if (item != null) { - item.setDissolved(); - adapter.updateItemAt(position, item); - } + viewModel.removeGroup(item.getId()); } @Override @@ -191,57 +152,13 @@ public class GroupListFragment extends BaseFragment implements return TAG; } - private void loadGroups() { - int revision = adapter.getRevision(); - controller.loadGroups( - new UiResultExceptionHandler, DbException>( - this) { - @Override - public void onResultUi(Collection groups) { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (groups.isEmpty()) list.showData(); - else adapter.replaceAll(groups); - } else { - LOG.info("Concurrent update, reloading"); - loadGroups(); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleDbException(exception); - } - }); - } - - private void loadAvailableGroups() { - controller.loadAvailableGroups( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Integer num) { - if (num == 0) { - snackbar.dismiss(); - } else { - snackbar.setText(getResources().getQuantityString( - R.plurals.groups_invitations_open, num, - num)); - if (!snackbar.isShownOrQueued()) snackbar.show(); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleDbException(exception); - } - }); - } - /** * This method is handling the available groups snackbar action */ @Override public void onClick(View v) { + // The snackbar dismisses itself when this is called + // and does not come back until the fragment gets recreated. Intent i = new Intent(getContext(), GroupInvitationActivity.class); startActivity(i); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListModule.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListModule.java index fcf9c09d1..d8cb791a2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListModule.java @@ -1,17 +1,18 @@ package org.briarproject.briar.android.privategroup.list; -import org.briarproject.briar.android.activity.ActivityScope; +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 GroupListModule { +public abstract class GroupListModule { - @ActivityScope - @Provides - GroupListController provideGroupListController( - GroupListControllerImpl groupListController) { - return groupListController; - } + @Binds + @IntoMap + @ViewModelKey(GroupListViewModel.class) + abstract ViewModel bindGroupListViewModel( + GroupListViewModel groupListViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java similarity index 64% rename from briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListControllerImpl.java rename to briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java index 5119407eb..1266fd47f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupListViewModel.java @@ -1,5 +1,7 @@ package org.briarproject.briar.android.privategroup.list; +import android.app.Application; + import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; @@ -18,11 +20,11 @@ import org.briarproject.bramble.api.sync.ClientId; 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.android.controller.DbControllerImpl; -import org.briarproject.briar.android.controller.handler.ExceptionHandler; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; +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.privategroup.GroupMessageHeader; import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroupManager; import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent; @@ -32,6 +34,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,10 +43,13 @@ import java.util.logging.Logger; import javax.inject.Inject; -import androidx.annotation.CallSuper; -import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import static java.util.Objects.requireNonNull; 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; @@ -51,113 +57,86 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT @MethodsNotNullByDefault @ParametersNotNullByDefault -class GroupListControllerImpl extends DbControllerImpl - implements GroupListController, EventListener { +class GroupListViewModel extends DbViewModel implements EventListener { private static final Logger LOG = - Logger.getLogger(GroupListControllerImpl.class.getName()); + getLogger(GroupListViewModel.class.getName()); - private final TransactionManager db; private final PrivateGroupManager groupManager; private final GroupInvitationManager groupInvitationManager; private final ContactManager contactManager; private final AndroidNotificationManager notificationManager; private final EventBus eventBus; - // UI thread - @Nullable - private GroupListListener listener; + private final MutableLiveData>> groupItems = + new MutableLiveData<>(); + private final MutableLiveData numInvitations = + new MutableLiveData<>(); @Inject - GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor, + GroupListViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, TransactionManager db, PrivateGroupManager groupManager, GroupInvitationManager groupInvitationManager, ContactManager contactManager, AndroidNotificationManager notificationManager, EventBus eventBus) { - super(dbExecutor, lifecycleManager); - this.db = db; + super(application, dbExecutor, lifecycleManager, db); this.groupManager = groupManager; this.groupInvitationManager = groupInvitationManager; this.contactManager = contactManager; this.notificationManager = notificationManager; this.eventBus = eventBus; + this.eventBus.addListener(this); } @Override - public void setGroupListListener(GroupListListener listener) { - this.listener = listener; + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); } - @Override - public void unsetGroupListListener(GroupListListener listener) { - if (this.listener == listener) this.listener = null; - } - - @Override - @CallSuper - public void onStart() { - if (listener == null) throw new IllegalStateException(); - eventBus.addListener(this); + void clearAllGroupMessageNotifications() { notificationManager.clearAllGroupMessageNotifications(); } @Override - @CallSuper - public void onStop() { - eventBus.removeListener(this); - } - - @Override - @CallSuper public void eventOccurred(Event e) { - if (listener == null) throw new IllegalStateException(); if (e instanceof GroupMessageAddedEvent) { GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; LOG.info("Private group message added"); - listener.onGroupMessageAdded(g.getHeader()); + onGroupMessageAdded(g.getHeader()); } else if (e instanceof GroupInvitationRequestReceivedEvent) { LOG.info("Private group invitation received"); - listener.onGroupInvitationReceived(); + loadNumInvitations(); } else if (e instanceof GroupAddedEvent) { GroupAddedEvent g = (GroupAddedEvent) e; ClientId id = g.getGroup().getClientId(); if (id.equals(CLIENT_ID)) { LOG.info("Private group added"); - listener.onGroupAdded(g.getGroup().getId()); + loadGroups(); } } else if (e instanceof GroupRemovedEvent) { GroupRemovedEvent g = (GroupRemovedEvent) e; ClientId id = g.getGroup().getClientId(); if (id.equals(CLIENT_ID)) { LOG.info("Private group removed"); - listener.onGroupRemoved(g.getGroup().getId()); + onGroupRemoved(g.getGroup().getId()); } } else if (e instanceof GroupDissolvedEvent) { GroupDissolvedEvent g = (GroupDissolvedEvent) e; LOG.info("Private group dissolved"); - listener.onGroupDissolved(g.getGroupId()); + onGroupDissolved(g.getGroupId()); } } - @Override - public void loadGroups( - ResultExceptionHandler, DbException> handler) { - runOnDbThread(() -> { - try { - db.transaction(true, txn -> loadGroups(txn, handler)); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); + void loadGroups() { + loadList(this::loadGroups, groupItems::setValue); } @DatabaseExecutor - private void loadGroups(Transaction txn, - ResultExceptionHandler, DbException> handler) - throws DbException { + private List loadGroups(Transaction txn) throws DbException { long start = now(); Collection groups = groupManager.getPrivateGroups(txn); List items = new ArrayList<>(groups.size()); @@ -168,7 +147,7 @@ class GroupListControllerImpl extends DbControllerImpl AuthorId authorId = g.getCreator().getId(); AuthorInfo authorInfo; if (authorInfos.containsKey(authorId)) { - authorInfo = authorInfos.get(authorId); + authorInfo = requireNonNull(authorInfos.get(authorId)); } else { authorInfo = contactManager.getAuthorInfo(txn, authorId); authorInfos.put(authorId, authorInfo); @@ -180,12 +159,49 @@ class GroupListControllerImpl extends DbControllerImpl // Continue } } + Collections.sort(items); logDuration(LOG, "Loading groups", start); - handler.onResult(items); + return items; } - @Override - public void removeGroup(GroupId g, ExceptionHandler handler) { + @UiThread + private void onGroupMessageAdded(GroupMessageHeader header) { + GroupId g = header.getGroupId(); + List list = updateListItem(groupItems, + itemToTest -> itemToTest.getId().equals(g), + itemToUpdate -> { + GroupItem newItem = new GroupItem(itemToUpdate); + newItem.addMessageHeader(header); + return newItem; + }); + if (list == null) return; + // re-sort as the order of items may have changed + Collections.sort(list); + groupItems.setValue(new LiveResult<>(list)); + } + + @UiThread + private void onGroupDissolved(GroupId groupId) { + List list = updateListItem(groupItems, + itemToTest -> itemToTest.getId().equals(groupId), + itemToUpdate -> { + GroupItem newItem = new GroupItem(itemToUpdate); + newItem.setDissolved(); + return newItem; + }); + if (list == null) return; + groupItems.setValue(new LiveResult<>(list)); + } + + @UiThread + private void onGroupRemoved(GroupId groupId) { + List list = + removeListItem(groupItems, i -> i.getId().equals(groupId)); + if (list == null) return; + groupItems.setValue(new LiveResult<>(list)); + } + + void removeGroup(GroupId g) { runOnDbThread(() -> { try { long start = now(); @@ -193,23 +209,26 @@ class GroupListControllerImpl extends DbControllerImpl logDuration(LOG, "Removing group", start); } catch (DbException e) { logException(LOG, WARNING, e); - handler.onException(e); } }); } - @Override - public void loadAvailableGroups( - ResultExceptionHandler handler) { + void loadNumInvitations() { runOnDbThread(() -> { try { - handler.onResult( - groupInvitationManager.getInvitations().size()); + int i = groupInvitationManager.getInvitations().size(); + numInvitations.postValue(i); } catch (DbException e) { logException(LOG, WARNING, e); - handler.onException(e); } }); } + LiveData>> getGroupItems() { + return groupItems; + } + + LiveData getNumInvitations() { + return numInvitations; + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupViewHolder.java index f1c50eefb..4179b4061 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/list/GroupViewHolder.java @@ -29,6 +29,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder { private final static float ALPHA = 0.42f; + private final Context ctx; private final ViewGroup layout; private final TextAvatarView avatar; private final TextView name; @@ -40,7 +41,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder { GroupViewHolder(View v) { super(v); - + ctx = v.getContext(); layout = (ViewGroup) v; avatar = v.findViewById(R.id.avatarView); name = v.findViewById(R.id.nameView); @@ -51,8 +52,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder { remove = v.findViewById(R.id.removeButton); } - void bindView(Context ctx, GroupItem group, - OnGroupRemoveClickListener listener) { + void bindView(GroupItem group, OnGroupRemoveClickListener listener) { // Avatar avatar.setText(group.getName().substring(0, 1)); avatar.setBackgroundBytes(group.getId().getBytes());