Move thread list events, fields and notification handling into ViewModels

This commit is contained in:
Torsten Grote
2021-01-08 11:14:11 -03:00
parent db53e79d1d
commit 1c107a851b
13 changed files with 215 additions and 263 deletions

View File

@@ -26,7 +26,6 @@ import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM;
@@ -63,6 +62,11 @@ public class ForumActivity extends
return viewModel; return viewModel;
} }
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
return new ThreadItemAdapter<>(this);
}
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
@@ -91,9 +95,9 @@ public class ForumActivity extends
} }
@Override @Override
protected ThreadItemAdapter<ForumPostItem> createAdapter( public void onStart() {
LinearLayoutManager layoutManager) { super.onStart();
return new ThreadItemAdapter<>(this, layoutManager); viewModel.clearForumPostNotification();
} }
@Override @Override

View File

@@ -17,10 +17,8 @@ import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.forum.ForumInvitationResponse; import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.forum.ForumManager; 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.ForumSharingManager;
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent; 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 org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.ArrayList; import java.util.ArrayList;
@@ -59,7 +57,6 @@ class ForumControllerImpl extends ThreadListControllerImpl<ForumPostItem>
@Override @Override
public void onActivityStart() { public void onActivityStart() {
super.onActivityStart(); super.onActivityStart();
notificationManager.clearForumPostNotification(getGroupId());
} }
@Override @Override
@@ -68,13 +65,7 @@ class ForumControllerImpl extends ThreadListControllerImpl<ForumPostItem>
ForumListener listener = (ForumListener) this.listener; ForumListener listener = (ForumListener) this.listener;
if (e instanceof ForumPostReceivedEvent) { if (e instanceof ForumInvitationResponseReceivedEvent) {
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) {
ForumInvitationResponseReceivedEvent f = ForumInvitationResponseReceivedEvent f =
(ForumInvitationResponseReceivedEvent) e; (ForumInvitationResponseReceivedEvent) e;
ForumInvitationResponse r = f.getMessageHeader(); ForumInvitationResponse r = f.getMessageHeader();
@@ -114,8 +105,4 @@ class ForumControllerImpl extends ThreadListControllerImpl<ForumPostItem>
}); });
} }
private ForumPostItem buildItem(ForumPostHeader header, String text) {
return new ForumPostItem(header, text);
}
} }

View File

@@ -29,6 +29,7 @@ import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumPost; import org.briarproject.briar.api.forum.ForumPost;
import org.briarproject.briar.api.forum.ForumPostHeader; import org.briarproject.briar.api.forum.ForumPostHeader;
import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -80,7 +81,20 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
@Override @Override
public void eventOccurred(Event e) { 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<Forum> loadForum() { LiveData<Forum> loadForum() {
@@ -141,7 +155,7 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
long start = now(); long start = now();
ForumPostHeader header = forumManager.addLocalPost(msg); ForumPostHeader header = forumManager.addLocalPost(msg);
textCache.put(msg.getMessage().getId(), text); textCache.put(msg.getMessage().getId(), text);
addItem(buildItem(header, text), true); addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing forum post", start); logDuration(LOG, "Storing forum post", start);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);

View File

@@ -7,13 +7,11 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import org.briarproject.bramble.api.contact.ContactId; 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.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; 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.conversation.GroupController.GroupListener;
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity; import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity; import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
@@ -29,7 +27,6 @@ import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@@ -50,9 +47,6 @@ public class GroupActivity extends
private GroupViewModel viewModel; private GroupViewModel viewModel;
@Nullable
private Boolean isCreator = null;
private boolean isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem, private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem; dissolveMenuItem;
@@ -73,6 +67,11 @@ public class GroupActivity extends
return viewModel; return viewModel;
} }
@Override
protected GroupMessageAdapter createAdapter() {
return new GroupMessageAdapter(this);
}
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
@@ -85,11 +84,7 @@ public class GroupActivity extends
observeOnce(viewModel.getPrivateGroup(), this, privateGroup -> observeOnce(viewModel.getPrivateGroup(), this, privateGroup ->
setTitle(privateGroup.getName()) setTitle(privateGroup.getName())
); );
observeOnce(viewModel.isCreator(), this, isCreator -> { observeOnce(viewModel.isCreator(), this, adapter::setIsCreator);
this.isCreator = isCreator; // TODO remove field
adapter.setPerspective(isCreator);
showMenuItems();
});
// Open member list on Toolbar click // Open member list on Toolbar click
if (toolbar != null) { if (toolbar != null) {
@@ -101,25 +96,18 @@ public class GroupActivity extends
}); });
} }
// start with group disabled and enable when not dissolved
setGroupEnabled(false); setGroupEnabled(false);
controller.isDissolved( viewModel.isDissolved().observe(this, dissolved -> {
new UiResultExceptionHandler<Boolean, DbException>(this) { setGroupEnabled(!dissolved);
@Override if (dissolved) onGroupDissolved();
public void onResultUi(Boolean isDissolved) { });
setGroupEnabled(!isDissolved);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
} }
@Override @Override
protected GroupMessageAdapter createAdapter( public void onStart() {
LinearLayoutManager layoutManager) { super.onStart();
return new GroupMessageAdapter(this, layoutManager); viewModel.clearGroupMessageNotifications();
} }
@Override @Override
@@ -139,8 +127,13 @@ public class GroupActivity extends
leaveMenuItem.setVisible(false); leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(false); dissolveMenuItem.setVisible(false);
// show items based on role // show items based on role (which will not change, so observe once)
showMenuItems(); observeOnce(viewModel.isCreator(), this, isCreator -> {
revealMenuItem.setVisible(!isCreator);
inviteMenuItem.setVisible(isCreator);
leaveMenuItem.setVisible(!isCreator);
dissolveMenuItem.setVisible(isCreator);
});
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -154,26 +147,26 @@ public class GroupActivity extends
startActivity(i1); startActivity(i1);
return true; return true;
} else if (itemId == R.id.action_group_reveal) { } else if (itemId == R.id.action_group_reveal) {
if (isCreator == null || isCreator) if (viewModel.isCreator().getValue())
throw new IllegalStateException(); throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class); Intent i2 = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes()); i2.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2); startActivity(i2);
return true; return true;
} else if (itemId == R.id.action_group_invite) { } else if (itemId == R.id.action_group_invite) {
if (isCreator == null || !isCreator) if (!viewModel.isCreator().getValue())
throw new IllegalStateException(); throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class); Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes()); i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE); startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true; return true;
} else if (itemId == R.id.action_group_leave) { } else if (itemId == R.id.action_group_leave) {
if (isCreator == null || isCreator) if (viewModel.isCreator().getValue())
throw new IllegalStateException(); throw new IllegalStateException();
showLeaveGroupDialog(); showLeaveGroupDialog();
return true; return true;
} else if (itemId == R.id.action_group_dissolve) { } else if (itemId == R.id.action_group_dissolve) {
if (isCreator == null || !isCreator) if (!viewModel.isCreator().getValue())
throw new IllegalStateException(); throw new IllegalStateException();
showDissolveGroupDialog(); showDissolveGroupDialog();
@@ -190,14 +183,6 @@ public class GroupActivity extends
} else super.onActivityResult(request, result, data); } 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 @Override
protected int getMaxTextLength() { protected int getMaxTextLength() {
return MAX_GROUP_POST_TEXT_LENGTH; return MAX_GROUP_POST_TEXT_LENGTH;
@@ -205,11 +190,10 @@ public class GroupActivity extends
@Override @Override
public void onReplyClick(GroupMessageItem item) { public void onReplyClick(GroupMessageItem item) {
if (!isDissolved) super.onReplyClick(item); if (!viewModel.isDissolved().getValue()) super.onReplyClick(item);
} }
private void setGroupEnabled(boolean enabled) { private void setGroupEnabled(boolean enabled) {
isDissolved = !enabled;
sendController.setReady(enabled); sendController.setReady(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f); 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() { private void showLeaveGroupDialog() {
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme); new AlertDialog.Builder(this, R.style.BriarDialogTheme);
@@ -268,7 +243,6 @@ public class GroupActivity extends
sharingController.getOnlineCount()); sharingController.getOnlineCount());
} }
@Override
public void onGroupDissolved() { public void onGroupDissolved() {
setGroupEnabled(false); setGroupEnabled(false);
AlertDialog.Builder builder = AlertDialog.Builder builder =

View File

@@ -1,9 +1,7 @@
package org.briarproject.briar.android.privategroup.conversation; package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.bramble.api.contact.ContactId; 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.identity.AuthorId;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadListController; import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.privategroup.Visibility; import org.briarproject.briar.api.privategroup.Visibility;
@@ -12,17 +10,10 @@ import androidx.annotation.UiThread;
public interface GroupController public interface GroupController
extends ThreadListController<GroupMessageItem> { extends ThreadListController<GroupMessageItem> {
void isDissolved(
ResultExceptionHandler<Boolean, DbException> handler);
interface GroupListener extends ThreadListListener<GroupMessageItem> { interface GroupListener extends ThreadListListener<GroupMessageItem> {
@UiThread @UiThread
void onContactRelationshipRevealed(AuthorId memberId, void onContactRelationshipRevealed(AuthorId memberId,
ContactId contactId, Visibility v); ContactId contactId, Visibility v);
@UiThread
void onGroupDissolved();
} }
} }

View File

@@ -16,13 +16,9 @@ import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadListControllerImpl; import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.privategroup.GroupMember; 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.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent; 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.GroupInvitationResponseReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
import java.util.ArrayList; import java.util.ArrayList;
@@ -61,7 +57,6 @@ class GroupControllerImpl extends ThreadListControllerImpl<GroupMessageItem>
@Override @Override
public void onActivityStart() { public void onActivityStart() {
super.onActivityStart(); super.onActivityStart();
notificationManager.clearGroupMessageNotification(getGroupId());
} }
@Override @Override
@@ -70,13 +65,7 @@ class GroupControllerImpl extends ThreadListControllerImpl<GroupMessageItem>
GroupListener listener = (GroupListener) this.listener; GroupListener listener = (GroupListener) this.listener;
if (e instanceof GroupMessageAddedEvent) { if (e instanceof ContactRelationshipRevealedEvent) {
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) {
ContactRelationshipRevealedEvent c = ContactRelationshipRevealedEvent c =
(ContactRelationshipRevealedEvent) e; (ContactRelationshipRevealedEvent) e;
if (getGroupId().equals(c.getGroupId())) { if (getGroupId().equals(c.getGroupId())) {
@@ -90,11 +79,6 @@ class GroupControllerImpl extends ThreadListControllerImpl<GroupMessageItem>
if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) { if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) {
listener.onInvitationAccepted(g.getContactId()); 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<GroupMessageItem>
}); });
} }
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<Boolean, DbException> handler) {
runOnDbThread(() -> {
try {
boolean isDissolved =
privateGroupManager.isDissolved(getGroupId());
handler.onResult(isDissolved);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
} }

View File

@@ -14,7 +14,6 @@ import org.briarproject.briar.api.privategroup.Visibility;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@@ -24,9 +23,8 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false; private boolean isCreator = false;
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener, GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
LinearLayoutManager layoutManager) { super(listener);
super(listener, layoutManager);
} }
@LayoutRes @LayoutRes
@@ -47,7 +45,7 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return new ThreadPostViewHolder<>(v); return new ThreadPostViewHolder<>(v);
} }
void setPerspective(boolean isCreator) { void setIsCreator(boolean isCreator) {
this.isCreator = isCreator; this.isCreator = isCreator;
notifyDataSetChanged(); notifyDataSetChanged();
} }

View File

@@ -30,6 +30,8 @@ import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.JoinMessageHeader; import org.briarproject.briar.api.privategroup.JoinMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager; 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.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -60,6 +62,8 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
private final MutableLiveData<PrivateGroup> privateGroup = private final MutableLiveData<PrivateGroup> privateGroup =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<Boolean> isCreator = new MutableLiveData<>(); private final MutableLiveData<Boolean> isCreator = new MutableLiveData<>();
private final MutableLiveData<Boolean> isDissolved =
new MutableLiveData<>();
@Inject @Inject
GroupViewModel(Application application, GroupViewModel(Application application,
@@ -84,7 +88,26 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
@Override @Override
public void eventOccurred(Event e) { 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 @Override
@@ -93,6 +116,10 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
loadPrivateGroup(groupId); loadPrivateGroup(groupId);
} }
public void clearGroupMessageNotifications() {
notificationManager.clearGroupMessageNotification(groupId);
}
private void loadPrivateGroup(GroupId groupId) { private void loadPrivateGroup(GroupId groupId) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
@@ -109,7 +136,10 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
@Override @Override
public void loadItems() { public void loadItems() {
loadList(txn -> { 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(); long start = now();
List<GroupMessageHeader> headers = List<GroupMessageHeader> headers =
privateGroupManager.getHeaders(txn, groupId); privateGroupManager.getHeaders(txn, groupId);
@@ -173,7 +203,7 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
GroupMessageHeader header = GroupMessageHeader header =
privateGroupManager.addLocalMessage(msg); privateGroupManager.addLocalMessage(msg);
textCache.put(msg.getMessage().getId(), text); textCache.put(msg.getMessage().getId(), text);
addItem(buildItem(header, text), true); addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing group message", start); logDuration(LOG, "Storing group message", start);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
@@ -199,4 +229,8 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
return isCreator; return isCreator;
} }
LiveData<Boolean> isDissolved() {
return isDissolved;
}
} }

View File

@@ -30,10 +30,8 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1; static final int UNDEFINED = -1;
private final ThreadItemListener<I> listener; private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager;
public ThreadItemAdapter(ThreadItemListener<I> listener, public ThreadItemAdapter(ThreadItemListener<I> listener) {
LinearLayoutManager layoutManager) {
super(new DiffUtil.ItemCallback<I>() { super(new DiffUtil.ItemCallback<I>() {
@Override @Override
public boolean areItemsTheSame(I a, I b) { public boolean areItemsTheSame(I a, I b) {
@@ -47,7 +45,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
} }
}); });
this.listener = listener; this.listener = listener;
this.layoutManager = layoutManager;
} }
@NonNull @NonNull
@@ -101,10 +98,17 @@ public class ThreadItemAdapter<I extends ThreadItem>
return null; 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 * Returns the position of the first unread item below the current viewport
*/ */
int getVisibleUnreadPosBottom() { int getVisibleUnreadPosBottom(LinearLayoutManager layoutManager) {
int positionBottom = layoutManager.findLastVisibleItemPosition(); int positionBottom = layoutManager.findLastVisibleItemPosition();
if (positionBottom == NO_POSITION) return NO_POSITION; if (positionBottom == NO_POSITION) return NO_POSITION;
for (int i = positionBottom + 1; i < getItemCount(); i++) { for (int i = positionBottom + 1; i < getItemCount(); i++) {
@@ -116,7 +120,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
/** /**
* Returns the position of the first unread item above the current viewport * Returns the position of the first unread item above the current viewport
*/ */
int getVisibleUnreadPosTop() { int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) {
int positionTop = layoutManager.findFirstVisibleItemPosition(); int positionTop = layoutManager.findFirstVisibleItemPosition();
int position = NO_POSITION; int position = NO_POSITION;
for (int i = 0; i < getItemCount(); i++) { for (int i = 0; i < getItemCount(); i++) {

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.threaded;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.MenuItem; import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar; 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.SharingController.SharingListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; 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.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; 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.Collection;
import java.util.List; import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
@@ -42,7 +39,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -50,30 +46,22 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>> public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity extends BriarActivity
implements ThreadListListener<I>, SendListener, SharingListener, implements ThreadListListener<I>, SendListener, SharingListener,
ThreadItemListener<I>, ThreadListDataSource { ThreadItemListener<I> {
protected static final String KEY_REPLY_ID = "replyId";
private static final Logger LOG =
getLogger(ThreadListActivity.class.getName());
protected A adapter;
protected final A adapter = createAdapter();
private ThreadScrollListener<I> scrollListener; private ThreadScrollListener<I> scrollListener;
protected BriarRecyclerView list; protected BriarRecyclerView list;
private LinearLayoutManager layoutManager; private LinearLayoutManager layoutManager;
protected TextInputView textInput; protected TextInputView textInput;
protected TextSendController sendController; protected TextSendController sendController;
protected GroupId groupId; protected GroupId groupId;
@Nullable
private Parcelable layoutManagerState;
@Nullable
private MessageId replyId;
protected abstract ThreadListController<I> getController(); protected abstract ThreadListController<I> getController();
protected abstract ThreadListViewModel<I> getViewModel(); protected abstract ThreadListViewModel<I> getViewModel();
protected abstract A createAdapter();
@Inject @Inject
protected SharingController sharingController; protected SharingController sharingController;
@@ -103,59 +91,85 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
list = findViewById(R.id.list); list = findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this); layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
scrollListener = new ThreadScrollListener<>(adapter, getController(), scrollListener = new ThreadScrollListener<>(adapter, getController(),
upButton, downButton); upButton, downButton);
list.getRecyclerView().addOnScrollListener(scrollListener); list.getRecyclerView().addOnScrollListener(scrollListener);
upButton.setOnClickListener(v -> { upButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosTop(); int position = adapter.getVisibleUnreadPosTop(layoutManager);
if (position != NO_POSITION) { if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position); list.getRecyclerView().scrollToPosition(position);
} }
}); });
downButton.setOnClickListener(v -> { downButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosBottom(); int position = adapter.getVisibleUnreadPosBottom(layoutManager);
if (position != NO_POSITION) { if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(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 getViewModel().getItems().observe(this, result -> result
.onError(this::handleException) .onError(this::handleException)
.onSuccess(this::displayItems) .onSuccess(this::displayItems)
); );
getViewModel().getGroupRemoved().observe(this, removed -> {
if (removed) supportFinishAfterTransition();
});
sharingController.setSharingListener(this); sharingController.setSharingListener(this);
loadSharingContacts(); 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 @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
getViewModel().storeMessageId(getFirstVisibleMessageId()); // store list position, so we can restore it when coming back here
}
@Override
@Nullable
public MessageId getFirstVisibleMessageId() {
if (layoutManager != null && adapter != null) { if (layoutManager != null && adapter != null) {
int position = MessageId id = adapter.getFirstVisibleMessageId(layoutManager);
layoutManager.findFirstVisibleItemPosition(); getViewModel().storeMessageId(id);
I i = adapter.getItemAt(position);
return i == null ? null : i.getId();
} }
return null;
} }
protected abstract A createAdapter(LinearLayoutManager layoutManager);
protected void displayItems(List<I> items) { protected void displayItems(List<I> items) {
if (items.isEmpty()) { if (items.isEmpty()) {
list.showData(); list.showData();
@@ -201,58 +215,9 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
}); });
} }
@CallSuper
@Override
public void onStart() {
super.onStart();
sharingController.onStart();
list.startPeriodicUpdate();
}
@CallSuper
@Override
public void onStop() {
super.onStop();
sharingController.onStop();
list.stopPeriodicUpdate();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (replyId != null) {
outState.putByteArray(KEY_REPLY_ID, replyId.getBytes());
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
@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();
replyId = null;
updateTextInput();
} else {
super.onBackPressed();
}
}
@Override @Override
public void onReplyClick(I item) { public void onReplyClick(I item) {
replyId = item.getId(); getViewModel().setReplyId(item.getId());
updateTextInput(); updateTextInput();
// FIXME This does not work for a hardware keyboard // FIXME This does not work for a hardware keyboard
if (textInput.isKeyboardOpen()) { if (textInput.isKeyboardOpen()) {
@@ -300,6 +265,7 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
} }
private void updateTextInput() { private void updateTextInput() {
MessageId replyId = getViewModel().getReplyId();
if (replyId != null) { if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint); textInput.setHint(R.string.forum_message_reply_hint);
textInput.showSoftKeyboard(); textInput.showSoftKeyboard();
@@ -318,20 +284,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
getViewModel().createAndStoreMessage(text, replyItem); getViewModel().createAndStoreMessage(text, replyItem);
textInput.hideSoftKeyboard(); textInput.hideSoftKeyboard();
textInput.clearText(); textInput.clearText();
replyId = null; getViewModel().setReplyId(null);
updateTextInput(); updateTextInput();
} }
protected abstract int getMaxTextLength(); protected abstract int getMaxTextLength();
@Override
public void onItemReceived(I item) {
getViewModel().addItem(item, false);
}
@Override
public void onGroupRemoved() {
supportFinishAfterTransition();
}
} }

View File

@@ -4,14 +4,11 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection; import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
@NotNullByDefault @NotNullByDefault
@@ -27,23 +24,9 @@ public interface ThreadListController<I extends ThreadItem>
void markItemsRead(Collection<I> items); void markItemsRead(Collection<I> items);
interface ThreadListListener<I> extends ThreadListDataSource { interface ThreadListListener<I> {
@UiThread
void onItemReceived(I item);
@UiThread
void onGroupRemoved();
@UiThread @UiThread
void onInvitationAccepted(ContactId c); void onInvitationAccepted(ContactId c);
} }
interface ThreadListDataSource {
@UiThread
@Nullable
MessageId getFirstVisibleMessageId();
}
} }

View File

@@ -14,7 +14,6 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; 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.bramble.api.system.Clock;
import org.briarproject.briar.android.controller.DbControllerImpl; import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -78,14 +77,12 @@ public abstract class ThreadListControllerImpl<I extends ThreadItem>
@CallSuper @CallSuper
@Override @Override
public void onActivityStart() { public void onActivityStart() {
notificationManager.blockNotification(getGroupId());
eventBus.addListener(this); eventBus.addListener(this);
} }
@CallSuper @CallSuper
@Override @Override
public void onActivityStop() { public void onActivityStop() {
notificationManager.unblockNotification(getGroupId());
eventBus.removeListener(this); eventBus.removeListener(this);
} }
@@ -94,16 +91,8 @@ public abstract class ThreadListControllerImpl<I extends ThreadItem>
} }
@CallSuper
@Override @Override
public void eventOccurred(Event e) { 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 @Override

View File

@@ -7,6 +7,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager; 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.EventBus;
import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager; 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.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; 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.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.viewmodel.DbViewModel; import org.briarproject.briar.android.viewmodel.DbViewModel;
@@ -65,14 +67,18 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
@DatabaseExecutor @DatabaseExecutor
private final MessageTree<I> messageTree = new MessageTreeImpl<>(); private final MessageTree<I> messageTree = new MessageTreeImpl<>();
protected final Map<MessageId, String> textCache = protected final Map<MessageId, String> textCache = // TODO still needed?
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private final MutableLiveData<LiveResult<List<I>>> items = private final MutableLiveData<LiveResult<List<I>>> items =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<Boolean> groupRemoved =
new MutableLiveData<>();
private final AtomicReference<MessageId> scrollToItem = private final AtomicReference<MessageId> scrollToItem =
new AtomicReference<>(); new AtomicReference<>();
protected volatile GroupId groupId; protected volatile GroupId groupId;
@Nullable
private MessageId replyId;
private final AtomicReference<MessageId> storedMessageId = private final AtomicReference<MessageId> storedMessageId =
new AtomicReference<>(); new AtomicReference<>();
@@ -114,6 +120,26 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
loadItems(); 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() { private void loadStoredMessageId() {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
@@ -161,9 +187,24 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
return messageTree.depthFirstOrder(); 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); 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())); items.postValue(new LiveResult<>(messageTree.depthFirstOrder()));
} }
@@ -171,6 +212,17 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
protected abstract String loadMessageText(Transaction txn, protected abstract String loadMessageText(Transaction txn,
PostHeader header) throws DbException; 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) { void storeMessageId(@Nullable MessageId messageId) {
if (messageId != null) runOnDbThread(() -> { if (messageId != null) runOnDbThread(() -> {
try { try {
@@ -190,6 +242,10 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
return items; return items;
} }
LiveData<Boolean> getGroupRemoved() {
return groupRemoved;
}
@Nullable @Nullable
MessageId getAndResetScrollToItem() { MessageId getAndResetScrollToItem() {
return scrollToItem.getAndSet(null); return scrollToItem.getAndSet(null);