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.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<ForumPostItem> 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<ForumPostItem> createAdapter(
LinearLayoutManager layoutManager) {
return new ThreadItemAdapter<>(this, layoutManager);
public void onStart() {
super.onStart();
viewModel.clearForumPostNotification();
}
@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.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<ForumPostItem>
@Override
public void onActivityStart() {
super.onActivityStart();
notificationManager.clearForumPostNotification(getGroupId());
}
@Override
@@ -68,13 +65,7 @@ class ForumControllerImpl extends ThreadListControllerImpl<ForumPostItem>
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<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.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<ForumPostItem> {
@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<Forum> loadForum() {
@@ -141,7 +155,7 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
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);

View File

@@ -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<Boolean, DbException>(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 =

View File

@@ -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<GroupMessageItem> {
void isDissolved(
ResultExceptionHandler<Boolean, DbException> handler);
interface GroupListener extends ThreadListListener<GroupMessageItem> {
@UiThread
void onContactRelationshipRevealed(AuthorId memberId,
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.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<GroupMessageItem>
@Override
public void onActivityStart() {
super.onActivityStart();
notificationManager.clearGroupMessageNotification(getGroupId());
}
@Override
@@ -70,13 +65,7 @@ class GroupControllerImpl extends ThreadListControllerImpl<GroupMessageItem>
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<GroupMessageItem>
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<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.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@@ -24,9 +23,8 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false;
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener,
LinearLayoutManager layoutManager) {
super(listener, layoutManager);
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
super(listener);
}
@LayoutRes
@@ -47,7 +45,7 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return new ThreadPostViewHolder<>(v);
}
void setPerspective(boolean isCreator) {
void setIsCreator(boolean isCreator) {
this.isCreator = isCreator;
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.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<GroupMessageItem> {
private final MutableLiveData<PrivateGroup> privateGroup =
new MutableLiveData<>();
private final MutableLiveData<Boolean> isCreator = new MutableLiveData<>();
private final MutableLiveData<Boolean> isDissolved =
new MutableLiveData<>();
@Inject
GroupViewModel(Application application,
@@ -84,7 +88,26 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
@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<GroupMessageItem> {
loadPrivateGroup(groupId);
}
public void clearGroupMessageNotifications() {
notificationManager.clearGroupMessageNotification(groupId);
}
private void loadPrivateGroup(GroupId groupId) {
runOnDbThread(() -> {
try {
@@ -109,7 +136,10 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
@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<GroupMessageHeader> headers =
privateGroupManager.getHeaders(txn, groupId);
@@ -173,7 +203,7 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
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<GroupMessageItem> {
return isCreator;
}
LiveData<Boolean> isDissolved() {
return isDissolved;
}
}

View File

@@ -30,10 +30,8 @@ public class ThreadItemAdapter<I extends ThreadItem>
static final int UNDEFINED = -1;
private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager;
public ThreadItemAdapter(ThreadItemListener<I> listener,
LinearLayoutManager layoutManager) {
public ThreadItemAdapter(ThreadItemListener<I> listener) {
super(new DiffUtil.ItemCallback<I>() {
@Override
public boolean areItemsTheSame(I a, I b) {
@@ -47,7 +45,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
}
});
this.listener = listener;
this.layoutManager = layoutManager;
}
@NonNull
@@ -101,10 +98,17 @@ public class ThreadItemAdapter<I extends ThreadItem>
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<I extends ThreadItem>
/**
* 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++) {

View File

@@ -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<I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity
implements ThreadListListener<I>, SendListener, SharingListener,
ThreadItemListener<I>, ThreadListDataSource {
protected static final String KEY_REPLY_ID = "replyId";
private static final Logger LOG =
getLogger(ThreadListActivity.class.getName());
protected A adapter;
ThreadItemListener<I> {
protected final A adapter = createAdapter();
private ThreadScrollListener<I> 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<I> getController();
protected abstract ThreadListViewModel<I> getViewModel();
protected abstract A createAdapter();
@Inject
protected SharingController sharingController;
@@ -103,59 +91,85 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
list = findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter);
scrollListener = new ThreadScrollListener<>(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<I> items) {
if (items.isEmpty()) {
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
public void onReplyClick(I item) {
replyId = item.getId();
getViewModel().setReplyId(item.getId());
updateTextInput();
// FIXME This does not work for a hardware keyboard
if (textInput.isKeyboardOpen()) {
@@ -300,6 +265,7 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
}
private void updateTextInput() {
MessageId replyId = getViewModel().getReplyId();
if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.showSoftKeyboard();
@@ -318,20 +284,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
getViewModel().createAndStoreMessage(text, replyItem);
textInput.hideSoftKeyboard();
textInput.clearText();
replyId = null;
getViewModel().setReplyId(null);
updateTextInput();
}
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.nullsafety.NotNullByDefault;
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.handler.ResultExceptionHandler;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
@@ -27,23 +24,9 @@ public interface ThreadListController<I extends ThreadItem>
void markItemsRead(Collection<I> items);
interface ThreadListListener<I> extends ThreadListDataSource {
@UiThread
void onItemReceived(I item);
@UiThread
void onGroupRemoved();
interface ThreadListListener<I> {
@UiThread
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.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<I extends ThreadItem>
@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<I extends ThreadItem>
}
@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

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.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<I extends ThreadItem>
@DatabaseExecutor
private final MessageTree<I> messageTree = new MessageTreeImpl<>();
protected final Map<MessageId, String> textCache =
protected final Map<MessageId, String> textCache = // TODO still needed?
new ConcurrentHashMap<>();
private final MutableLiveData<LiveResult<List<I>>> items =
new MutableLiveData<>();
private final MutableLiveData<Boolean> groupRemoved =
new MutableLiveData<>();
private final AtomicReference<MessageId> scrollToItem =
new AtomicReference<>();
protected volatile GroupId groupId;
@Nullable
private MessageId replyId;
private final AtomicReference<MessageId> storedMessageId =
new AtomicReference<>();
@@ -114,6 +120,26 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
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<I extends ThreadItem>
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<I extends ThreadItem>
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<I extends ThreadItem>
return items;
}
LiveData<Boolean> getGroupRemoved() {
return groupRemoved;
}
@Nullable
MessageId getAndResetScrollToItem() {
return scrollToItem.getAndSet(null);