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 f1904fdab..d2c4bac64 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 @@ -35,9 +35,11 @@ import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.navdrawer.NavDrawerModule; +import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule; import org.briarproject.briar.android.settings.SettingsModule; import org.briarproject.briar.android.privategroup.list.GroupListModule; import org.briarproject.briar.android.reporting.DevReportModule; +import org.briarproject.briar.android.sharing.SharingModule; import org.briarproject.briar.android.test.TestAvatarCreatorImpl; import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.api.android.AndroidNotificationManager; @@ -79,8 +81,10 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; DevReportModule.class, ContactListModule.class, // below need to be within same scope as ViewModelProvider.Factory - ForumModule.BindsModule.class, + ForumModule.class, GroupListModule.class, + GroupConversationModule.class, + SharingModule.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 3835a7aaa..3b1e91ef9 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 @@ -32,7 +32,6 @@ import org.briarproject.briar.android.conversation.ImageFragment; import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumListFragment; -import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.IntroductionActivity; @@ -50,7 +49,6 @@ import org.briarproject.briar.android.navdrawer.TransportsActivity; import org.briarproject.briar.android.panic.PanicPreferencesActivity; import org.briarproject.briar.android.panic.PanicResponderActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity; -import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule; import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity; import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment; import org.briarproject.briar.android.privategroup.creation.CreateGroupModule; @@ -89,12 +87,10 @@ import dagger.Component; ActivityModule.class, BlogModule.class, CreateGroupModule.class, - ForumModule.class, GroupInvitationModule.class, - GroupConversationModule.class, GroupMemberModule.class, GroupRevealModule.class, - SharingModule.class + SharingModule.SharingLegacyModule.class }, dependencies = AndroidComponent.class) public interface ActivityComponent { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java index 594154f36..08f196e18 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java @@ -14,7 +14,6 @@ import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.DestroyableContext; import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.controller.ActivityLifecycleController; -import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.util.UiUtils; @@ -87,7 +86,6 @@ public abstract class BaseActivity extends AppCompatActivity activityComponent = DaggerActivityComponent.builder() .androidComponent(applicationComponent) .activityModule(getActivityModule()) - .forumModule(getForumModule()) .build(); injectActivity(activityComponent); super.onCreate(state); @@ -122,11 +120,6 @@ public abstract class BaseActivity extends AppCompatActivity return new ActivityModule(this); } - // TODO use a test module where this is used in tests - protected ForumModule getForumModule() { - return new ForumModule(); - } - @Override protected void onStart() { super.onStart(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java index 8957706f7..baf49a711 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java @@ -160,7 +160,6 @@ public abstract class BriarActivity extends BaseActivity { * @param ownLayout true if the custom toolbar brings its own layout * @return the Toolbar object or null if content view did not contain one */ - @Nullable protected Toolbar setUpCustomToolbar(boolean ownLayout) { // Custom Toolbar Toolbar toolbar = findViewById(R.id.toolbar); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java index 01f885578..3ebe7024f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java @@ -7,6 +7,7 @@ import java.util.Collection; import androidx.annotation.UiThread; +@Deprecated @NotNullByDefault public interface SharingController { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java index 858c94c53..b49eb6aa8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java @@ -18,6 +18,7 @@ import javax.inject.Inject; import androidx.annotation.UiThread; +@Deprecated @NotNullByDefault public class SharingControllerImpl implements SharingController, EventListener { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java index f77ac93e0..abb30abc5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumActivity.java @@ -6,52 +6,54 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.widget.Toast; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.db.DbException; 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.forum.ForumController.ForumListener; import org.briarproject.briar.android.sharing.ForumSharingStatusActivity; import org.briarproject.briar.android.sharing.ShareForumActivity; import org.briarproject.briar.android.threaded.ThreadItemAdapter; import org.briarproject.briar.android.threaded.ThreadListActivity; -import org.briarproject.briar.android.threaded.ThreadListController; -import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.android.threaded.ThreadListViewModel; import javax.annotation.Nullable; import javax.inject.Inject; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.lifecycle.ViewModelProvider; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static android.widget.Toast.LENGTH_SHORT; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM; +import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH; @MethodsNotNullByDefault @ParametersNotNullByDefault public class ForumActivity extends - ThreadListActivity> - implements ForumListener { + ThreadListActivity> { @Inject - ForumController forumController; + ViewModelProvider.Factory viewModelFactory; + + private ForumViewModel viewModel; @Override public void injectActivity(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(ForumViewModel.class); } @Override - protected ThreadListController getController() { - return forumController; + protected ThreadListViewModel getViewModel() { + return viewModel; + } + + @Override + protected ThreadItemAdapter createAdapter() { + return new ThreadItemAdapter<>(this); } @Override @@ -59,36 +61,27 @@ public class ForumActivity extends super.onCreate(state); Toolbar toolbar = setUpCustomToolbar(false); - - Intent i = getIntent(); - String groupName = i.getStringExtra(GROUP_NAME); - if (groupName != null) setTitle(groupName); - else loadNamedGroup(); - // Open member list on Toolbar click - if (toolbar != null) { - toolbar.setOnClickListener(v -> { - Intent i1 = new Intent(ForumActivity.this, - ForumSharingStatusActivity.class); - i1.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i1); - }); + toolbar.setOnClickListener(v -> { + Intent i = new Intent(ForumActivity.this, + ForumSharingStatusActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); + }); + + String groupName = getIntent().getStringExtra(GROUP_NAME); + if (groupName != null) { + setTitle(groupName); + } else { + observeOnce(viewModel.loadForum(), this, forum -> + setTitle(forum.getName()) + ); } } @Override - protected void onNamedGroupLoaded(Forum forum) { - setTitle(forum.getName()); - } - - @Override - protected ThreadItemAdapter createAdapter( - LinearLayoutManager layoutManager) { - return new ThreadItemAdapter<>(this, layoutManager); - } - - @Override - protected void onActivityResult(int request, int result, Intent data) { + protected void onActivityResult(int request, int result, + @Nullable Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) { @@ -101,32 +94,31 @@ public class ForumActivity extends // Inflate the menu items for use in the action bar MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.forum_actions, menu); - - return super.onCreateOptionsMenu(menu); + super.onCreateOptionsMenu(menu); + return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle presses on the action bar items - switch (item.getItemId()) { - case R.id.action_forum_share: - Intent i2 = new Intent(this, ShareForumActivity.class); - i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i2.putExtra(GROUP_ID, groupId.getBytes()); - startActivityForResult(i2, REQUEST_SHARE_FORUM); - return true; - case R.id.action_forum_sharing_status: - Intent i3 = new Intent(this, ForumSharingStatusActivity.class); - i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - i3.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i3); - return true; - case R.id.action_forum_delete: - showUnsubscribeDialog(); - return true; - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.action_forum_share) { + Intent i = new Intent(this, ShareForumActivity.class); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivityForResult(i, REQUEST_SHARE_FORUM); + return true; + } else if (itemId == R.id.action_forum_sharing_status) { + Intent i = new Intent(this, ForumSharingStatusActivity.class); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); + return true; + } else if (itemId == R.id.action_forum_delete) { + showUnsubscribeDialog(); + return true; } + return super.onOptionsItemSelected(item); } @Override @@ -135,7 +127,7 @@ public class ForumActivity extends } private void showUnsubscribeDialog() { - OnClickListener okListener = (dialog, which) -> deleteForum(); + OnClickListener okListener = (dialog, which) -> viewModel.deleteForum(); AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.dialog_title_leave_forum)); @@ -145,27 +137,4 @@ public class ForumActivity extends builder.show(); } - private void deleteForum() { - forumController.deleteNamedGroup( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Void v) { - Toast.makeText(ForumActivity.this, - R.string.forum_left_toast, LENGTH_SHORT).show(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @Override - public void onForumLeft(ContactId c) { - sharingController.remove(c); - setToolbarSubTitle(sharingController.getTotalCount(), - sharingController.getOnlineCount()); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java deleted file mode 100644 index 6658e60fc..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumController.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.briarproject.briar.android.forum; - -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.android.threaded.ThreadListController; -import org.briarproject.briar.api.forum.Forum; - -import androidx.annotation.UiThread; - -@NotNullByDefault -interface ForumController extends ThreadListController { - - interface ForumListener extends ThreadListListener { - @UiThread - void onForumLeft(ContactId c); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java deleted file mode 100644 index 6ad325d5b..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumControllerImpl.java +++ /dev/null @@ -1,185 +0,0 @@ -package org.briarproject.briar.android.forum; - -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.crypto.CryptoExecutor; -import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.android.forum.ForumController.ForumListener; -import org.briarproject.briar.android.threaded.ThreadListControllerImpl; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; -import org.briarproject.briar.api.forum.Forum; -import org.briarproject.briar.api.forum.ForumInvitationResponse; -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.ForumInvitationResponseReceivedEvent; -import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; -import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import static java.lang.Math.max; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; - -@NotNullByDefault -class ForumControllerImpl extends - ThreadListControllerImpl - implements ForumController { - - private static final Logger LOG = - Logger.getLogger(ForumControllerImpl.class.getName()); - - private final ForumManager forumManager; - private final ForumSharingManager forumSharingManager; - - @Inject - ForumControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, IdentityManager identityManager, - @CryptoExecutor Executor cryptoExecutor, - ForumManager forumManager, ForumSharingManager forumSharingManager, - EventBus eventBus, Clock clock, MessageTracker messageTracker, - AndroidNotificationManager notificationManager) { - super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor, - eventBus, clock, notificationManager, messageTracker); - this.forumManager = forumManager; - this.forumSharingManager = forumSharingManager; - } - - @Override - public void onActivityStart() { - super.onActivityStart(); - notificationManager.clearForumPostNotification(getGroupId()); - } - - @Override - public void eventOccurred(Event e) { - super.eventOccurred(e); - - 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) { - ForumInvitationResponseReceivedEvent f = - (ForumInvitationResponseReceivedEvent) e; - ForumInvitationResponse r = f.getMessageHeader(); - if (r.getShareableId().equals(getGroupId()) && r.wasAccepted()) { - LOG.info("Forum invitation was accepted"); - listener.onInvitationAccepted(f.getContactId()); - } - } else if (e instanceof ContactLeftShareableEvent) { - ContactLeftShareableEvent c = (ContactLeftShareableEvent) e; - if (c.getGroupId().equals(getGroupId())) { - LOG.info("Forum left by contact"); - listener.onForumLeft(c.getContactId()); - } - } - } - - @Override - protected Forum loadNamedGroup() throws DbException { - return forumManager.getForum(getGroupId()); - } - - @Override - protected Collection loadHeaders() throws DbException { - return forumManager.getPostHeaders(getGroupId()); - } - - @Override - protected String loadMessageText(ForumPostHeader h) throws DbException { - return forumManager.getPostText(h.getId()); - } - - @Override - protected void markRead(MessageId id) throws DbException { - forumManager.setReadFlag(getGroupId(), id, true); - } - - @Override - public void loadSharingContacts( - ResultExceptionHandler, DbException> handler) { - runOnDbThread(() -> { - try { - Collection contacts = - forumSharingManager.getSharedWith(getGroupId()); - Collection contactIds = - new ArrayList<>(contacts.size()); - for (Contact c : contacts) contactIds.add(c.getId()); - handler.onResult(contactIds); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void createAndStoreMessage(String text, - @Nullable ForumPostItem parentItem, - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - LocalAuthor author = identityManager.getLocalAuthor(); - GroupCount count = forumManager.getGroupCount(getGroupId()); - long timestamp = max(count.getLatestMsgTime() + 1, - clock.currentTimeMillis()); - MessageId parentId = parentItem != null ? - parentItem.getId() : null; - createMessage(text, timestamp, parentId, author, handler); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - private void createMessage(String text, long timestamp, - @Nullable MessageId parentId, LocalAuthor author, - ResultExceptionHandler handler) { - cryptoExecutor.execute(() -> { - LOG.info("Creating forum post..."); - ForumPost msg = forumManager.createLocalPost(getGroupId(), text, - timestamp, parentId, author); - storePost(msg, text, handler); - }); - } - - @Override - protected ForumPostHeader addLocalMessage(ForumPost p) throws DbException { - return forumManager.addLocalPost(p); - } - - @Override - protected void deleteNamedGroup(Forum forum) throws DbException { - forumManager.removeForum(forum); - } - - @Override - protected ForumPostItem buildItem(ForumPostHeader header, String text) { - return new ForumPostItem(header, text); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java index 62394c76d..2f2aba2cd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumModule.java @@ -1,32 +1,23 @@ package org.briarproject.briar.android.forum; -import org.briarproject.briar.android.activity.ActivityScope; -import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.viewmodel.ViewModelKey; import androidx.lifecycle.ViewModel; import dagger.Binds; import dagger.Module; -import dagger.Provides; import dagger.multibindings.IntoMap; @Module -public class ForumModule { +public interface ForumModule { - @Module - public interface BindsModule { - @Binds - @IntoMap - @ViewModelKey(ForumListViewModel.class) - ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel); - } + @Binds + @IntoMap + @ViewModelKey(ForumListViewModel.class) + ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel); - @ActivityScope - @Provides - ForumController provideForumController(BaseActivity activity, - ForumControllerImpl forumController) { - activity.addLifecycleController(forumController); - return forumController; - } + @Binds + @IntoMap + @ViewModelKey(ForumViewModel.class) + ViewModel bindForumViewModel(ForumViewModel forumViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java index 7366725de..4973a5d0b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumPostItem.java @@ -6,7 +6,6 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.android.threaded.ThreadItem; import org.briarproject.briar.api.forum.ForumPostHeader; -import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @NotThreadSafe @@ -17,9 +16,4 @@ class ForumPostItem extends ThreadItem { h.getAuthorInfo(), h.isRead()); } - ForumPostItem(MessageId messageId, @Nullable MessageId parentId, - String text, long timestamp, Author author, AuthorInfo authorInfo) { - super(messageId, parentId, text, timestamp, author, authorInfo, true); - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java new file mode 100644 index 000000000..af9254714 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/forum/ForumViewModel.java @@ -0,0 +1,231 @@ +package org.briarproject.briar.android.forum; + +import android.app.Application; +import android.widget.Toast; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.crypto.CryptoExecutor; +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.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.R; +import org.briarproject.briar.android.sharing.SharingController; +import org.briarproject.briar.android.threaded.ThreadListViewModel; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.forum.Forum; +import org.briarproject.briar.api.forum.ForumInvitationResponse; +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.ForumInvitationResponseReceivedEvent; +import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent; +import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static android.widget.Toast.LENGTH_SHORT; +import static java.lang.Math.max; +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; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +class ForumViewModel extends ThreadListViewModel { + + private static final Logger LOG = getLogger(ForumViewModel.class.getName()); + + private final ForumManager forumManager; + private final ForumSharingManager forumSharingManager; + + @Inject + ForumViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + IdentityManager identityManager, + AndroidNotificationManager notificationManager, + SharingController sharingController, + @CryptoExecutor Executor cryptoExecutor, + Clock clock, + MessageTracker messageTracker, + EventBus eventBus, + ForumManager forumManager, + ForumSharingManager forumSharingManager) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor, + identityManager, notificationManager, sharingController, + cryptoExecutor, clock, messageTracker, eventBus); + this.forumManager = forumManager; + this.forumSharingManager = forumSharingManager; + } + + @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 = + new ForumPostItem(f.getHeader(), f.getText()); + addItem(item, false); + } + } else if (e instanceof ForumInvitationResponseReceivedEvent) { + ForumInvitationResponseReceivedEvent f = + (ForumInvitationResponseReceivedEvent) e; + ForumInvitationResponse r = f.getMessageHeader(); + if (r.getShareableId().equals(groupId) && r.wasAccepted()) { + LOG.info("Forum invitation was accepted"); + sharingController.add(f.getContactId()); + } + } else if (e instanceof ContactLeftShareableEvent) { + ContactLeftShareableEvent c = (ContactLeftShareableEvent) e; + if (c.getGroupId().equals(groupId)) { + LOG.info("Forum left by contact"); + sharingController.remove(c.getContactId()); + } + } else { + super.eventOccurred(e); + } + } + + protected void clearNotifications() { + notificationManager.clearForumPostNotification(groupId); + } + + LiveData loadForum() { + MutableLiveData forum = new MutableLiveData<>(); + runOnDbThread(() -> { + try { + Forum f = forumManager.getForum(groupId); + forum.postValue(f); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + return forum; + } + + @Override + public void loadItems() { + loadList(txn -> { + long start = now(); + List headers = + forumManager.getPostHeaders(txn, groupId); + logDuration(LOG, "Loading headers", start); + start = now(); + List items = new ArrayList<>(); + for (ForumPostHeader header : headers) { + items.add(loadItem(txn, header)); + } + logDuration(LOG, "Loading bodies and creating items", start); + return items; + }, this::setItems); + } + + private ForumPostItem loadItem(Transaction txn, ForumPostHeader header) + throws DbException { + String text = forumManager.getPostText(txn, header.getId()); + return new ForumPostItem(header, text); + } + + @Override + public void createAndStoreMessage(String text, + @Nullable MessageId parentId) { + runOnDbThread(() -> { + try { + LocalAuthor author = identityManager.getLocalAuthor(); + GroupCount count = forumManager.getGroupCount(groupId); + long timestamp = max(count.getLatestMsgTime() + 1, + clock.currentTimeMillis()); + createMessage(text, timestamp, parentId, author); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void createMessage(String text, long timestamp, + @Nullable MessageId parentId, LocalAuthor author) { + cryptoExecutor.execute(() -> { + LOG.info("Creating forum post..."); + ForumPost msg = forumManager.createLocalPost(groupId, text, + timestamp, parentId, author); + storePost(msg, text); + }); + } + + private void storePost(ForumPost msg, String text) { + runOnDbThread(false, txn -> { + long start = now(); + ForumPostHeader header = forumManager.addLocalPost(txn, msg); + logDuration(LOG, "Storing forum post", start); + txn.attach(() -> { + ForumPostItem item = new ForumPostItem(header, text); + addItem(item, true); + }); + }, e -> logException(LOG, WARNING, e)); + } + + @Override + protected void markItemRead(ForumPostItem item) { + runOnDbThread(() -> { + try { + forumManager.setReadFlag(groupId, item.getId(), true); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + public void loadSharingContacts() { + runOnDbThread(true, txn -> { + Collection contacts = + forumSharingManager.getSharedWith(txn, groupId); + Collection contactIds = new ArrayList<>(contacts.size()); + for (Contact c : contacts) contactIds.add(c.getId()); + txn.attach(() -> sharingController.addAll(contactIds)); + }, e -> logException(LOG, WARNING, e)); + } + + void deleteForum() { + runOnDbThread(() -> { + try { + Forum f = forumManager.getForum(groupId); + forumManager.removeForum(f); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + Toast.makeText(getApplication(), R.string.forum_left_toast, + LENGTH_SHORT).show(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java index f81004ad2..47e4cb476 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupActivity.java @@ -1,67 +1,60 @@ package org.briarproject.briar.android.privategroup.conversation; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; import android.view.Menu; 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.identity.LocalAuthor; 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.UiExceptionHandler; -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; import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity; import org.briarproject.briar.android.threaded.ThreadListActivity; -import org.briarproject.briar.android.threaded.ThreadListController; -import org.briarproject.briar.api.privategroup.PrivateGroup; -import org.briarproject.briar.api.privategroup.Visibility; +import org.briarproject.briar.android.threaded.ThreadListViewModel; import javax.annotation.Nullable; import javax.inject.Inject; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.lifecycle.ViewModelProvider; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE; +import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_TEXT_LENGTH; @MethodsNotNullByDefault @ParametersNotNullByDefault public class GroupActivity extends - ThreadListActivity - implements GroupListener, OnClickListener { + ThreadListActivity { @Inject - GroupController controller; + ViewModelProvider.Factory viewModelFactory; - @Nullable - private Boolean isCreator = null; - private boolean isDissolved = false; - private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem, - dissolveMenuItem; + private GroupViewModel viewModel; @Override public void injectActivity(ActivityComponent component) { component.inject(this); + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(GroupViewModel.class); } @Override - protected ThreadListController getController() { - return controller; + protected ThreadListViewModel getViewModel() { + return viewModel; + } + + @Override + protected GroupMessageAdapter createAdapter() { + return new GroupMessageAdapter(this); } @Override @@ -69,65 +62,27 @@ public class GroupActivity extends super.onCreate(state); Toolbar toolbar = setUpCustomToolbar(false); - - Intent i = getIntent(); - String groupName = i.getStringExtra(GROUP_NAME); - if (groupName != null) setTitle(groupName); - loadNamedGroup(); - // Open member list on Toolbar click - if (toolbar != null) { - toolbar.setOnClickListener(v -> { - Intent i1 = new Intent(GroupActivity.this, - GroupMemberListActivity.class); - i1.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i1); - }); - } + toolbar.setOnClickListener(v -> { + Intent i = new Intent(GroupActivity.this, + GroupMemberListActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); + }); + String groupName = getIntent().getStringExtra(GROUP_NAME); + if (groupName != null) setTitle(groupName); + observeOnce(viewModel.getPrivateGroup(), this, privateGroup -> + setTitle(privateGroup.getName()) + ); + observeOnce(viewModel.isCreator(), this, adapter::setIsCreator); + + // start with group disabled and enable when not dissolved setGroupEnabled(false); - } - - @Override - protected GroupMessageAdapter createAdapter( - LinearLayoutManager layoutManager) { - return new GroupMessageAdapter(this, layoutManager); - } - - @Override - protected void loadItems() { - controller.isDissolved( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(Boolean isDissolved) { - setGroupEnabled(!isDissolved); - GroupActivity.super.loadItems(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @Override - protected void onNamedGroupLoaded(PrivateGroup group) { - setTitle(group.getName()); - controller.loadLocalAuthor( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(LocalAuthor author) { - isCreator = group.getCreator().equals(author); - adapter.setPerspective(isCreator); - showMenuItems(); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + viewModel.isDissolved().observeEvent(this, dissolved -> { + setGroupEnabled(!dissolved); + if (dissolved) onGroupDissolved(); + }); } @Override @@ -136,74 +91,61 @@ public class GroupActivity extends MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.group_actions, menu); - revealMenuItem = menu.findItem(R.id.action_group_reveal); - inviteMenuItem = menu.findItem(R.id.action_group_invite); - leaveMenuItem = menu.findItem(R.id.action_group_leave); - dissolveMenuItem = menu.findItem(R.id.action_group_dissolve); - - // all role-dependent items are invisible until we know our role - revealMenuItem.setVisible(false); - inviteMenuItem.setVisible(false); - leaveMenuItem.setVisible(false); - dissolveMenuItem.setVisible(false); - - // show items based on role - showMenuItems(); - - return super.onCreateOptionsMenu(menu); + // show items based on role (which will not change, so observe once) + observeOnce(viewModel.isCreator(), this, isCreator -> { + menu.findItem(R.id.action_group_reveal).setVisible(!isCreator); + menu.findItem(R.id.action_group_invite).setVisible(isCreator); + menu.findItem(R.id.action_group_leave).setVisible(!isCreator); + menu.findItem(R.id.action_group_dissolve).setVisible(isCreator); + }); + super.onCreateOptionsMenu(menu); + return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_group_member_list: - Intent i1 = new Intent(this, GroupMemberListActivity.class); - i1.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i1); - return true; - case R.id.action_group_reveal: - if (isCreator == null || isCreator) - throw new IllegalStateException(); - Intent i2 = new Intent(this, RevealContactsActivity.class); - i2.putExtra(GROUP_ID, groupId.getBytes()); - startActivity(i2); - return true; - case R.id.action_group_invite: - if (isCreator == null || !isCreator) - throw new IllegalStateException(); - Intent i3 = new Intent(this, GroupInviteActivity.class); - i3.putExtra(GROUP_ID, groupId.getBytes()); - startActivityForResult(i3, REQUEST_GROUP_INVITE); - return true; - case R.id.action_group_leave: - if (isCreator == null || isCreator) - throw new IllegalStateException(); - showLeaveGroupDialog(); - return true; - case R.id.action_group_dissolve: - if (isCreator == null || !isCreator) - throw new IllegalStateException(); - showDissolveGroupDialog(); - default: - return super.onOptionsItemSelected(item); + int itemId = item.getItemId(); + if (itemId == R.id.action_group_member_list) { + Intent i = new Intent(this, GroupMemberListActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); + return true; + } else if (itemId == R.id.action_group_reveal) { + if (requireNonNull(viewModel.isCreator().getValue())) + throw new IllegalStateException(); + Intent i = new Intent(this, RevealContactsActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivity(i); + return true; + } else if (itemId == R.id.action_group_invite) { + if (!requireNonNull(viewModel.isCreator().getValue())) + throw new IllegalStateException(); + Intent i = new Intent(this, GroupInviteActivity.class); + i.putExtra(GROUP_ID, groupId.getBytes()); + startActivityForResult(i, REQUEST_GROUP_INVITE); + return true; + } else if (itemId == R.id.action_group_leave) { + if (requireNonNull(viewModel.isCreator().getValue())) + throw new IllegalStateException(); + showLeaveGroupDialog(); + return true; + } else if (itemId == R.id.action_group_dissolve) { + if (!requireNonNull(viewModel.isCreator().getValue())) + throw new IllegalStateException(); + showDissolveGroupDialog(); + return true; } + return super.onOptionsItemSelected(item); } @Override - protected void onActivityResult(int request, int result, Intent data) { + protected void onActivityResult(int request, int result, + @Nullable Intent data) { if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) { displaySnackbar(R.string.groups_invitation_sent); } 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; @@ -211,11 +153,11 @@ public class GroupActivity extends @Override public void onReplyClick(GroupMessageItem item) { - if (!isDissolved) super.onReplyClick(item); + Boolean isDissolved = viewModel.isDissolved().getLastValue(); + if (isDissolved != null && !isDissolved) super.onReplyClick(item); } private void setGroupEnabled(boolean enabled) { - isDissolved = !enabled; sendController.setReady(enabled); list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f); @@ -227,21 +169,13 @@ 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); builder.setTitle(getString(R.string.groups_leave_dialog_title)); builder.setMessage(getString(R.string.groups_leave_dialog_message)); - builder.setNegativeButton(R.string.dialog_button_leave, this); + builder.setNegativeButton(R.string.dialog_button_leave, + (d, w) -> deleteGroup()); builder.setPositiveButton(R.string.cancel, null); builder.show(); } @@ -251,37 +185,19 @@ public class GroupActivity extends new AlertDialog.Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.groups_dissolve_dialog_title)); builder.setMessage(getString(R.string.groups_dissolve_dialog_message)); - builder.setNegativeButton(R.string.groups_dissolve_button, this); + builder.setNegativeButton(R.string.groups_dissolve_button, + (d, w) -> deleteGroup()); builder.setPositiveButton(R.string.cancel, null); builder.show(); } - @Override - public void onClick(DialogInterface dialog, int which) { - controller.deleteNamedGroup( - new UiExceptionHandler(this) { - // The activity is going to be destroyed by the - // GroupRemovedEvent being fired - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + private void deleteGroup() { + // The activity is going to be destroyed by the + // GroupRemovedEvent being fired + viewModel.deletePrivateGroup(); } - @Override - public void onContactRelationshipRevealed(AuthorId memberId, ContactId c, - Visibility v) { - adapter.updateVisibility(memberId, v); - - sharingController.add(c); - setToolbarSubTitle(sharingController.getTotalCount(), - sharingController.getOnlineCount()); - } - - @Override - public void onGroupDissolved() { - setGroupEnabled(false); + private void onGroupDissolved() { AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.groups_dissolved_dialog_title)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java deleted file mode 100644 index 571f55bf1..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupController.java +++ /dev/null @@ -1,33 +0,0 @@ -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.bramble.api.identity.LocalAuthor; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.android.threaded.ThreadListController; -import org.briarproject.briar.api.privategroup.PrivateGroup; -import org.briarproject.briar.api.privategroup.Visibility; - -import androidx.annotation.UiThread; - -public interface GroupController - extends ThreadListController { - - void loadLocalAuthor( - ResultExceptionHandler handler); - - void isDissolved( - ResultExceptionHandler handler); - - interface GroupListener extends ThreadListListener { - - @UiThread - void onContactRelationshipRevealed(AuthorId memberId, - ContactId contactId, Visibility v); - - @UiThread - void onGroupDissolved(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java deleted file mode 100644 index 5b0b15ad8..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupControllerImpl.java +++ /dev/null @@ -1,242 +0,0 @@ -package org.briarproject.briar.android.privategroup.conversation; - -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.crypto.CryptoExecutor; -import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.android.privategroup.conversation.GroupController.GroupListener; -import org.briarproject.briar.android.threaded.ThreadListControllerImpl; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; -import org.briarproject.briar.api.privategroup.GroupMember; -import org.briarproject.briar.api.privategroup.GroupMessage; -import org.briarproject.briar.api.privategroup.GroupMessageFactory; -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.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; -import java.util.Collection; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import static java.lang.Math.max; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -class GroupControllerImpl extends - ThreadListControllerImpl - implements GroupController { - - private static final Logger LOG = - Logger.getLogger(GroupControllerImpl.class.getName()); - - private final PrivateGroupManager privateGroupManager; - private final GroupMessageFactory groupMessageFactory; - - @Inject - GroupControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, IdentityManager identityManager, - @CryptoExecutor Executor cryptoExecutor, - PrivateGroupManager privateGroupManager, - GroupMessageFactory groupMessageFactory, EventBus eventBus, - MessageTracker messageTracker, Clock clock, - AndroidNotificationManager notificationManager) { - super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor, - eventBus, clock, notificationManager, messageTracker); - this.privateGroupManager = privateGroupManager; - this.groupMessageFactory = groupMessageFactory; - } - - @Override - public void onActivityStart() { - super.onActivityStart(); - notificationManager.clearGroupMessageNotification(getGroupId()); - } - - @Override - public void eventOccurred(Event e) { - super.eventOccurred(e); - - 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) { - ContactRelationshipRevealedEvent c = - (ContactRelationshipRevealedEvent) e; - if (getGroupId().equals(c.getGroupId())) { - listener.onContactRelationshipRevealed(c.getMemberId(), - c.getContactId(), c.getVisibility()); - } - } else if (e instanceof GroupInvitationResponseReceivedEvent) { - GroupInvitationResponseReceivedEvent g = - (GroupInvitationResponseReceivedEvent) e; - GroupInvitationResponse r = g.getMessageHeader(); - 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(); - } - } - } - - @Override - protected PrivateGroup loadNamedGroup() throws DbException { - return privateGroupManager.getPrivateGroup(getGroupId()); - } - - @Override - protected Collection loadHeaders() throws DbException { - return privateGroupManager.getHeaders(getGroupId()); - } - - @Override - protected String loadMessageText(GroupMessageHeader header) - throws DbException { - if (header instanceof JoinMessageHeader) { - // will be looked up later - return ""; - } - return privateGroupManager.getMessageText(header.getId()); - } - - @Override - protected void markRead(MessageId id) throws DbException { - privateGroupManager.setReadFlag(getGroupId(), id, true); - } - - @Override - public void loadSharingContacts( - ResultExceptionHandler, DbException> handler) { - runOnDbThread(() -> { - try { - Collection members = - privateGroupManager.getMembers(getGroupId()); - Collection contactIds = new ArrayList<>(); - for (GroupMember m : members) { - if (m.getContactId() != null) - contactIds.add(m.getContactId()); - } - handler.onResult(contactIds); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void createAndStoreMessage(String text, - @Nullable GroupMessageItem parentItem, - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - LocalAuthor author = identityManager.getLocalAuthor(); - MessageId parentId = null; - MessageId previousMsgId = - privateGroupManager.getPreviousMsgId(getGroupId()); - GroupCount count = - privateGroupManager.getGroupCount(getGroupId()); - long timestamp = count.getLatestMsgTime(); - if (parentItem != null) parentId = parentItem.getId(); - timestamp = max(clock.currentTimeMillis(), timestamp + 1); - createMessage(text, timestamp, parentId, author, previousMsgId, - handler); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - private void createMessage(String text, long timestamp, - @Nullable MessageId parentId, LocalAuthor author, - MessageId previousMsgId, - ResultExceptionHandler handler) { - cryptoExecutor.execute(() -> { - LOG.info("Creating group message..."); - GroupMessage msg = groupMessageFactory - .createGroupMessage(getGroupId(), timestamp, - parentId, author, text, previousMsgId); - storePost(msg, text, handler); - }); - } - - @Override - protected GroupMessageHeader addLocalMessage(GroupMessage message) - throws DbException { - return privateGroupManager.addLocalMessage(message); - } - - @Override - protected void deleteNamedGroup(PrivateGroup group) throws DbException { - privateGroupManager.removePrivateGroup(group.getId()); - } - - @Override - protected GroupMessageItem buildItem(GroupMessageHeader header, - String text) { - if (header instanceof JoinMessageHeader) { - return new JoinMessageItem((JoinMessageHeader) header, text); - } - return new GroupMessageItem(header, text); - } - - @Override - public void loadLocalAuthor( - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - LocalAuthor author = identityManager.getLocalAuthor(); - handler.onResult(author); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @Override - public void isDissolved( - ResultExceptionHandler handler) { - runOnDbThread(() -> { - try { - boolean isDissolved = - privateGroupManager.isDissolved(getGroupId()); - handler.onResult(isDissolved); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupConversationModule.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupConversationModule.java index 6fa102375..88e43cf59 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupConversationModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupConversationModule.java @@ -1,19 +1,18 @@ package org.briarproject.briar.android.privategroup.conversation; -import org.briarproject.briar.android.activity.ActivityScope; -import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.viewmodel.ViewModelKey; +import androidx.lifecycle.ViewModel; +import dagger.Binds; import dagger.Module; -import dagger.Provides; +import dagger.multibindings.IntoMap; @Module -public class GroupConversationModule { +public interface GroupConversationModule { + + @Binds + @IntoMap + @ViewModelKey(GroupViewModel.class) + ViewModel bindGroupViewModel(GroupViewModel groupViewModel); - @ActivityScope - @Provides - GroupController provideGroupController(BaseActivity activity, - GroupControllerImpl groupController) { - activity.addLifecycleController(groupController); - return groupController; - } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java index 3b67f8b94..4eedbd580 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupMessageAdapter.java @@ -4,19 +4,14 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder; import org.briarproject.briar.android.threaded.ThreadItemAdapter; import org.briarproject.briar.android.threaded.ThreadPostViewHolder; -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; @UiThread @NotNullByDefault @@ -24,15 +19,14 @@ class GroupMessageAdapter extends ThreadItemAdapter { private boolean isCreator = false; - GroupMessageAdapter(ThreadItemListener listener, - LinearLayoutManager layoutManager) { - super(listener, layoutManager); + GroupMessageAdapter(ThreadItemListener listener) { + super(listener); } @LayoutRes @Override public int getItemViewType(int position) { - GroupMessageItem item = items.get(position); + GroupMessageItem item = getItem(position); return item.getLayout(); } @@ -47,30 +41,9 @@ class GroupMessageAdapter extends ThreadItemAdapter { return new ThreadPostViewHolder<>(v); } - void setPerspective(boolean isCreator) { + void setIsCreator(boolean isCreator) { this.isCreator = isCreator; notifyDataSetChanged(); } - void updateVisibility(AuthorId memberId, Visibility v) { - int position = findItemPosition(memberId); - if (position != NO_POSITION) { - GroupMessageItem item = items.get(position); - if (item instanceof JoinMessageItem) { - ((JoinMessageItem) item).setVisibility(v); - notifyItemChanged(findItemPosition(item), item); - } - } - } - - private int findItemPosition(AuthorId a) { - int count = items.size(); - for (int i = 0; i < count; i++) { - GroupMessageItem item = items.get(i); - if (item.getAuthor().getId().equals(a)) - return i; - } - return NO_POSITION; // Not found - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java new file mode 100644 index 000000000..2b25ee75e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/GroupViewModel.java @@ -0,0 +1,289 @@ +package org.briarproject.briar.android.privategroup.conversation; + +import android.app.Application; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.crypto.CryptoExecutor; +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.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.android.sharing.SharingController; +import org.briarproject.briar.android.threaded.ThreadListViewModel; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; +import org.briarproject.briar.api.android.AndroidNotificationManager; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.privategroup.GroupMember; +import org.briarproject.briar.api.privategroup.GroupMessage; +import org.briarproject.briar.api.privategroup.GroupMessageFactory; +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.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; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.lang.Math.max; +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; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +class GroupViewModel extends ThreadListViewModel { + + private static final Logger LOG = getLogger(GroupViewModel.class.getName()); + + private final PrivateGroupManager privateGroupManager; + private final GroupMessageFactory groupMessageFactory; + + private final MutableLiveData privateGroup = + new MutableLiveData<>(); + private final MutableLiveData isCreator = new MutableLiveData<>(); + private final MutableLiveEvent isDissolved = + new MutableLiveEvent<>(); + + @Inject + GroupViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + EventBus eventBus, + IdentityManager identityManager, + AndroidNotificationManager notificationManager, + SharingController sharingController, + @CryptoExecutor Executor cryptoExecutor, + Clock clock, + MessageTracker messageTracker, + PrivateGroupManager privateGroupManager, + GroupMessageFactory groupMessageFactory) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor, + identityManager, notificationManager, sharingController, + cryptoExecutor, clock, messageTracker, eventBus); + this.privateGroupManager = privateGroupManager; + this.groupMessageFactory = groupMessageFactory; + } + + @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, false); + // In case the join message comes from the creator, + // we need to reload the sharing contacts + // in case it was delayed and the sharing count is wrong (#850). + if (item instanceof JoinMessageItem && + (((JoinMessageItem) item).isInitial())) { + loadSharingContacts(); + } + } + } else if (e instanceof GroupInvitationResponseReceivedEvent) { + GroupInvitationResponseReceivedEvent g = + (GroupInvitationResponseReceivedEvent) e; + GroupInvitationResponse r = g.getMessageHeader(); + if (r.getShareableId().equals(groupId) && r.wasAccepted()) { + sharingController.add(g.getContactId()); + } + } else if (e instanceof ContactRelationshipRevealedEvent) { + ContactRelationshipRevealedEvent c = + (ContactRelationshipRevealedEvent) e; + if (c.getGroupId().equals(groupId)) { + sharingController.add(c.getContactId()); + } + } else if (e instanceof GroupDissolvedEvent) { + GroupDissolvedEvent g = (GroupDissolvedEvent) e; + if (g.getGroupId().equals(groupId)) { + isDissolved.setEvent(true); + } + } else { + super.eventOccurred(e); + } + } + + @Override + protected void performInitialLoad() { + super.performInitialLoad(); + loadPrivateGroup(groupId); + } + + protected void clearNotifications() { + notificationManager.clearGroupMessageNotification(groupId); + } + + private void loadPrivateGroup(GroupId groupId) { + runOnDbThread(() -> { + try { + PrivateGroup g = privateGroupManager.getPrivateGroup(groupId); + privateGroup.postValue(g); + Author author = identityManager.getLocalAuthor(); + isCreator.postValue(g.getCreator().equals(author)); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + @Override + public void loadItems() { + loadList(txn -> { + // check first if group is dissolved + isDissolved + .postEvent(privateGroupManager.isDissolved(txn, groupId)); + // now continue to load the items + long start = now(); + List headers = + privateGroupManager.getHeaders(txn, groupId); + logDuration(LOG, "Loading headers", start); + start = now(); + List items = new ArrayList<>(); + for (GroupMessageHeader header : headers) { + items.add(loadItem(txn, header)); + } + logDuration(LOG, "Loading bodies and creating items", start); + return items; + }, this::setItems); + } + + private GroupMessageItem loadItem(Transaction txn, + GroupMessageHeader header) throws DbException { + String text; + if (header instanceof JoinMessageHeader) { + // will be looked up later + text = ""; + } else { + text = privateGroupManager.getMessageText(txn, header.getId()); + } + return buildItem(header, text); + } + + 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 createAndStoreMessage(String text, + @Nullable MessageId parentId) { + runOnDbThread(() -> { + try { + LocalAuthor author = identityManager.getLocalAuthor(); + MessageId previousMsgId = + privateGroupManager.getPreviousMsgId(groupId); + GroupCount count = privateGroupManager.getGroupCount(groupId); + long timestamp = count.getLatestMsgTime(); + timestamp = max(clock.currentTimeMillis(), timestamp + 1); + createMessage(text, timestamp, parentId, author, previousMsgId); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void createMessage(String text, long timestamp, + @Nullable MessageId parentId, LocalAuthor author, + MessageId previousMsgId) { + cryptoExecutor.execute(() -> { + LOG.info("Creating group message..."); + GroupMessage msg = groupMessageFactory.createGroupMessage(groupId, + timestamp, parentId, author, text, previousMsgId); + storePost(msg, text); + }); + } + + private void storePost(GroupMessage msg, String text) { + runOnDbThread(false, txn -> { + long start = now(); + GroupMessageHeader header = + privateGroupManager.addLocalMessage(txn, msg); + logDuration(LOG, "Storing group message", start); + txn.attach(() -> + addItem(buildItem(header, text), true) + ); + }, e -> logException(LOG, WARNING, e)); + } + + @Override + protected void markItemRead(GroupMessageItem item) { + runOnDbThread(() -> { + try { + privateGroupManager.setReadFlag(groupId, item.getId(), true); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + public void loadSharingContacts() { + runOnDbThread(true, txn -> { + Collection members = + privateGroupManager.getMembers(txn, groupId); + Collection contactIds = new ArrayList<>(); + for (GroupMember m : members) { + if (m.getContactId() != null) + contactIds.add(m.getContactId()); + } + txn.attach(() -> sharingController.addAll(contactIds)); + }, e -> logException(LOG, WARNING, e)); + } + + void deletePrivateGroup() { + runOnDbThread(() -> { + try { + privateGroupManager.removePrivateGroup(groupId); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData getPrivateGroup() { + return privateGroup; + } + + LiveData isCreator() { + return isCreator; + } + + LiveEvent isDissolved() { + return isDissolved; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/JoinMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/JoinMessageItem.java index 592962371..234fcccfb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/JoinMessageItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/privategroup/conversation/JoinMessageItem.java @@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.conversation; import org.briarproject.briar.R; import org.briarproject.briar.api.privategroup.JoinMessageHeader; -import org.briarproject.briar.api.privategroup.Visibility; import javax.annotation.concurrent.NotThreadSafe; @@ -13,13 +12,11 @@ import androidx.annotation.UiThread; @NotThreadSafe class JoinMessageItem extends GroupMessageItem { - private Visibility visibility; private final boolean isInitial; JoinMessageItem(JoinMessageHeader h, String text) { super(h, text); - this.visibility = h.getVisibility(); - this.isInitial = h.isInitial(); + isInitial = h.isInitial(); } @Override @@ -33,14 +30,6 @@ class JoinMessageItem extends GroupMessageItem { return R.layout.list_item_group_join_notice; } - Visibility getVisibility() { - return visibility; - } - - void setVisibility(Visibility visibility) { - this.visibility = visibility; - } - boolean isInitial() { return isInitial; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingController.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingController.java new file mode 100644 index 000000000..3fd3f4e96 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingController.java @@ -0,0 +1,54 @@ +package org.briarproject.briar.android.sharing; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.Collection; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; + +@NotNullByDefault +public interface SharingController { + + /** + * Call this when the owning ViewModel gets cleared, + * so the {@link EventBus} can get unregistered. + */ + void onCleared(); + + /** + * Adds one contact to be tracked. + */ + @UiThread + void add(ContactId c); + + /** + * Adds a collection of contacts to be tracked. + */ + @UiThread + void addAll(Collection contacts); + + /** + * Call this when the contact identified by c is no longer sharing + * the given group identified by GroupId g. + */ + @UiThread + void remove(ContactId c); + + /** + * Returns the total number of contacts that have been added. + */ + LiveData getSharingInfo(); + + class SharingInfo { + public final int total, online; + + SharingInfo(int total, int online) { + this.total = total; + this.online = online; + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingControllerImpl.java new file mode 100644 index 000000000..b58ab1a08 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingControllerImpl.java @@ -0,0 +1,102 @@ +package org.briarproject.briar.android.sharing; + +import org.briarproject.bramble.api.connection.ConnectionRegistry; +import org.briarproject.bramble.api.contact.ContactId; +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.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +@NotNullByDefault +public class SharingControllerImpl implements SharingController, EventListener { + + private final EventBus eventBus; + private final ConnectionRegistry connectionRegistry; + + // UI thread + private final Set contacts = new HashSet<>(); + private final MutableLiveData sharingInfo = + new MutableLiveData<>(); + + @Inject + SharingControllerImpl(EventBus eventBus, + ConnectionRegistry connectionRegistry) { + this.eventBus = eventBus; + this.connectionRegistry = connectionRegistry; + eventBus.addListener(this); + } + + @Override + public void onCleared() { + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof ContactConnectedEvent) { + setConnected(((ContactConnectedEvent) e).getContactId()); + } else if (e instanceof ContactDisconnectedEvent) { + setConnected(((ContactDisconnectedEvent) e).getContactId()); + } + } + + @UiThread + private void setConnected(ContactId c) { + if (contacts.contains(c)) { + updateLiveData(); + } + } + + @UiThread + private void updateLiveData() { + int online = getOnlineCount(); + sharingInfo.setValue(new SharingInfo(contacts.size(), online)); + } + + private int getOnlineCount() { + int online = 0; + for (ContactId c : contacts) { + if (connectionRegistry.isConnected(c)) online++; + } + return online; + } + + @UiThread + @Override + public void addAll(Collection c) { + contacts.addAll(c); + updateLiveData(); + } + + @UiThread + @Override + public void add(ContactId c) { + contacts.add(c); + updateLiveData(); + } + + @UiThread + @Override + public void remove(ContactId c) { + contacts.remove(c); + updateLiveData(); + } + + @Override + public LiveData getSharingInfo() { + return sharingInfo; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingModule.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingModule.java index 50bcceb2b..137aa28e4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/SharingModule.java @@ -9,36 +9,47 @@ import dagger.Provides; @Module public class SharingModule { - @ActivityScope - @Provides - ShareForumController provideShareForumController( - ShareForumControllerImpl shareForumController) { - return shareForumController; + @Module + @Deprecated + public static class SharingLegacyModule { + + @ActivityScope + @Provides + ShareForumController provideShareForumController( + ShareForumControllerImpl shareForumController) { + return shareForumController; + } + + @ActivityScope + @Provides + BlogInvitationController provideInvitationBlogController( + BaseActivity activity, + BlogInvitationControllerImpl blogInvitationController) { + activity.addLifecycleController(blogInvitationController); + return blogInvitationController; + } + + @ActivityScope + @Provides + ForumInvitationController provideInvitationForumController( + BaseActivity activity, + ForumInvitationControllerImpl forumInvitationController) { + activity.addLifecycleController(forumInvitationController); + return forumInvitationController; + } + + @ActivityScope + @Provides + ShareBlogController provideShareBlogController( + ShareBlogControllerImpl shareBlogController) { + return shareBlogController; + } } - @ActivityScope @Provides - BlogInvitationController provideInvitationBlogController( - BaseActivity activity, - BlogInvitationControllerImpl blogInvitationController) { - activity.addLifecycleController(blogInvitationController); - return blogInvitationController; - } - - @ActivityScope - @Provides - ForumInvitationController provideInvitationForumController( - BaseActivity activity, - ForumInvitationControllerImpl forumInvitationController) { - activity.addLifecycleController(forumInvitationController); - return forumInvitationController; - } - - @ActivityScope - @Provides - ShareBlogController provideShareBlogController( - ShareBlogControllerImpl shareBlogController) { - return shareBlogController; + SharingController provideSharingController( + SharingControllerImpl sharingController) { + return sharingController; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java deleted file mode 100644 index 40e211270..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/NestedTreeList.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.briarproject.briar.android.threaded; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.api.client.MessageTree; -import org.briarproject.briar.api.client.MessageTree.MessageNode; -import org.briarproject.briar.client.MessageTreeImpl; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import androidx.annotation.UiThread; - -@UiThread -@NotNullByDefault -public class NestedTreeList implements Iterable { - - private final MessageTree tree = new MessageTreeImpl<>(); - private List depthFirstCollection = new ArrayList<>(); - - public void addAll(Collection collection) { - tree.add(collection); - depthFirstCollection = new ArrayList<>(tree.depthFirstOrder()); - } - - public void add(T elem) { - tree.add(elem); - depthFirstCollection = new ArrayList<>(tree.depthFirstOrder()); - } - - public void clear() { - tree.clear(); - depthFirstCollection.clear(); - } - - public T get(int index) { - return depthFirstCollection.get(index); - } - - public int size() { - return depthFirstCollection.size(); - } - - public boolean contains(MessageId m) { - return tree.contains(m); - } - - @Override - public Iterator iterator() { - return depthFirstCollection.iterator(); - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItem.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItem.java index 0d3d69205..bde7c3e7d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItem.java @@ -99,4 +99,14 @@ public abstract class ThreadItem implements MessageNode { return highlighted; } + @Override + public int hashCode() { + return messageId.hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof ThreadItem && + messageId.equals(((ThreadItem) o).messageId); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java index c293f7858..7e5bf01ae 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java @@ -4,39 +4,45 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.util.ItemReturningAdapter; -import org.briarproject.briar.android.util.VersionedAdapter; - -import java.util.Collection; import javax.annotation.Nullable; import androidx.annotation.NonNull; import androidx.annotation.UiThread; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.ListAdapter; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; @UiThread +@NotNullByDefault public class ThreadItemAdapter - extends RecyclerView.Adapter> - implements VersionedAdapter, ItemReturningAdapter { + extends ListAdapter> + implements ItemReturningAdapter { static final int UNDEFINED = -1; - protected final NestedTreeList items = new NestedTreeList<>(); private final ThreadItemListener listener; - private final LinearLayoutManager layoutManager; - private volatile int revision = 0; + public ThreadItemAdapter(ThreadItemListener listener) { + super(new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(I a, I b) { + return a.equals(b); + } - public ThreadItemAdapter(ThreadItemListener listener, - LinearLayoutManager layoutManager) { + @Override + public boolean areContentsTheSame(I a, I b) { + return a.isHighlighted() == b.isHighlighted() && + a.isRead() == b.isRead(); + } + }); this.listener = listener; - this.layoutManager = layoutManager; } @NonNull @@ -51,76 +57,27 @@ public class ThreadItemAdapter @Override public void onBindViewHolder(@NonNull BaseThreadItemViewHolder ui, int position) { - I item = items.get(position); + I item = getItem(position); ui.bind(item, listener); } - @Override - public int getItemCount() { - return items.size(); - } - - @Override - public int getRevision() { - return revision; - } - - @Override - public void incrementRevision() { - revision++; - } - - void setItemWithIdVisible(MessageId messageId) { - int pos = 0; - for (I item : items) { - if (item.getId().equals(messageId)) { - layoutManager.scrollToPosition(pos); - break; - } - pos++; - } - } - - public void setItems(Collection items) { - this.items.clear(); - this.items.addAll(items); - notifyDataSetChanged(); - } - - public void add(I item) { - items.add(item); - notifyItemInserted(findItemPosition(item)); - } - - @Nullable - public I getItemAt(int position) { - if (position == NO_POSITION || position >= items.size()) { - return null; - } - return items.get(position); - } - - protected int findItemPosition(@Nullable I item) { - for (int i = 0; i < items.size(); i++) { - if (items.get(i).equals(item)) return i; + int findItemPosition(MessageId id) { + for (int i = 0; i < getItemCount(); i++) { + if (id.equals(getItem(i).getId())) return i; } return NO_POSITION; // Not found } - boolean contains(MessageId m) { - return items.contains(m); - } - /** * Highlights the item with the given {@link MessageId} * and disables the highlight for a previously highlighted item, if any. - * + *

* Only one item can be highlighted at a time. */ void setHighlightedItem(@Nullable MessageId id) { - for (int i = 0; i < items.size(); i++) { - I item = items.get(i); - if (id != null && item.getId().equals(id)) { + for (int i = 0; i < getItemCount(); i++) { + I item = getItem(i); + if (item.getId().equals(id)) { item.setHighlighted(true); notifyItemChanged(i, item); } else if (item.isHighlighted()) { @@ -132,20 +89,27 @@ public class ThreadItemAdapter @Nullable I getHighlightedItem() { - for (I i : items) { - if (i.isHighlighted()) return i; + for (I item : getCurrentList()) { + if (item.isHighlighted()) return item; } 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 < items.size(); i++) { - if (!items.get(i).isRead()) return i; + for (int i = positionBottom + 1; i < getItemCount(); i++) { + if (!getItem(i).isRead()) return i; } return NO_POSITION; } @@ -153,11 +117,11 @@ public class ThreadItemAdapter /** * Returns the position of the first unread item above the current viewport */ - int getVisibleUnreadPosTop() { + int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) { int positionTop = layoutManager.findFirstVisibleItemPosition(); int position = NO_POSITION; - for (int i = 0; i < items.size(); i++) { - if (i < positionTop && !items.get(i).isRead()) { + for (int i = 0; i < getItemCount(); i++) { + if (i < positionTop && !getItem(i).isRead()) { position = i; } else if (i >= positionTop) { return position; @@ -166,6 +130,11 @@ public class ThreadItemAdapter return NO_POSITION; } + @Override + public I getItemAt(int position) { + return getItem(position); + } + public interface ThreadItemListener { void onReplyClick(I item); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java deleted file mode 100644 index f517e8e9c..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.briarproject.briar.android.threaded; - -import org.briarproject.bramble.api.sync.MessageId; - -import java.util.List; - -import javax.annotation.Nullable; - -public interface ThreadItemList extends List { - - @Nullable - MessageId getFirstVisibleItemId(); - - void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId); -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java deleted file mode 100644 index bfd63fa5b..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.briarproject.briar.android.threaded; - -import org.briarproject.bramble.api.sync.MessageId; - -import java.util.ArrayList; - -import javax.annotation.Nullable; - -public class ThreadItemListImpl extends ArrayList - implements ThreadItemList { - - private MessageId bottomVisibleItemId; - - @Override - public MessageId getFirstVisibleItemId() { - return bottomVisibleItemId; - } - - @Override - public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) { - this.bottomVisibleItemId = bottomVisibleItemId; - } -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index 828be5091..ac045316b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -2,25 +2,18 @@ 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; -import org.briarproject.bramble.api.contact.ContactId; -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.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.BriarActivity; -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.sharing.SharingController.SharingInfo; 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; import org.briarproject.briar.android.view.TextInputView; @@ -28,18 +21,13 @@ import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.UnreadMessageButton; import org.briarproject.briar.api.attachment.AttachmentHeader; -import org.briarproject.briar.api.client.NamedGroup; -import java.util.Collection; import java.util.List; -import java.util.logging.Logger; import javax.annotation.Nullable; -import javax.inject.Inject; import androidx.annotation.CallSuper; import androidx.annotation.StringRes; -import androidx.annotation.UiThread; import androidx.appcompat.app.ActionBar; import androidx.recyclerview.widget.LinearLayoutManager; @@ -48,32 +36,19 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @MethodsNotNullByDefault @ParametersNotNullByDefault -public abstract class ThreadListActivity> - extends BriarActivity - implements ThreadListListener, SendListener, SharingListener, - ThreadItemListener, ThreadListDataSource { +public abstract class ThreadListActivity> + extends BriarActivity implements SendListener, ThreadItemListener { - protected static final String KEY_REPLY_ID = "replyId"; - - private static final Logger LOG = - Logger.getLogger(ThreadListActivity.class.getName()); - - protected A adapter; - private ThreadScrollListener scrollListener; + protected final A adapter = createAdapter(); + protected abstract ThreadListViewModel getViewModel(); + protected abstract A createAdapter(); protected BriarRecyclerView list; - private LinearLayoutManager layoutManager; protected TextInputView textInput; protected TextSendController sendController; protected GroupId groupId; - @Nullable - private Parcelable layoutManagerState; - @Nullable - private MessageId replyId; - protected abstract ThreadListController getController(); - - @Inject - protected SharingController sharingController; + private LinearLayoutManager layoutManager; + private ThreadScrollListener scrollListener; @CallSuper @Override @@ -86,7 +61,8 @@ public abstract class ThreadListActivity viewModel = getViewModel(); + viewModel.setGroupId(groupId); textInput = findViewById(R.id.text_input_container); sendController = new TextSendController(textInput, this, false); @@ -100,131 +76,41 @@ public abstract class ThreadListActivity(adapter, getController(), + scrollListener = new ThreadScrollListener<>(adapter, viewModel, 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); - } + viewModel.getItems().observe(this, result -> result + .onError(this::handleException) + .onSuccess(this::displayItems) + ); - sharingController.setSharingListener(this); - loadSharingContacts(); - } + viewModel.getSharingInfo().observe(this, this::setToolbarSubTitle); - @Override - @Nullable - public MessageId getFirstVisibleMessageId() { - if (layoutManager != null && adapter != null) { - int position = - layoutManager.findFirstVisibleItemPosition(); - I i = adapter.getItemAt(position); - return i == null ? null : i.getId(); - } - return null; - } - - protected abstract A createAdapter(LinearLayoutManager layoutManager); - - protected void loadNamedGroup() { - getController().loadNamedGroup( - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(G groupItem) { - onNamedGroupLoaded(groupItem); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - @UiThread - protected abstract void onNamedGroupLoaded(G groupItem); - - protected void loadItems() { - int revision = adapter.getRevision(); - getController().loadItems( - new UiResultExceptionHandler, DbException>( - this) { - @Override - public void onResultUi(ThreadItemList items) { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (items.isEmpty()) { - list.showData(); - } else { - displayItems(items); - updateTextInput(); - } - } else { - LOG.info("Concurrent update, reloading"); - loadItems(); - } - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); - } - - private void displayItems(ThreadItemList items) { - adapter.setItems(items); - MessageId messageId = items.getFirstVisibleItemId(); - if (messageId != null) - adapter.setItemWithIdVisible(messageId); - list.showData(); - if (layoutManagerState == null) { - list.scrollToPosition(0); // Scroll to the top - } else { - layoutManager.onRestoreInstanceState(layoutManagerState); - } - } - - protected void loadSharingContacts() { - getController().loadSharingContacts( - new UiResultExceptionHandler, DbException>( - this) { - @Override - public void onResultUi(Collection contacts) { - sharingController.addAll(contacts); - int online = sharingController.getOnlineCount(); - setToolbarSubTitle(contacts.size(), online); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }); + viewModel.getGroupRemoved().observe(this, removed -> { + if (removed) supportFinishAfterTransition(); + }); } @CallSuper @Override public void onStart() { super.onStart(); - sharingController.onStart(); - loadItems(); + getViewModel().blockAndClearNotifications(); list.startPeriodicUpdate(); } @@ -232,91 +118,98 @@ public abstract class ThreadListActivity items) { + if (items.isEmpty()) { + list.showData(); + } else { + adapter.submitList(items, () -> { + // do stuff *after* list had been updated + scrollAfterListCommitted(); + updateTextInput(); + }); + } + } + + /** + * Scrolls to the first visible item last time the activity was open, + * if one exists and this is the first time, the list gets displayed. + * Or scrolls to a locally added item that has just been added to the list. + */ + private void scrollAfterListCommitted() { + MessageId restoredFirstVisibleItemId = + getViewModel().getAndResetRestoredMessageId(); + MessageId scrollToItem = + getViewModel().getAndResetScrollToItem(); + if (restoredFirstVisibleItemId != null) { + scrollToItemAtTop(restoredFirstVisibleItemId); + } else if (scrollToItem != null) { + scrollToItemAtTop(scrollToItem); + } + scrollListener.updateUnreadButtons(layoutManager); + } + @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()) { - scrollToItemAtTop(item); + scrollToItemAtTop(item.getId()); } else { // wait with scrolling until keyboard opened textInput.setOnKeyboardShownListener(() -> { - scrollToItemAtTop(item); + scrollToItemAtTop(item.getId()); textInput.setOnKeyboardShownListener(null); }); } } - @Override - public void onSharingInfoUpdated(int total, int online) { - setToolbarSubTitle(total, online); - } - - @Override - public void onInvitationAccepted(ContactId c) { - sharingController.add(c); - setToolbarSubTitle(sharingController.getTotalCount(), - sharingController.getOnlineCount()); - } - - protected void setToolbarSubTitle(int total, int online) { + protected void setToolbarSubTitle(SharingInfo sharingInfo) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { - actionBar.setSubtitle( - getString(R.string.shared_with, total, online)); + actionBar.setSubtitle(getString(R.string.shared_with, + sharingInfo.total, sharingInfo.online)); } } - private void scrollToItemAtTop(I item) { - int position = adapter.findItemPosition(item); + private void scrollToItemAtTop(MessageId messageId) { + int position = adapter.findItemPosition(messageId); if (position != NO_POSITION) { - layoutManager - .scrollToPositionWithOffset(position, 0); + layoutManager.scrollToPositionWithOffset(position, 0); } } @@ -327,6 +220,7 @@ public abstract class ThreadListActivity headers) { if (isNullOrEmpty(text)) throw new AssertionError(); - I replyItem = adapter.getHighlightedItem(); - UiResultExceptionHandler handler = - new UiResultExceptionHandler(this) { - @Override - public void onResultUi(I result) { - addItem(result, true); - } - - @Override - public void onExceptionUi(DbException exception) { - handleException(exception); - } - }; - getController().createAndStoreMessage(text, replyItem, handler); + MessageId replyId = getViewModel().getReplyId(); + getViewModel().createAndStoreMessage(text, replyId); textInput.hideSoftKeyboard(); textInput.clearText(); - replyId = null; + getViewModel().setReplyId(null); updateTextInput(); } protected abstract int getMaxTextLength(); - @Override - public void onItemReceived(I item) { - addItem(item, false); - } - - @Override - public void onGroupRemoved() { - supportFinishAfterTransition(); - } - - private void addItem(I item, boolean isLocal) { - adapter.incrementRevision(); - MessageId parent = item.getParentId(); - if (parent != null && !adapter.contains(parent)) { - // We've incremented the adapter's revision, so the item will be - // loaded when its parent has been loaded - LOG.info("Ignoring item with missing parent"); - return; - } - adapter.add(item); - - if (isLocal) { - scrollToItemAtTop(item); - } else { - scrollListener.updateUnreadButtons(layoutManager); - } - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java deleted file mode 100644 index f8eb71bed..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.briarproject.briar.android.threaded; - -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.ExceptionHandler; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.api.client.NamedGroup; - -import java.util.Collection; - -import javax.annotation.Nullable; - -import androidx.annotation.UiThread; - -@NotNullByDefault -public interface ThreadListController - extends ActivityLifecycleController { - - void setGroupId(GroupId groupId); - - void loadNamedGroup(ResultExceptionHandler handler); - - void loadSharingContacts( - ResultExceptionHandler, DbException> handler); - - void loadItems( - ResultExceptionHandler, DbException> handler); - - void markItemRead(I item); - - void markItemsRead(Collection items); - - void createAndStoreMessage(String text, @Nullable I parentItem, - ResultExceptionHandler handler); - - void deleteNamedGroup(ExceptionHandler handler); - - interface ThreadListListener extends ThreadListDataSource { - - @UiThread - void onItemReceived(I item); - - @UiThread - void onGroupRemoved(); - - @UiThread - void onInvitationAccepted(ContactId c); - } - - interface ThreadListDataSource { - - @UiThread @Nullable - MessageId getFirstVisibleMessageId(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java deleted file mode 100644 index 61eeb2a9e..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java +++ /dev/null @@ -1,273 +0,0 @@ -package org.briarproject.briar.android.threaded; - -import android.app.Activity; - -import org.briarproject.bramble.api.crypto.CryptoExecutor; -import org.briarproject.bramble.api.db.DatabaseExecutor; -import org.briarproject.bramble.api.db.DbException; -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; -import org.briarproject.bramble.api.lifecycle.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.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.android.controller.handler.ExceptionHandler; -import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; -import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener; -import org.briarproject.briar.api.android.AndroidNotificationManager; -import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.NamedGroup; -import org.briarproject.briar.api.client.PostHeader; -import org.briarproject.briar.api.client.ThreadedMessage; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import androidx.annotation.CallSuper; - -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logDuration; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.bramble.util.LogUtils.now; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public abstract class ThreadListControllerImpl> - extends DbControllerImpl - implements ThreadListController, EventListener { - - private static final Logger LOG = - Logger.getLogger(ThreadListControllerImpl.class.getName()); - - private final EventBus eventBus; - private final MessageTracker messageTracker; - private final Map textCache = new ConcurrentHashMap<>(); - - private volatile GroupId groupId; - - protected final IdentityManager identityManager; - protected final AndroidNotificationManager notificationManager; - protected final Executor cryptoExecutor; - protected final Clock clock; - - // UI thread - protected L listener; - - protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, IdentityManager identityManager, - @CryptoExecutor Executor cryptoExecutor, EventBus eventBus, - Clock clock, AndroidNotificationManager notificationManager, - MessageTracker messageTracker) { - super(dbExecutor, lifecycleManager); - this.identityManager = identityManager; - this.cryptoExecutor = cryptoExecutor; - this.notificationManager = notificationManager; - this.clock = clock; - this.eventBus = eventBus; - this.messageTracker = messageTracker; - } - - @Override - public void setGroupId(GroupId groupId) { - this.groupId = groupId; - } - - @CallSuper - @SuppressWarnings("unchecked") - @Override - public void onActivityCreate(Activity activity) { - listener = (L) activity; - } - - @CallSuper - @Override - public void onActivityStart() { - notificationManager.blockNotification(getGroupId()); - eventBus.addListener(this); - } - - @CallSuper - @Override - public void onActivityStop() { - notificationManager.unblockNotification(getGroupId()); - eventBus.removeListener(this); - } - - @Override - public void onActivityDestroy() { - MessageId messageId = listener.getFirstVisibleMessageId(); - if (messageId != null) { - dbExecutor.execute(() -> { - try { - messageTracker.storeMessageId(groupId, messageId); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - } - - @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 - public void loadNamedGroup( - ResultExceptionHandler handler) { - checkGroupId(); - runOnDbThread(() -> { - try { - long start = now(); - G groupItem = loadNamedGroup(); - logDuration(LOG, "Loading group", start); - handler.onResult(groupItem); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @DatabaseExecutor - protected abstract G loadNamedGroup() throws DbException; - - @Override - public void loadItems( - ResultExceptionHandler, DbException> handler) { - checkGroupId(); - runOnDbThread(() -> { - try { - // Load headers - long start = now(); - Collection headers = loadHeaders(); - logDuration(LOG, "Loading headers", start); - - // Load bodies into cache - start = now(); - for (H header : headers) { - if (!textCache.containsKey(header.getId())) { - textCache.put(header.getId(), - loadMessageText(header)); - } - } - logDuration(LOG, "Loading bodies", start); - - // Build and hand over items - handler.onResult(buildItems(headers)); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @DatabaseExecutor - protected abstract Collection loadHeaders() throws DbException; - - @DatabaseExecutor - protected abstract String loadMessageText(H header) throws DbException; - - @Override - public void markItemRead(I item) { - markItemsRead(Collections.singletonList(item)); - } - - @Override - public void markItemsRead(Collection items) { - runOnDbThread(() -> { - try { - long start = now(); - for (I i : items) { - markRead(i.getId()); - } - logDuration(LOG, "Marking read", start); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - @DatabaseExecutor - protected abstract void markRead(MessageId id) throws DbException; - - protected void storePost(M msg, String text, - ResultExceptionHandler resultHandler) { - runOnDbThread(() -> { - try { - long start = now(); - H header = addLocalMessage(msg); - textCache.put(msg.getMessage().getId(), text); - logDuration(LOG, "Storing message", start); - resultHandler.onResult(buildItem(header, text)); - } catch (DbException e) { - logException(LOG, WARNING, e); - resultHandler.onException(e); - } - }); - } - - @DatabaseExecutor - protected abstract H addLocalMessage(M message) throws DbException; - - @Override - public void deleteNamedGroup(ExceptionHandler handler) { - runOnDbThread(() -> { - try { - long start = now(); - G groupItem = loadNamedGroup(); - deleteNamedGroup(groupItem); - logDuration(LOG, "Removing group", start); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onException(e); - } - }); - } - - @DatabaseExecutor - protected abstract void deleteNamedGroup(G groupItem) throws DbException; - - private ThreadItemList buildItems(Collection headers) - throws DbException { - ThreadItemList items = new ThreadItemListImpl<>(); - for (H h : headers) { - items.add(buildItem(h, textCache.get(h.getId()))); - } - MessageId msgId = messageTracker.loadStoredMessageId(groupId); - if (LOG.isLoggable(INFO)) - LOG.info("Loaded last top visible message id " + msgId); - items.setFirstVisibleId(msgId); - return items; - } - - protected abstract I buildItem(H header, String text); - - protected GroupId getGroupId() { - checkGroupId(); - return groupId; - } - - private void checkGroupId() { - if (groupId == null) throw new IllegalStateException(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java new file mode 100644 index 000000000..06100d636 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListViewModel.java @@ -0,0 +1,256 @@ +package org.briarproject.briar.android.threaded; + +import android.app.Application; + +import org.briarproject.bramble.api.crypto.CryptoExecutor; +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +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; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.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.sharing.SharingController; +import org.briarproject.briar.android.sharing.SharingController.SharingInfo; +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; +import org.briarproject.briar.api.client.MessageTree; +import org.briarproject.briar.client.MessageTreeImpl; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import androidx.annotation.CallSuper; +import androidx.annotation.UiThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public abstract class ThreadListViewModel + extends DbViewModel implements EventListener { + + private static final Logger LOG = + getLogger(ThreadListViewModel.class.getName()); + + protected final IdentityManager identityManager; + protected final AndroidNotificationManager notificationManager; + protected final SharingController sharingController; + protected final Executor cryptoExecutor; + protected final Clock clock; + private final MessageTracker messageTracker; + private final EventBus eventBus; + + // UIThread + private final MessageTree messageTree = new MessageTreeImpl<>(); + private final MutableLiveData>> items = + new MutableLiveData<>(); + private final MutableLiveData groupRemoved = + new MutableLiveData<>(); + private final AtomicReference scrollToItem = + new AtomicReference<>(); + + protected volatile GroupId groupId; + @Nullable + private MessageId replyId; + /** + * Stored list position. Needs to be loaded and set before the list itself. + */ + private final AtomicReference storedMessageId = + new AtomicReference<>(); + + public ThreadListViewModel(Application application, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor, + IdentityManager identityManager, + AndroidNotificationManager notificationManager, + SharingController sharingController, + @CryptoExecutor Executor cryptoExecutor, + Clock clock, + MessageTracker messageTracker, + EventBus eventBus) { + super(application, dbExecutor, lifecycleManager, db, androidExecutor); + this.identityManager = identityManager; + this.notificationManager = notificationManager; + this.cryptoExecutor = cryptoExecutor; + this.clock = clock; + this.sharingController = sharingController; + this.messageTracker = messageTracker; + this.eventBus = eventBus; + this.eventBus.addListener(this); + } + + @Override + protected void onCleared() { + super.onCleared(); + eventBus.removeListener(this); + sharingController.onCleared(); + } + + /** + * Needs to be called right after initialization, + * before calling any other methods. + */ + public final void setGroupId(GroupId groupId) { + boolean needsInitialLoad = this.groupId == null; + this.groupId = groupId; + if (needsInitialLoad) performInitialLoad(); + } + + @CallSuper + protected void performInitialLoad() { + // load stored MessageId (last list position) before the list itself + loadStoredMessageId(); + loadItems(); + loadSharingContacts(); + } + + protected abstract void clearNotifications(); + + void blockAndClearNotifications() { + notificationManager.blockNotification(groupId); + clearNotifications(); + } + + 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 { + storedMessageId + .set(messageTracker.loadStoredMessageId(groupId)); + if (LOG.isLoggable(INFO)) { + LOG.info("Loaded last top visible message id " + + storedMessageId); + } + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + public abstract void loadItems(); + + public abstract void createAndStoreMessage(String text, + @Nullable MessageId parentMessageId); + + /** + * Loads the ContactIds of all contacts the group is shared with + * and adds them to {@link SharingController}. + */ + protected abstract void loadSharingContacts(); + + @UiThread + protected void setItems(LiveResult> items) { + if (items.hasError()) { + this.items.setValue(items); + } else { + messageTree.clear(); + // not null, because hasError() is false + messageTree.add(requireNonNull(items.getResultOrNull())); + LiveResult> result = + new LiveResult<>(messageTree.depthFirstOrder()); + this.items.setValue(result); + } + } + + /** + * Add a remote item on the UI thread. + * + * @param scrollToItem whether the list will scroll to the newly added item + */ + @UiThread + protected void addItem(I item, boolean scrollToItem) { + messageTree.add(item); + if (scrollToItem) this.scrollToItem.set(item.getId()); + items.setValue(new LiveResult<>(messageTree.depthFirstOrder())); + } + + @UiThread + void setReplyId(@Nullable MessageId id) { + replyId = id; + } + + @UiThread + @Nullable + MessageId getReplyId() { + return replyId; + } + + void storeMessageId(@Nullable MessageId messageId) { + if (messageId != null) { + runOnDbThread(() -> { + try { + messageTracker.storeMessageId(groupId, messageId); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + } + + protected abstract void markItemRead(I item); + + /** + * Returns the {@link MessageId} of the item that was at the top of the + * list last time or null if there has been nothing stored, yet. + */ + @Nullable + MessageId getAndResetRestoredMessageId() { + return storedMessageId.getAndSet(null); + } + + LiveData>> getItems() { + return items; + } + + LiveData getSharingInfo() { + return sharingController.getSharingInfo(); + } + + LiveData getGroupRemoved() { + return groupRemoved; + } + + @Nullable + MessageId getAndResetScrollToItem() { + return scrollToItem.getAndSet(null); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadScrollListener.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadScrollListener.java index 2181d6de3..e0f1ec327 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadScrollListener.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadScrollListener.java @@ -20,15 +20,15 @@ class ThreadScrollListener private static final Logger LOG = getLogger(ThreadScrollListener.class.getName()); - private final ThreadListController controller; + private final ThreadListViewModel viewModel; private final UnreadMessageButton upButton, downButton; ThreadScrollListener(ThreadItemAdapter adapter, - ThreadListController controller, + ThreadListViewModel viewModel, UnreadMessageButton upButton, UnreadMessageButton downButton) { super(adapter); - this.controller = controller; + this.viewModel = viewModel; this.upButton = upButton; this.downButton = downButton; } @@ -44,7 +44,7 @@ class ThreadScrollListener protected void onItemVisible(I item) { if (!item.isRead()) { item.setRead(true); - controller.markItemRead(item); + viewModel.markItemRead(item); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java index b6966c95a..e237b3962 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/DbViewModel.java @@ -5,6 +5,7 @@ import android.app.Application; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbCallable; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.DbRunnable; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager; @@ -23,6 +24,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.arch.core.util.Function; +import androidx.core.util.Consumer; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.recyclerview.widget.RecyclerView; @@ -57,8 +59,8 @@ public abstract class DbViewModel extends AndroidViewModel { } /** - * Runs the given task on the {@link DatabaseExecutor} - * and waits for the DB to open. + * Waits for the DB to open and runs the given task on the + * {@link DatabaseExecutor}. *

* If you need a list of items to be displayed in a * {@link RecyclerView.Adapter}, @@ -76,6 +78,29 @@ public abstract class DbViewModel extends AndroidViewModel { }); } + /** + * Waits for the DB to open and runs the given task on the + * {@link DatabaseExecutor}. + *

+ * If you need a list of items to be displayed in a + * {@link RecyclerView.Adapter}, + * use {@link #loadList(DbCallable, UiConsumer)} instead. + */ + protected void runOnDbThread(boolean readOnly, + DbRunnable task, Consumer err) { + dbExecutor.execute(() -> { + try { + lifecycleManager.waitForDatabase(); + db.transaction(readOnly, task); + } catch (InterruptedException e) { + LOG.warning("Interrupted while waiting for database"); + Thread.currentThread().interrupt(); + } catch (Exception e) { + err.accept(e); + } + }); + } + /** * Loads a list of items on the {@link DatabaseExecutor} within a single * {@link Transaction} and publishes it as a {@link LiveResult} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java index 15eb83e8e..e4e8e8308 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/viewmodel/LiveEvent.java @@ -39,6 +39,17 @@ public class LiveEvent extends LiveData> { super.observeForever(observer); } + /** + * Returns the last value of the event (even if already consumed) + * or null if there hasn't been any value so far. + */ + @Nullable + public T getLastValue() { + ConsumableEvent event = getValue(); + if (event == null) return null; + return event.content; + } + static class ConsumableEvent { private final T content; diff --git a/briar-android/src/main/res/menu/group_actions.xml b/briar-android/src/main/res/menu/group_actions.xml index 000e6e38b..b5bed53b6 100644 --- a/briar-android/src/main/res/menu/group_actions.xml +++ b/briar-android/src/main/res/menu/group_actions.xml @@ -1,36 +1,44 @@ -

+ + android:visible="false" + app:showAsAction="ifRoom" + tools:visible="true" /> + app:showAsAction="never" /> + android:visible="false" + app:showAsAction="never" + tools:visible="true" /> + android:visible="false" + app:showAsAction="never" + tools:visible="true" /> + android:visible="false" + app:showAsAction="never" + tools:visible="true" /> \ No newline at end of file diff --git a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java index 50c4c80fc..e69de29bb 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java @@ -1,120 +0,0 @@ -package org.briarproject.briar.android.forum; - -import android.content.Intent; - -import junit.framework.Assert; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.briar.api.identity.AuthorInfo; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; -import org.briarproject.briar.android.threaded.ThreadItemAdapter; -import org.briarproject.briar.android.threaded.ThreadItemList; -import org.briarproject.briar.android.threaded.ThreadItemListImpl; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.Arrays; - -import static junit.framework.Assert.assertEquals; -import static org.briarproject.briar.api.identity.AuthorInfo.Status.UNKNOWN; -import static org.briarproject.bramble.test.TestUtils.getAuthor; -import static org.briarproject.bramble.test.TestUtils.getRandomId; -import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@RunWith(RobolectricTestRunner.class) -@Config(sdk = 21) -public class ForumActivityTest { - - private final static MessageId[] MESSAGE_IDS = new MessageId[6]; - - static { - for (int i = 0; i < MESSAGE_IDS.length; i++) - MESSAGE_IDS[i] = new MessageId(getRandomId()); - } - - private final static MessageId[] PARENT_IDS = { - null, - MESSAGE_IDS[0], - MESSAGE_IDS[1], - MESSAGE_IDS[2], - MESSAGE_IDS[0], - null - }; - - /* - 1 - -> 2 - -> 3 - -> 4 - 5 - 6 - */ - private final static int[] LEVELS = { - 0, 1, 2, 3, 1, 0 - }; - - private TestForumActivity forumActivity; - @Captor - private ArgumentCaptor, DbException>> - rc; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - Intent intent = new Intent(); - intent.putExtra("briar.GROUP_ID", getRandomId()); - forumActivity = Robolectric.buildActivity(TestForumActivity.class, - intent).create().start().resume().get(); - } - - private ThreadItemList getDummyData() { - ForumPostItem[] forumPostItems = new ForumPostItem[6]; - for (int i = 0; i < forumPostItems.length; i++) { - Author author = getAuthor(); - String text = getRandomString(MAX_FORUM_POST_TEXT_LENGTH); - forumPostItems[i] = new ForumPostItem(MESSAGE_IDS[i], PARENT_IDS[i], - text, System.currentTimeMillis(), author, - new AuthorInfo(UNKNOWN)); - forumPostItems[i].setLevel(LEVELS[i]); - } - ThreadItemList list = new ThreadItemListImpl<>(); - list.addAll(Arrays.asList(forumPostItems)); - return list; - } - - @Test - public void testNestedEntries() { - ForumController mc = forumActivity.getController(); - ThreadItemList dummyData = getDummyData(); - verify(mc, times(1)).loadItems(rc.capture()); - rc.getValue().onResult(dummyData); - ThreadItemAdapter adapter = forumActivity.getAdapter(); - Assert.assertNotNull(adapter); - assertEquals(6, adapter.getItemCount()); - assertEquals(dummyData.get(0).getText(), - adapter.getItemAt(0).getText()); - assertEquals(dummyData.get(1).getText(), - adapter.getItemAt(1).getText()); - assertEquals(dummyData.get(2).getText(), - adapter.getItemAt(2).getText()); - assertEquals(dummyData.get(3).getText(), - adapter.getItemAt(3).getText()); - assertEquals(dummyData.get(4).getText(), - adapter.getItemAt(4).getText()); - assertEquals(dummyData.get(5).getText(), - adapter.getItemAt(5).getText()); - } - -} diff --git a/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java b/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java deleted file mode 100644 index dd71be55f..000000000 --- a/briar-android/src/test/java/org/briarproject/briar/android/forum/TestForumActivity.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.briarproject.briar.android.forum; - -import android.os.Bundle; - -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.ActivityModule; -import org.briarproject.briar.android.activity.BaseActivity; -import org.briarproject.briar.android.controller.BriarController; -import org.briarproject.briar.android.controller.BriarControllerImpl; -import org.briarproject.briar.android.threaded.ThreadItemAdapter; -import org.mockito.Mockito; - -import javax.annotation.Nullable; - -/** - * This class exposes the ForumController and offers the possibility to - * override it. - */ -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public class TestForumActivity extends ForumActivity { - - @Override - public ForumController getController() { - return forumController; - } - - public ThreadItemAdapter getAdapter() { - return adapter; - } - - @Override - public void onCreate(@Nullable Bundle state) { - setTheme(R.style.BriarTheme_NoActionBar); - super.onCreate(state); - } - - @Override - protected ActivityModule getActivityModule() { - return new ActivityModule(this) { - - @Override - protected BriarController provideBriarController( - BriarControllerImpl briarController) { - BriarController c = Mockito.mock(BriarController.class); - Mockito.when(c.accountSignedIn()).thenReturn(true); - return c; - } - - }; - } - - @Override - protected ForumModule getForumModule() { - return new ForumModule() { - @Override - ForumController provideForumController(BaseActivity activity, - ForumControllerImpl forumController) { - return Mockito.mock(ForumController.class); - } - }; - } -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java index e850391d0..579059a01 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTree.java @@ -4,7 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import java.util.Collection; -import java.util.Comparator; +import java.util.List; import javax.annotation.Nullable; @@ -15,11 +15,9 @@ public interface MessageTree { void add(T node); - void setComparator(Comparator comparator); - void clear(); - Collection depthFirstOrder(); + List depthFirstOrder(); boolean contains(MessageId m); diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java index 7d6bd42a9..cb1e0878b 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumManager.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.MessageTracker.GroupCount; import java.util.Collection; +import java.util.List; import javax.annotation.Nullable; @@ -59,6 +60,12 @@ public interface ForumManager { */ ForumPostHeader addLocalPost(ForumPost p) throws DbException; + /** + * Stores a local forum post. + */ + ForumPostHeader addLocalPost(Transaction txn, ForumPost p) + throws DbException; + /** * Returns the forum with the given ID. */ @@ -84,11 +91,22 @@ public interface ForumManager { */ String getPostText(MessageId m) throws DbException; + /** + * Returns the text of the forum post with the given ID. + */ + String getPostText(Transaction txn, MessageId m) throws DbException; + /** * Returns the headers of all posts in the given forum. */ Collection getPostHeaders(GroupId g) throws DbException; + /** + * Returns the headers of all posts in the given forum. + */ + List getPostHeaders(Transaction txn, GroupId g) + throws DbException; + /** * Registers a hook to be called whenever a forum is removed. */ @@ -97,7 +115,6 @@ public interface ForumManager { /** * Returns the group count for the given forum. */ - @Deprecated GroupCount getGroupCount(GroupId g) throws DbException; /** diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/JoinMessageHeader.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/JoinMessageHeader.java index dfcfeff5d..f84b59d82 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/JoinMessageHeader.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/JoinMessageHeader.java @@ -8,21 +8,14 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class JoinMessageHeader extends GroupMessageHeader { - private final Visibility visibility; private final boolean isInitial; - public JoinMessageHeader(GroupMessageHeader h, Visibility visibility, - boolean isInitial) { + public JoinMessageHeader(GroupMessageHeader h, boolean isInitial) { super(h.getGroupId(), h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(), h.getAuthorInfo(), h.isRead()); - this.visibility = visibility; this.isInitial = isInitial; } - public Visibility getVisibility() { - return visibility; - } - public boolean isInitial() { return isInitial; } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java index 478b03975..a6873b52a 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/PrivateGroupManager.java @@ -12,6 +12,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.MessageTracker.GroupCount; import java.util.Collection; +import java.util.List; @NotNullByDefault public interface PrivateGroupManager { @@ -81,6 +82,12 @@ public interface PrivateGroupManager { */ GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException; + /** + * Stores and sends a local private group message. + */ + GroupMessageHeader addLocalMessage(Transaction txn, GroupMessage p) + throws DbException; + /** * Returns the private group with the given ID. */ @@ -107,16 +114,33 @@ public interface PrivateGroupManager { */ String getMessageText(MessageId m) throws DbException; + /** + * Returns the text of the private group message with the given ID. + */ + String getMessageText(Transaction txn, MessageId m) throws DbException; + /** * Returns the headers of all messages in the given private group. */ Collection getHeaders(GroupId g) throws DbException; + /** + * Returns the headers of all messages in the given private group. + */ + List getHeaders(Transaction txn, GroupId g) + throws DbException; + /** * Returns all members of the given private group. */ Collection getMembers(GroupId g) throws DbException; + /** + * Returns all members of the given private group. + */ + Collection getMembers(Transaction txn, GroupId g) + throws DbException; + /** * Returns true if the given author is a member of the given private group. */ diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java index 3e6adc2dc..2170744ed 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/SharingManager.java @@ -3,6 +3,7 @@ package org.briarproject.briar.api.sharing; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.briar.api.client.SessionId; @@ -45,6 +46,12 @@ public interface SharingManager */ Collection getSharedWith(GroupId g) throws DbException; + /** + * Returns all contacts with whom the given group is shared. + */ + Collection getSharedWith(Transaction txn, GroupId g) + throws DbException; + /** * Returns true if the group not already shared and no invitation is open */ diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java index 8176e3969..caca96b11 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/MessageTreeImpl.java @@ -30,7 +30,7 @@ public class MessageTreeImpl private final List> unsortedLists = new ArrayList<>(); @SuppressWarnings("UseCompareMethod") - private Comparator comparator = (o1, o2) -> + private final Comparator comparator = (o1, o2) -> Long.valueOf(o1.getTimestamp()).compareTo(o2.getTimestamp()); @Override @@ -79,6 +79,7 @@ public class MessageTreeImpl @GuardedBy("this") private void sortUnsorted() { for (List list : unsortedLists) { + //noinspection Java8ListSort Collections.sort(list, comparator); } unsortedLists.clear(); @@ -95,17 +96,7 @@ public class MessageTreeImpl } @Override - public synchronized void setComparator(Comparator comparator) { - this.comparator = comparator; - // Sort all lists with the new comparator - Collections.sort(roots, comparator); - for (Map.Entry> entry : nodeMap.entrySet()) { - Collections.sort(entry.getValue(), comparator); - } - } - - @Override - public synchronized Collection depthFirstOrder() { + public synchronized List depthFirstOrder() { List orderedList = new ArrayList<>(); for (T root : roots) { traverse(orderedList, root, 0); diff --git a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java index 4e4a63af3..6dacf6c4d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/forum/ForumManagerImpl.java @@ -126,29 +126,30 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { @Override public ForumPostHeader addLocalPost(ForumPost p) throws DbException { - return db.transactionWithResult(false, txn -> { - try { - return addLocalPost(txn, p); - } catch (FormatException e) { - throw new AssertionError(e); - } - }); + return db.transactionWithResult(false, txn -> addLocalPost(txn, p)); } - private ForumPostHeader addLocalPost(Transaction txn, ForumPost p) - throws DbException, FormatException { - BdfDictionary meta = new BdfDictionary(); - meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp()); - if (p.getParent() != null) meta.put(KEY_PARENT, p.getParent()); - Author a = p.getAuthor(); - meta.put(KEY_AUTHOR, clientHelper.toList(a)); - meta.put(KEY_LOCAL, true); - meta.put(MSG_KEY_READ, true); - clientHelper.addLocalMessage(txn, p.getMessage(), meta, true, false); - messageTracker.trackOutgoingMessage(txn, p.getMessage()); - AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn); - return new ForumPostHeader(p.getMessage().getId(), p.getParent(), - p.getMessage().getTimestamp(), p.getAuthor(), authorInfo, true); + @Override + public ForumPostHeader addLocalPost(Transaction txn, ForumPost p) + throws DbException { + try { + BdfDictionary meta = new BdfDictionary(); + meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp()); + if (p.getParent() != null) meta.put(KEY_PARENT, p.getParent()); + Author a = p.getAuthor(); + meta.put(KEY_AUTHOR, clientHelper.toList(a)); + meta.put(KEY_LOCAL, true); + meta.put(MSG_KEY_READ, true); + clientHelper + .addLocalMessage(txn, p.getMessage(), meta, true, false); + messageTracker.trackOutgoingMessage(txn, p.getMessage()); + AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn); + return new ForumPostHeader(p.getMessage().getId(), p.getParent(), + p.getMessage().getTimestamp(), p.getAuthor(), authorInfo, + true); + } catch (FormatException e) { + throw new AssertionError(e); + } } @Override @@ -192,6 +193,15 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { } } + @Override + public String getPostText(Transaction txn, MessageId m) throws DbException { + try { + return getPostText(clientHelper.getMessageAsList(txn, m)); + } catch (FormatException e) { + throw new DbException(e); + } + } + private String getPostText(BdfList body) throws FormatException { // Parent ID, author, text, signature return body.getString(2); @@ -200,33 +210,35 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager { @Override public Collection getPostHeaders(GroupId g) throws DbException { + return db.transactionWithResult(true, txn -> getPostHeaders(txn, g)); + } + + @Override + public List getPostHeaders(Transaction txn, GroupId g) + throws DbException { try { - return db.transactionWithResult(true, txn -> { - Collection headers = new ArrayList<>(); - Map metadata = - clientHelper.getMessageMetadataAsDictionary(txn, g); - // get all authors we need to get the info for - Set authors = new HashSet<>(); - for (Entry entry : - metadata.entrySet()) { - BdfList authorList = entry.getValue().getList(KEY_AUTHOR); - Author a = clientHelper.parseAndValidateAuthor(authorList); - authors.add(a.getId()); - } - // get information for all authors - Map authorInfos = new HashMap<>(); - for (AuthorId id : authors) { - authorInfos.put(id, authorManager.getAuthorInfo(txn, id)); - } - // Parse the metadata - for (Entry entry : - metadata.entrySet()) { - BdfDictionary meta = entry.getValue(); - headers.add(getForumPostHeader(txn, entry.getKey(), meta, - authorInfos)); - } - return headers; - }); + List headers = new ArrayList<>(); + Map metadata = + clientHelper.getMessageMetadataAsDictionary(txn, g); + // get all authors we need to get the info for + Set authors = new HashSet<>(); + for (Entry entry : metadata.entrySet()) { + BdfList authorList = entry.getValue().getList(KEY_AUTHOR); + Author a = clientHelper.parseAndValidateAuthor(authorList); + authors.add(a.getId()); + } + // get information for all authors + Map authorInfos = new HashMap<>(); + for (AuthorId id : authors) { + authorInfos.put(id, authorManager.getAuthorInfo(txn, id)); + } + // Parse the metadata + for (Entry entry : metadata.entrySet()) { + BdfDictionary meta = entry.getValue(); + headers.add(getForumPostHeader(txn, entry.getKey(), meta, + authorInfos)); + } + return headers; } catch (FormatException e) { throw new DbException(e); } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java index 7bf044347..4f0a101f5 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/PrivateGroupManagerImpl.java @@ -145,7 +145,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE); setPreviousMsgId(txn, m.getMessage().getGroupId(), m.getMessage().getId()); - attachJoinMessageAddedEvent(txn, m.getMessage(), meta, true, VISIBLE); + attachJoinMessageAddedEvent(txn, m.getMessage(), meta, true); } @Override @@ -210,34 +210,35 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook @Override public GroupMessageHeader addLocalMessage(GroupMessage m) throws DbException { - return db.transactionWithResult(false, txn -> { - try { - return addLocalMessage(txn, m); - } catch (FormatException e) { - throw new DbException(e); - } - }); + return db.transactionWithResult(false, txn -> addLocalMessage(txn, m)); } - private GroupMessageHeader addLocalMessage(Transaction txn, GroupMessage m) - throws DbException, FormatException { - // store message and metadata - BdfDictionary meta = new BdfDictionary(); - meta.put(KEY_TYPE, POST.getInt()); - if (m.getParent() != null) - meta.put(KEY_PARENT_MSG_ID, m.getParent()); - addMessageMetadata(meta, m); - GroupId g = m.getMessage().getGroupId(); - clientHelper.addLocalMessage(txn, m.getMessage(), meta, true, false); - // track message - setPreviousMsgId(txn, g, m.getMessage().getId()); - messageTracker.trackOutgoingMessage(txn, m.getMessage()); - // broadcast event - attachGroupMessageAddedEvent(txn, m.getMessage(), meta, true); - AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn); - return new GroupMessageHeader(m.getMessage().getGroupId(), - m.getMessage().getId(), m.getParent(), - m.getMessage().getTimestamp(), m.getMember(), authorInfo, true); + @Override + public GroupMessageHeader addLocalMessage(Transaction txn, GroupMessage m) + throws DbException { + try { + // store message and metadata + BdfDictionary meta = new BdfDictionary(); + meta.put(KEY_TYPE, POST.getInt()); + if (m.getParent() != null) + meta.put(KEY_PARENT_MSG_ID, m.getParent()); + addMessageMetadata(meta, m); + GroupId g = m.getMessage().getGroupId(); + clientHelper + .addLocalMessage(txn, m.getMessage(), meta, true, false); + // track message + setPreviousMsgId(txn, g, m.getMessage().getId()); + messageTracker.trackOutgoingMessage(txn, m.getMessage()); + // broadcast event + attachGroupMessageAddedEvent(txn, m.getMessage(), meta, true); + AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn); + return new GroupMessageHeader(m.getMessage().getGroupId(), + m.getMessage().getId(), m.getParent(), + m.getMessage().getTimestamp(), m.getMember(), authorInfo, + true); + } catch (FormatException e) { + throw new DbException(e); + } } private void addMessageMetadata(BdfDictionary meta, GroupMessage m) { @@ -315,6 +316,16 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook } } + @Override + public String getMessageText(Transaction txn, MessageId m) + throws DbException { + try { + return getMessageText(clientHelper.getMessageAsList(txn, m)); + } catch (FormatException e) { + throw new DbException(e); + } + } + private String getMessageText(BdfList body) throws FormatException { // Message type (0), member (1), parent ID (2), previous message ID (3), // text (4), signature (5) @@ -324,8 +335,13 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook @Override public Collection getHeaders(GroupId g) throws DbException { - Collection headers = new ArrayList<>(); - Transaction txn = db.startTransaction(true); + return db.transactionWithResult(true, txn -> getHeaders(txn, g)); + } + + @Override + public List getHeaders(Transaction txn, GroupId g) + throws DbException { + List headers = new ArrayList<>(); try { Map metadata = clientHelper.getMessageMetadataAsDictionary(txn, g); @@ -339,27 +355,20 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook for (AuthorId id : authors) { authorInfos.put(id, authorManager.getAuthorInfo(txn, id)); } - // get current visibilities for join messages - Map visibilities = getMembers(txn, g); // parse the metadata for (Entry entry : metadata.entrySet()) { BdfDictionary meta = entry.getValue(); if (meta.getLong(KEY_TYPE) == JOIN.getInt()) { - Author member = getAuthor(meta); - Visibility v = visibilities.get(member); headers.add(getJoinMessageHeader(txn, g, entry.getKey(), - meta, authorInfos, v)); + meta, authorInfos)); } else { headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta, authorInfos)); } } - db.commitTransaction(txn); return headers; } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } @@ -389,46 +398,45 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook private JoinMessageHeader getJoinMessageHeader(Transaction txn, GroupId g, MessageId id, BdfDictionary meta, - Map authorInfos, Visibility v) + Map authorInfos) throws DbException, FormatException { GroupMessageHeader header = getGroupMessageHeader(txn, g, id, meta, authorInfos); boolean creator = meta.getBoolean(KEY_INITIAL_JOIN_MSG); - return new JoinMessageHeader(header, v, creator); + return new JoinMessageHeader(header, creator); } @Override public Collection getMembers(GroupId g) throws DbException { - Transaction txn = db.startTransaction(true); - try { - Collection members = new ArrayList<>(); - Map authors = getMembers(txn, g); - LocalAuthor la = identityManager.getLocalAuthor(txn); - PrivateGroup privateGroup = getPrivateGroup(txn, g); - for (Entry m : authors.entrySet()) { - Author a = m.getKey(); - AuthorInfo authorInfo = - authorManager.getAuthorInfo(txn, a.getId()); - Status status = authorInfo.getStatus(); - Visibility v = m.getValue(); - ContactId c = null; - if (v != INVISIBLE && - (status == VERIFIED || status == UNVERIFIED)) { - c = contactManager.getContact(txn, a.getId(), la.getId()) - .getId(); - } - boolean isCreator = privateGroup.getCreator().equals(a); - members.add(new GroupMember(a, authorInfo, isCreator, c, v)); - } - db.commitTransaction(txn); - return members; - } finally { - db.endTransaction(txn); - } + return db.transactionWithResult(true, txn -> getMembers(txn, g)); } - private Map getMembers(Transaction txn, GroupId g) + @Override + public Collection getMembers(Transaction txn, GroupId g) + throws DbException { + Collection members = new ArrayList<>(); + Map authors = getMemberAuthors(txn, g); + LocalAuthor la = identityManager.getLocalAuthor(txn); + PrivateGroup privateGroup = getPrivateGroup(txn, g); + for (Entry m : authors.entrySet()) { + Author a = m.getKey(); + AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, a.getId()); + Status status = authorInfo.getStatus(); + Visibility v = m.getValue(); + ContactId c = null; + if (v != INVISIBLE && + (status == VERIFIED || status == UNVERIFIED)) { + c = contactManager.getContact(txn, a.getId(), la.getId()) + .getId(); + } + boolean isCreator = privateGroup.getCreator().equals(a); + members.add(new GroupMember(a, authorInfo, isCreator, c, v)); + } + return members; + } + + private Map getMemberAuthors(Transaction txn, GroupId g) throws DbException { try { BdfDictionary meta = @@ -450,7 +458,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook @Override public boolean isMember(Transaction txn, GroupId g, Author a) throws DbException { - for (Author member : getMembers(txn, g).keySet()) { + for (Author member : getMemberAuthors(txn, g).keySet()) { if (member.equals(a)) return true; } return false; @@ -544,7 +552,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook addMember(txn, m.getGroupId(), member, v); // track message and broadcast event messageTracker.trackIncomingMessage(txn, m); - attachJoinMessageAddedEvent(txn, m, meta, false, v); + attachJoinMessageAddedEvent(txn, m, meta, false); } private void handleGroupMessage(Transaction txn, Message m, @@ -594,10 +602,10 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook } private void attachJoinMessageAddedEvent(Transaction txn, Message m, - BdfDictionary meta, boolean local, Visibility v) + BdfDictionary meta, boolean local) throws DbException, FormatException { JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(), - m.getId(), meta, Collections.emptyMap(), v); + m.getId(), meta, Collections.emptyMap()); txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, "", local)); } diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java index 2b3abbd64..4a24ece90 100644 --- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java @@ -426,17 +426,17 @@ abstract class SharingManagerImpl @Override public Collection getSharedWith(GroupId g) throws DbException { + return db.transactionWithResult(true, txn -> getSharedWith(txn, g)); + } + + @Override + public Collection getSharedWith(Transaction txn, GroupId g) + throws DbException { // TODO report also pending invitations Collection contacts = new ArrayList<>(); - Transaction txn = db.startTransaction(true); - try { - for (Contact c : db.getContacts(txn)) { - if (db.getGroupVisibility(txn, c.getId(), g) == SHARED) - contacts.add(c); - } - db.commitTransaction(txn); - } finally { - db.endTransaction(txn); + for (Contact c : db.getContacts(txn)) { + if (db.getGroupVisibility(txn, c.getId(), g) == SHARED) + contacts.add(c); } return contacts; } diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/PrivateGroupManagerIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/PrivateGroupManagerIntegrationTest.java index 8a87fbe20..8dd6e855b 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/PrivateGroupManagerIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/PrivateGroupManagerIntegrationTest.java @@ -124,10 +124,8 @@ public class PrivateGroupManagerIntegrationTest addGroup(); // create and add test message with no previousMsgId - @SuppressWarnings("ConstantConditions") - GroupMessage msg = groupMessageFactory - .createGroupMessage(groupId0, clock.currentTimeMillis(), null, - author0, "test", null); + GroupMessage msg = groupMessageFactory.createGroupMessage(groupId0, + clock.currentTimeMillis(), null, author0, "test", null); groupManager0.addLocalMessage(msg); // sync test message @@ -342,52 +340,19 @@ public class PrivateGroupManagerIntegrationTest Collection members0 = groupManager0.getMembers(groupId0); assertEquals(2, members0.size()); for (GroupMember m : members0) { - if (m.getAuthor().equals(author0)) { - assertEquals(VISIBLE, m.getVisibility()); - } else { + if (!m.getAuthor().equals(author0)) { assertEquals(author1, m.getAuthor()); - assertEquals(VISIBLE, m.getVisibility()); } + assertEquals(VISIBLE, m.getVisibility()); } Collection members1 = groupManager1.getMembers(groupId0); assertEquals(2, members1.size()); for (GroupMember m : members1) { - if (m.getAuthor().equals(author1)) { - assertEquals(VISIBLE, m.getVisibility()); - } else { + if (!m.getAuthor().equals(author1)) { assertEquals(author0, m.getAuthor()); - assertEquals(VISIBLE, m.getVisibility()); - } - } - } - - @Test - public void testJoinMessages() throws Exception { - addGroup(); - - Collection headers0 = - groupManager0.getHeaders(groupId0); - for (GroupMessageHeader h : headers0) { - if (h instanceof JoinMessageHeader) { - JoinMessageHeader j = (JoinMessageHeader) h; - // all relationships of the creator are visible - assertEquals(VISIBLE, j.getVisibility()); - } - } - - Collection headers1 = - groupManager1.getHeaders(groupId0); - for (GroupMessageHeader h : headers1) { - if (h instanceof JoinMessageHeader) { - JoinMessageHeader j = (JoinMessageHeader) h; - if (h.getAuthor().equals(author1)) - // we are visible to ourselves - assertEquals(VISIBLE, j.getVisibility()); - else - // our relationship to the creator is visible - assertEquals(VISIBLE, j.getVisibility()); } + assertEquals(VISIBLE, m.getVisibility()); } } @@ -463,34 +428,6 @@ public class PrivateGroupManagerIntegrationTest assertEquals(REVEALED_BY_CONTACT, m.getVisibility()); } } - - // assert that join messages reflect revealed relationship - Collection headers1 = - groupManager1.getHeaders(groupId0); - for (GroupMessageHeader h : headers1) { - if (h instanceof JoinMessageHeader) { - JoinMessageHeader j = (JoinMessageHeader) h; - if (h.getAuthor().equals(author2)) - // 1 revealed the relationship to 2 - assertEquals(REVEALED_BY_US, j.getVisibility()); - else - // 1's other relationship (to 1 and creator) are visible - assertEquals(VISIBLE, j.getVisibility()); - } - } - Collection headers2 = - groupManager2.getHeaders(groupId0); - for (GroupMessageHeader h : headers2) { - if (h instanceof JoinMessageHeader) { - JoinMessageHeader j = (JoinMessageHeader) h; - if (h.getAuthor().equals(author1)) - // 2's relationship was revealed by 1 - assertEquals(REVEALED_BY_CONTACT, j.getVisibility()); - else - // 2's other relationship (to 2 and creator) are visible - assertEquals(VISIBLE, j.getVisibility()); - } - } } @Test