diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
index 4c4ba9b96..ae3e770a4 100644
--- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java
@@ -3,16 +3,14 @@ package org.briarproject;
import net.jodah.concurrentunit.Waiter;
import org.briarproject.api.Bytes;
-import org.briarproject.api.clients.MessageQueueManager;
import org.briarproject.api.clients.ContactGroupFactory;
+import org.briarproject.api.clients.MessageQueueManager;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.db.DatabaseComponent;
@@ -820,12 +818,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
// sharer posts into the forum
long time = clock.currentTimeMillis();
- byte[] body = TestUtils.getRandomBytes(42);
- KeyParser keyParser = cryptoComponent.getSignatureKeyParser();
- PrivateKey key = keyParser.parsePrivateKey(author0.getPrivateKey());
+ String body = TestUtils.getRandomString(42);
ForumPost p = forumPostFactory
.createPseudonymousPost(forum0.getId(), time, null, author0,
- "text/plain", body, key);
+ body);
forumManager0.addLocalPost(p);
// sync forum post
@@ -841,11 +837,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
// now invitee creates a post
time = clock.currentTimeMillis();
- body = TestUtils.getRandomBytes(42);
- key = keyParser.parsePrivateKey(author1.getPrivateKey());
+ body = TestUtils.getRandomString(42);
p = forumPostFactory
.createPseudonymousPost(forum0.getId(), time, null, author1,
- "text/plain", body, key);
+ body);
forumManager1.addLocalPost(p);
// sync forum post
@@ -886,11 +881,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
// now invitee creates a post
time = clock.currentTimeMillis();
- body = TestUtils.getRandomBytes(42);
- key = keyParser.parsePrivateKey(author1.getPrivateKey());
+ body = TestUtils.getRandomString(42);
p = forumPostFactory
.createPseudonymousPost(forum0.getId(), time, null, author1,
- "text/plain", body, key);
+ body);
forumManager1.addLocalPost(p);
// sync forum post
diff --git a/briar-android-tests/src/test/java/org/briarproject/MessageSizeIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/MessageSizeIntegrationTest.java
index 5e5b877cc..45fb9d723 100644
--- a/briar-android-tests/src/test/java/org/briarproject/MessageSizeIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/MessageSizeIntegrationTest.java
@@ -6,8 +6,8 @@ import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.forum.ForumConstants;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.messaging.MessagingConstants;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
@@ -68,17 +68,17 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
String authorName = TestUtils.getRandomString(
MAX_AUTHOR_NAME_LENGTH);
byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH];
- Author author = authorFactory.createAuthor(authorName, authorPublic);
+ PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate();
+ LocalAuthor author = authorFactory
+ .createLocalAuthor(authorName, authorPublic,
+ privateKey.getEncoded());
// Create a maximum-length forum post
GroupId groupId = new GroupId(TestUtils.getRandomId());
long timestamp = Long.MAX_VALUE;
MessageId parent = new MessageId(TestUtils.getRandomId());
- String contentType = TestUtils.getRandomString(
- ForumConstants.MAX_CONTENT_TYPE_LENGTH);
- byte[] body = new byte[MAX_FORUM_POST_BODY_LENGTH];
- PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate();
+ String body = TestUtils.getRandomString(MAX_FORUM_POST_BODY_LENGTH);
ForumPost post = forumPostFactory.createPseudonymousPost(groupId,
- timestamp, parent, author, contentType, body, privateKey);
+ timestamp, parent, author, body);
// Check the size of the serialised message
int length = post.getMessage().getRaw().length;
assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH
diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 86c5239e5..1bb12bfff 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -100,6 +100,17 @@
/>
+
+
+
+
+
+
diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml
index b796da899..81c7e4a2b 100644
--- a/briar-android/res/layout/activity_forum.xml
+++ b/briar-android/res/layout/activity_forum.xml
@@ -7,12 +7,12 @@
android:orientation="vertical">
+ app:emptyText="@string/no_forum_posts"
+ app:scrollToEnd="false"/>
+ app:showAsAction="always"/>
-
+
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 883dceee6..b1652f5fb 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -75,6 +75,7 @@
Send
No data
…
+ The entered text is too long
It seems that you are new here and have no contacts yet.\n\nTap the + icon at the top and follow the instructions to add some friends to your list.\n\nPlease remember: You can only add new contacts face-to-face to prevent anyone from impersonating you or reading your messages in the future.
@@ -155,6 +156,14 @@
This group is dissolved
Remove
Add Private Group
+ This group is empty.\n\nYou can use the pen icon at the top to compose the first message.
+ Compose Message
+ Message sent
+ Message received
+ Member List
+ Invite Members
+ Leave Group
+ Dissolve Group
You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index d7cab786b..4e66c87ab 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -28,6 +28,7 @@ import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.android.panic.PanicPreferencesActivity;
import org.briarproject.android.panic.PanicResponderActivity;
+import org.briarproject.android.privategroup.conversation.GroupActivity;
import org.briarproject.android.privategroup.list.GroupListFragment;
import org.briarproject.android.sharing.ContactSelectorFragment;
import org.briarproject.android.sharing.InvitationsBlogActivity;
@@ -72,6 +73,8 @@ public interface ActivityComponent {
void inject(InvitationsBlogActivity activity);
+ void inject(GroupActivity activity);
+
void inject(CreateForumActivity activity);
void inject(ShareForumActivity activity);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 3a81fc60f..07cace566 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -21,6 +21,8 @@ import org.briarproject.android.controller.SetupController;
import org.briarproject.android.controller.SetupControllerImpl;
import org.briarproject.android.forum.ForumController;
import org.briarproject.android.forum.ForumControllerImpl;
+import org.briarproject.android.privategroup.conversation.GroupController;
+import org.briarproject.android.privategroup.conversation.GroupControllerImpl;
import org.briarproject.android.privategroup.list.GroupListController;
import org.briarproject.android.privategroup.list.GroupListControllerImpl;
@@ -99,6 +101,13 @@ public class ActivityModule {
return groupListController;
}
+ @ActivityScope
+ @Provides
+ protected GroupController provideGroupController(
+ GroupControllerImpl groupController) {
+ return groupController;
+ }
+
@ActivityScope
@Provides
protected ForumController provideForumController(
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index 9b7e6edba..ceb3cd9e0 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -19,7 +19,6 @@ import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.feed.FeedManager;
import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
@@ -37,6 +36,7 @@ import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.plugins.PluginManager;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.settings.SettingsManager;
+import org.briarproject.api.system.Clock;
import org.briarproject.plugins.AndroidPluginsModule;
import org.briarproject.system.AndroidSystemModule;
@@ -102,8 +102,6 @@ public interface AndroidComponent extends CoreEagerSingletons {
BlogSharingManager blogSharingManager();
- ForumPostFactory forumPostFactory();
-
BlogManager blogManager();
BlogPostFactory blogPostFactory();
@@ -124,6 +122,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
FeedManager feedManager();
+ Clock clock();
+
@IoExecutor
Executor ioExecutor();
diff --git a/briar-android/src/org/briarproject/android/BriarActivity.java b/briar-android/src/org/briarproject/android/BriarActivity.java
index 49dc574a8..4c01808af 100644
--- a/briar-android/src/org/briarproject/android/BriarActivity.java
+++ b/briar-android/src/org/briarproject/android/BriarActivity.java
@@ -26,6 +26,7 @@ public abstract class BriarActivity extends BaseActivity {
"briar.LOCAL_AUTHOR_HANDLE";
public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED";
public static final String GROUP_ID = "briar.GROUP_ID";
+ public static final String GROUP_NAME = "briar.GROUP_NAME";
public static final int REQUEST_PASSWORD = 1;
diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
index ff1644b1c..4c9024599 100644
--- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
@@ -31,7 +31,6 @@ import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
public class CreateForumActivity extends BriarActivity
@@ -150,7 +149,7 @@ public class CreateForumActivity extends BriarActivity
Intent i = new Intent(CreateForumActivity.this,
ForumActivity.class);
i.putExtra(GROUP_ID, f.getId().getBytes());
- i.putExtra(FORUM_NAME, f.getName());
+ i.putExtra(GROUP_NAME, f.getName());
startActivity(i);
Toast.makeText(CreateForumActivity.this,
R.string.forum_created_toast, LENGTH_LONG).show();
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 1cb6662fe..81b41f99f 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -3,155 +3,79 @@ package org.briarproject.android.forum;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.design.widget.Snackbar;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.StringRes;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
-import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.View;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
-import org.briarproject.android.BriarActivity;
-import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
-import org.briarproject.android.controller.handler.UiResultHandler;
-import org.briarproject.android.forum.ForumController.ForumPostListener;
-import org.briarproject.android.forum.NestedForumAdapter.OnNestedForumListener;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.sharing.SharingStatusForumActivity;
-import org.briarproject.android.view.BriarRecyclerView;
-import org.briarproject.android.view.TextInputView;
-import org.briarproject.android.view.TextInputView.TextInputListener;
+import org.briarproject.android.threaded.ThreadListActivity;
+import org.briarproject.android.threaded.ThreadListController;
import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.List;
import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.widget.Toast.LENGTH_SHORT;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
-public class ForumActivity extends BriarActivity implements
- ForumPostListener, TextInputListener, OnNestedForumListener {
-
- static final String FORUM_NAME = "briar.FORUM_NAME";
+public class ForumActivity extends
+ ThreadListActivity {
private static final int REQUEST_FORUM_SHARED = 3;
- private static final String KEY_INPUT_VISIBILITY = "inputVisibility";
- private static final String KEY_REPLY_ID = "replyId";
@Inject
- AndroidNotificationManager notificationManager;
-
- @Inject
- protected ForumController forumController;
-
- // Protected access for testing
- protected NestedForumAdapter forumAdapter;
-
- private BriarRecyclerView recyclerView;
- private TextInputView textInput;
-
- private volatile GroupId groupId = null;
-
- @Override
- public void onCreate(final Bundle state) {
- super.onCreate(state);
-
- setContentView(R.layout.activity_forum);
-
- Intent i = getIntent();
- byte[] b = i.getByteArrayExtra(GROUP_ID);
- if (b == null) throw new IllegalStateException();
- groupId = new GroupId(b);
- String forumName = i.getStringExtra(FORUM_NAME);
- if (forumName != null) setTitle(forumName);
-
- textInput = (TextInputView) findViewById(R.id.text_input_container);
- textInput.setVisibility(GONE);
- textInput.setListener(this);
- recyclerView =
- (BriarRecyclerView) findViewById(R.id.forum_discussion_list);
- LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(linearLayoutManager);
- forumAdapter = new NestedForumAdapter(this, this, linearLayoutManager);
- recyclerView.setAdapter(forumAdapter);
-
- forumController.loadForum(groupId,
- new UiResultExceptionHandler
, DbException>(
- this) {
- @Override
- public void onResultUi(List result) {
- Forum forum = forumController.getForum();
- if (forum != null) setTitle(forum.getName());
- List entries = new ArrayList<>(result);
- if (entries.isEmpty()) {
- recyclerView.showData();
- } else {
- forumAdapter.setEntries(entries);
- if (state != null) {
- byte[] replyId =
- state.getByteArray(KEY_REPLY_ID);
- if (replyId != null)
- forumAdapter.setReplyEntryById(replyId);
- }
- }
- }
-
- @Override
- public void onExceptionUi(DbException exception) {
- // TODO Improve UX ?
- finish();
- }
- });
- }
-
- @Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- textInput.setVisibility(
- savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
- VISIBLE : GONE);
- }
-
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean(KEY_INPUT_VISIBILITY,
- textInput.getVisibility() == VISIBLE);
- ForumEntry replyEntry = forumAdapter.getReplyEntry();
- if (replyEntry != null) {
- outState.putByteArray(KEY_REPLY_ID,
- replyEntry.getMessageId().getBytes());
- }
- }
+ ForumController forumController;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
- private void displaySnackbarShort(int stringId) {
- Snackbar snackbar =
- Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.show();
+ @Override
+ protected ThreadListController getController() {
+ return forumController;
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ Intent i = getIntent();
+ String groupName = i.getStringExtra(GROUP_NAME);
+ if (groupName != null) setTitle(groupName);
+ else loadNamedGroup();
+ }
+
+ @Override
+ protected void onNamedGroupLoaded(Forum forum) {
+ setTitle(forum.getName());
+ }
+
+ @Override
+ @LayoutRes
+ protected int getLayout() {
+ return R.layout.activity_forum;
+ }
+
+ @Override
+ protected NestedForumAdapter createAdapter(
+ LinearLayoutManager layoutManager) {
+ return new NestedForumAdapter(this, layoutManager);
}
@Override
@@ -172,34 +96,10 @@ public class ForumActivity extends BriarActivity implements
return super.onCreateOptionsMenu(menu);
}
- @Override
- public void onBackPressed() {
- if (textInput.getVisibility() == VISIBLE) {
- textInput.setVisibility(GONE);
- forumAdapter.setReplyEntry(null);
- } else {
- super.onBackPressed();
- }
- }
-
- private void showTextInput(@Nullable ForumEntry replyEntry) {
- // An animation here would be an overkill because of the keyboard
- // popping up.
- // only clear the text when the input container was not visible
- if (textInput.getVisibility() != VISIBLE) {
- textInput.setVisibility(VISIBLE);
- textInput.setText("");
- }
- textInput.showSoftKeyboard();
- textInput.setHint(replyEntry == null ? R.string.forum_new_message_hint :
- R.string.forum_message_reply_hint);
- forumAdapter.setReplyEntry(replyEntry);
- }
-
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
- ActivityOptionsCompat options = ActivityOptionsCompat
- .makeCustomAnimation(this, android.R.anim.slide_in_left,
+ ActivityOptionsCompat options =
+ makeCustomAnimation(this, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
// Handle presses on the action bar items
switch (item.getItemId()) {
@@ -229,69 +129,29 @@ public class ForumActivity extends BriarActivity implements
}
@Override
- public void onResume() {
- super.onResume();
- notificationManager.blockNotification(groupId);
- notificationManager.clearForumPostNotification(groupId);
- recyclerView.startPeriodicUpdate();
+ protected int getMaxBodyLength() {
+ return MAX_FORUM_POST_BODY_LENGTH;
}
@Override
- public void onPause() {
- super.onPause();
- notificationManager.unblockNotification(groupId);
- recyclerView.stopPeriodicUpdate();
+ @StringRes
+ protected int getItemPostedString() {
+ return R.string.forum_new_entry_posted;
}
@Override
- public void onSendClick(String text) {
- if (text.trim().length() == 0)
- return;
- ForumEntry replyEntry = forumAdapter.getReplyEntry();
- UiResultExceptionHandler resultHandler =
- new UiResultExceptionHandler(this) {
- @Override
- public void onResultUi(ForumEntry result) {
- onForumEntryAdded(result, true);
- }
-
- @Override
- public void onExceptionUi(DbException exception) {
- // TODO Improve UX ?
- finish();
- }
- };
-
- if (replyEntry == null) {
- // root post
- forumController.createPost(StringUtils.toUtf8(text), resultHandler);
- } else {
- forumController.createPost(StringUtils.toUtf8(text),
- replyEntry.getId(), resultHandler);
- }
- textInput.hideSoftKeyboard();
- textInput.setVisibility(GONE);
- forumAdapter.setReplyEntry(null);
+ @StringRes
+ protected int getItemReceivedString() {
+ return R.string.forum_new_entry_received;
}
private void showUnsubscribeDialog() {
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
- public void onClick(DialogInterface dialog, int which) {
- forumController.unsubscribe(
- new UiResultHandler(
- ForumActivity.this) {
- @Override
- public void onResultUi(Boolean result) {
- if (result) {
- Toast.makeText(ForumActivity.this,
- R.string.forum_left_toast,
- LENGTH_SHORT)
- .show();
- }
- }
- });
+ public void onClick(final DialogInterface dialog,
+ int which) {
+ deleteNamedGroup();
}
};
AlertDialog.Builder builder =
@@ -299,66 +159,30 @@ public class ForumActivity extends BriarActivity implements
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_leave_forum));
builder.setMessage(getString(R.string.dialog_message_leave_forum));
- builder.setPositiveButton(R.string.dialog_button_leave, okListener);
- builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setNegativeButton(R.string.dialog_button_leave, okListener);
+ builder.setPositiveButton(R.string.cancel, null);
builder.show();
}
- @Override
- public void onEntryVisible(ForumEntry forumEntry) {
- if (!forumEntry.isRead()) {
- forumEntry.setRead(true);
- forumController.entryRead(forumEntry);
- }
- }
-
- @Override
- public void onReplyClick(ForumEntry forumEntry) {
- showTextInput(forumEntry);
- }
-
- private void onForumEntryAdded(final ForumEntry entry, boolean isLocal) {
- forumAdapter.addEntry(entry);
- if (isLocal && forumAdapter.isVisible(entry)) {
- displaySnackbarShort(R.string.forum_new_entry_posted);
- } else {
- Snackbar snackbar = Snackbar.make(recyclerView,
- isLocal ? R.string.forum_new_entry_posted :
- R.string.forum_new_entry_received,
- Snackbar.LENGTH_LONG);
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.setActionTextColor(ContextCompat
- .getColor(ForumActivity.this,
- R.color.briar_button_positive));
- snackbar.setAction(R.string.show, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- forumAdapter.scrollToEntry(entry);
- }
- });
- snackbar.getView().setBackgroundResource(R.color.briar_primary);
- snackbar.show();
- }
- }
-
- @Override
- public void onForumPostReceived(ForumPostHeader header) {
- forumController.loadPost(header,
- new UiResultExceptionHandler(this) {
+ private void deleteNamedGroup() {
+ forumController.deleteNamedGroup(
+ new UiResultExceptionHandler(
+ ForumActivity.this) {
@Override
- public void onResultUi(final ForumEntry result) {
- onForumEntryAdded(result, false);
+ public void onResultUi(Void v) {
+ Toast.makeText(ForumActivity.this,
+ R.string.forum_left_toast,
+ LENGTH_SHORT)
+ .show();
}
@Override
- public void onExceptionUi(DbException exception) {
- // TODO add proper exception handling
+ public void onExceptionUi(
+ DbException exception) {
+ // TODO proper error handling
+ finish();
}
});
}
- @Override
- public void onForumRemoved() {
- finish();
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java
index ad08dbc4d..9b1fba564 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumController.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumController.java
@@ -1,51 +1,10 @@
package org.briarproject.android.forum;
-import android.support.annotation.Nullable;
-import android.support.annotation.UiThread;
-
-import org.briarproject.android.DestroyableContext;
-import org.briarproject.android.controller.ActivityLifecycleController;
-import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.android.controller.handler.ResultHandler;
-import org.briarproject.api.db.DbException;
+import org.briarproject.android.threaded.ThreadListController;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-import java.util.Collection;
-import java.util.List;
-
-public interface ForumController extends ActivityLifecycleController {
-
- void loadForum(GroupId groupId,
- ResultExceptionHandler, DbException> resultHandler);
-
- @Nullable
- Forum getForum();
-
- void loadPost(ForumPostHeader header,
- ResultExceptionHandler resultHandler);
-
- void unsubscribe(ResultHandler resultHandler);
-
- void entryRead(ForumEntry forumEntry);
-
- void entriesRead(Collection messageIds);
-
- void createPost(byte[] body,
- ResultExceptionHandler resultHandler);
-
- void createPost(byte[] body, MessageId parentId,
- ResultExceptionHandler resultHandler);
-
- interface ForumPostListener extends DestroyableContext {
-
- @UiThread
- void onForumPostReceived(ForumPostHeader header);
-
- @UiThread
- void onForumRemoved();
- }
+public interface ForumController
+ extends ThreadListController {
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
index b6a8ddce5..fa62f1b29 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -1,388 +1,126 @@
package org.briarproject.android.forum;
-import android.app.Activity;
import android.support.annotation.Nullable;
-import org.briarproject.android.controller.DbControllerImpl;
-import org.briarproject.android.controller.handler.ResultExceptionHandler;
-import org.briarproject.android.controller.handler.ResultHandler;
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.threaded.ThreadListControllerImpl;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.crypto.CryptoExecutor;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.ForumPostReceivedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPost;
-import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
-import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
import org.briarproject.util.StringUtils;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import javax.inject.Inject;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.identity.Author.Status.OURSELVES;
-
-public class ForumControllerImpl extends DbControllerImpl
- implements ForumController, EventListener {
+public class ForumControllerImpl
+ extends ThreadListControllerImpl
+ implements ForumController {
private static final Logger LOG =
Logger.getLogger(ForumControllerImpl.class.getName());
- private final Executor cryptoExecutor;
- private final ForumPostFactory forumPostFactory;
- private final CryptoComponent crypto;
private final ForumManager forumManager;
- private final EventBus eventBus;
- private final IdentityManager identityManager;
-
- private final Map bodyCache = new ConcurrentHashMap<>();
- private final AtomicLong newestTimeStamp = new AtomicLong();
-
- private volatile LocalAuthor localAuthor = null;
- private volatile Forum forum = null;
- private volatile ForumPostListener listener;
@Inject
ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
- LifecycleManager lifecycleManager,
+ LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor,
- ForumPostFactory forumPostFactory, CryptoComponent crypto,
ForumManager forumManager, EventBus eventBus,
- IdentityManager identityManager) {
- super(dbExecutor, lifecycleManager);
- this.cryptoExecutor = cryptoExecutor;
- this.forumPostFactory = forumPostFactory;
- this.crypto = crypto;
+ AndroidNotificationManager notificationManager, Clock clock) {
+ super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
+ eventBus, notificationManager, clock);
this.forumManager = forumManager;
- this.eventBus = eventBus;
- this.identityManager = identityManager;
- }
-
- @Override
- public void onActivityCreate(Activity activity) {
- if (activity instanceof ForumPostListener) {
- listener = (ForumPostListener) activity;
- } else {
- throw new IllegalStateException(
- "An activity that injects the ForumController must " +
- "implement the ForumPostListener");
- }
}
@Override
public void onActivityResume() {
- eventBus.addListener(this);
- }
-
- @Override
- public void onActivityPause() {
- eventBus.removeListener(this);
- }
-
- @Override
- public void onActivityDestroy() {
+ super.onActivityResume();
+ notificationManager.clearForumPostNotification(getGroupId());
}
@Override
public void eventOccurred(Event e) {
- if (forum == null) return;
+ super.eventOccurred(e);
+
if (e instanceof ForumPostReceivedEvent) {
final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
- if (pe.getGroupId().equals(forum.getId())) {
+ if (pe.getGroupId().equals(getGroupId())) {
LOG.info("Forum post received, adding...");
final ForumPostHeader fph = pe.getForumPostHeader();
- updateNewestTimestamp(fph.getTimestamp());
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
- listener.onForumPostReceived(fph);
- }
- });
- }
- } else if (e instanceof GroupRemovedEvent) {
- GroupRemovedEvent s = (GroupRemovedEvent) e;
- if (s.getGroup().getId().equals(forum.getId())) {
- LOG.info("Forum removed");
- listener.runOnUiThreadUnlessDestroyed(new Runnable() {
- @Override
- public void run() {
- listener.onForumRemoved();
+ listener.onHeaderReceived(fph);
}
});
}
}
}
- /**
- * This should only be run from the DbThread.
- *
- * @throws DbException
- */
- private void loadForum(GroupId groupId) throws DbException {
- // Get Forum
- long now = System.currentTimeMillis();
- forum = forumManager.getForum(groupId);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading forum took " + duration + " ms");
-
- // Get First Identity
- now = System.currentTimeMillis();
- localAuthor = identityManager.getLocalAuthor();
- duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading author took " + duration + " ms");
+ @Override
+ protected Forum loadNamedGroup() throws DbException {
+ return forumManager.getForum(getGroupId());
}
- /**
- * This should only be run from the DbThread.
- *
- * @throws DbException
- */
- private Collection loadHeaders() throws DbException {
- if (forum == null)
- throw new RuntimeException("Forum has not been initialized");
-
- // Get Headers
- long now = System.currentTimeMillis();
- Collection headers =
- forumManager.getPostHeaders(forum.getId());
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading headers took " + duration + " ms");
- return headers;
+ @Override
+ protected Collection loadHeaders() throws DbException {
+ return forumManager.getPostHeaders(getGroupId());
}
- /**
- * This should only be run from the DbThread.
- *
- * @throws DbException
- */
- private void loadBodies(Collection headers)
+ @Override
+ protected String loadMessageBody(MessageId id) throws DbException {
+ return StringUtils.fromUtf8(forumManager.getPostBody(id));
+ }
+
+ @Override
+ protected void markRead(MessageId id) throws DbException {
+ forumManager.setReadFlag(getGroupId(), id, true);
+ }
+
+ @Override
+ protected long getLatestTimestamp() throws DbException {
+ GroupCount count = forumManager.getGroupCount(getGroupId());
+ return count.getLatestMsgTime();
+ }
+
+ @Override
+ protected ForumPost createLocalMessage(String body, long timestamp,
+ @Nullable MessageId parentId, LocalAuthor author) {
+ return forumManager
+ .createLocalPost(getGroupId(), body, timestamp, parentId,
+ author);
+ }
+
+ @Override
+ protected ForumPostHeader addLocalMessage(ForumPost p)
throws DbException {
- // Get Bodies
- long now = System.currentTimeMillis();
- for (ForumPostHeader header : headers) {
- if (!bodyCache.containsKey(header.getId())) {
- byte[] body = forumManager.getPostBody(header.getId());
- bodyCache.put(header.getId(), body);
- }
- }
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading bodies took " + duration + " ms");
- }
-
- private List buildForumEntries(
- Collection headers) {
- List entries = new ArrayList<>();
- for (ForumPostHeader h : headers) {
- byte[] body = bodyCache.get(h.getId());
- entries.add(new ForumEntry(h, StringUtils.fromUtf8(body)));
- }
- return entries;
- }
-
- private void updateNewestTimeStamp(Collection headers) {
- for (ForumPostHeader h : headers) {
- updateNewestTimestamp(h.getTimestamp());
- }
+ return forumManager.addLocalPost(p);
}
@Override
- public void loadForum(final GroupId groupId,
- final ResultExceptionHandler, DbException> resultHandler) {
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- LOG.info("Loading forum...");
- try {
- if (forum == null) {
- loadForum(groupId);
- }
- // Get Forum Posts and Bodies
- Collection headers = loadHeaders();
- updateNewestTimeStamp(headers);
- loadBodies(headers);
- resultHandler.onResult(buildForumEntries(headers));
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- resultHandler.onException(e);
- }
- }
- });
+ protected void deleteNamedGroup(Forum forum) throws DbException {
+ forumManager.removeForum(forum);
}
@Override
- @Nullable
- public Forum getForum() {
- return forum;
+ protected ForumItem buildItem(ForumPostHeader header, String body) {
+ return new ForumItem(header, body);
}
- @Override
- public void loadPost(final ForumPostHeader header,
- final ResultExceptionHandler resultHandler) {
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- LOG.info("Loading post...");
- try {
- loadBodies(Collections.singletonList(header));
- resultHandler.onResult(new ForumEntry(header, StringUtils
- .fromUtf8(bodyCache.get(header.getId()))));
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- resultHandler.onException(e);
- }
- }
- });
- }
-
- @Override
- public void unsubscribe(final ResultHandler resultHandler) {
- if (forum == null) return;
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- try {
- long now = System.currentTimeMillis();
- forumManager.removeForum(forum);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Removing forum took " + duration + " ms");
- resultHandler.onResult(true);
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- resultHandler.onResult(false);
- }
- }
- });
- }
-
- @Override
- public void entryRead(ForumEntry forumEntry) {
- entriesRead(Collections.singletonList(forumEntry));
- }
-
- @Override
- public void entriesRead(final Collection forumEntries) {
- if (forum == null) return;
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- try {
- long now = System.currentTimeMillis();
- for (ForumEntry fe : forumEntries) {
- forumManager
- .setReadFlag(forum.getId(), fe.getId(), true);
- }
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Marking read took " + duration + " ms");
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- @Override
- public void createPost(byte[] body,
- ResultExceptionHandler resultHandler) {
- createPost(body, null, resultHandler);
- }
-
- @Override
- public void createPost(final byte[] body, final MessageId parentId,
- final ResultExceptionHandler resultHandler) {
- cryptoExecutor.execute(new Runnable() {
- @Override
- public void run() {
- LOG.info("Create post...");
- long timestamp = System.currentTimeMillis();
- timestamp = Math.max(timestamp, newestTimeStamp.get());
- ForumPost p;
- try {
- KeyParser keyParser = crypto.getSignatureKeyParser();
- byte[] b = localAuthor.getPrivateKey();
- PrivateKey authorKey = keyParser.parsePrivateKey(b);
- p = forumPostFactory.createPseudonymousPost(
- forum.getId(), timestamp, parentId, localAuthor,
- "text/plain", body, authorKey);
- } catch (GeneralSecurityException | FormatException e) {
- throw new RuntimeException(e);
- }
- bodyCache.put(p.getMessage().getId(), body);
- storePost(p, resultHandler);
- }
- });
- }
-
- private void storePost(final ForumPost p,
- final ResultExceptionHandler resultHandler) {
- runOnDbThread(new Runnable() {
- @Override
- public void run() {
- try {
- LOG.info("Store post...");
- long now = System.currentTimeMillis();
- forumManager.addLocalPost(p);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Storing message took " + duration + " ms");
-
- ForumPostHeader h =
- new ForumPostHeader(p.getMessage().getId(),
- p.getParent(),
- p.getMessage().getTimestamp(),
- p.getAuthor(), OURSELVES, true);
-
- resultHandler.onResult(new ForumEntry(h, StringUtils
- .fromUtf8(bodyCache.get(p.getMessage().getId()))));
-
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- resultHandler.onException(e);
- }
- }
- });
- }
-
- private void updateNewestTimestamp(long update) {
- long newest = newestTimeStamp.get();
- while (newest < update) {
- if (newestTimeStamp.compareAndSet(newest, update)) return;
- newest = newestTimeStamp.get();
- }
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumItem.java b/briar-android/src/org/briarproject/android/forum/ForumItem.java
new file mode 100644
index 000000000..80f28e2d3
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumItem.java
@@ -0,0 +1,24 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.threaded.ThreadItem;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.sync.MessageId;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+public class ForumItem extends ThreadItem {
+
+ ForumItem(ForumPostHeader h, String body) {
+ super(h.getId(), h.getParentId(), body, h.getTimestamp(), h.getAuthor(),
+ h.getAuthorStatus(), h.isRead());
+ }
+
+ public ForumItem(MessageId messageId, MessageId parentId, String text,
+ long timestamp, Author author, Status status) {
+ super(messageId, parentId, text, timestamp, author, status, true);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index ca237eb35..040b81662 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -2,7 +2,6 @@ package org.briarproject.android.forum;
import android.content.Context;
import android.content.Intent;
-import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@@ -21,7 +20,7 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.android.BriarActivity.GROUP_ID;
-import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
+import static org.briarproject.android.BriarActivity.GROUP_NAME;
class ForumListAdapter
extends BriarAdapter {
@@ -84,7 +83,7 @@ class ForumListAdapter
Intent i = new Intent(ctx, ForumActivity.class);
Forum f = item.getForum();
i.putExtra(GROUP_ID, f.getId().getBytes());
- i.putExtra(FORUM_NAME, f.getName());
+ i.putExtra(GROUP_NAME, f.getName());
ctx.startActivity(i);
}
});
@@ -115,17 +114,6 @@ class ForumListAdapter
return a.getForum().equals(b.getForum());
}
- @Nullable
- public ForumListItem findItem(GroupId g) {
- for (int i = 0; i < items.size(); i++) {
- ForumListItem item = items.get(i);
- if (item.getForum().getGroup().getId().equals(g)) {
- return item;
- }
- }
- return null;
- }
-
int findItemPosition(GroupId g) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
index f1e2f5de2..f690df90a 100644
--- a/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/NestedForumAdapter.java
@@ -1,289 +1,20 @@
package org.briarproject.android.forum;
-import android.animation.Animator;
-import android.animation.ArgbEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.drawable.ColorDrawable;
-import android.support.annotation.Nullable;
-import android.support.v4.content.ContextCompat;
+import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.TextView;
import org.briarproject.R;
-import org.briarproject.android.util.NestedTreeList;
-import org.briarproject.android.view.AuthorView;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
+import org.briarproject.android.threaded.ThreadItemAdapter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+@UiThread
+public class NestedForumAdapter extends ThreadItemAdapter {
-import static android.support.v7.widget.RecyclerView.NO_POSITION;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-public class NestedForumAdapter
- extends RecyclerView.Adapter {
-
- private static final int UNDEFINED = -1;
-
- private final NestedTreeList forumEntries =
- new NestedTreeList<>();
- private final Map animatingEntries =
- new HashMap<>();
- // highlight not dependant on time
- private ForumEntry replyEntry;
- // temporary highlight
- private ForumEntry addedEntry;
- private final Context ctx;
- private final OnNestedForumListener listener;
- private final LinearLayoutManager layoutManager;
-
- public NestedForumAdapter(Context ctx, OnNestedForumListener listener,
+ public NestedForumAdapter(ThreadItemListener listener,
LinearLayoutManager layoutManager) {
- this.ctx = ctx;
- this.listener = listener;
- this.layoutManager = layoutManager;
- }
-
- ForumEntry getReplyEntry() {
- return replyEntry;
- }
-
- void setEntries(List entries) {
- forumEntries.clear();
- forumEntries.addAll(entries);
- notifyItemRangeInserted(0, entries.size());
- }
-
- void addEntry(ForumEntry entry) {
- forumEntries.add(entry);
- addedEntry = entry;
- if (entry.getParentId() == null) {
- notifyItemInserted(getVisiblePos(entry));
- } else {
- // Try to find the entry's parent and perform the proper ui update if
- // it's present and visible.
- for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
- ForumEntry higherEntry = forumEntries.get(i);
- if (higherEntry.getLevel() < entry.getLevel()) {
- // parent found
- if (higherEntry.isShowingDescendants()) {
- int parentVisiblePos = getVisiblePos(higherEntry);
- if (parentVisiblePos != NO_POSITION) {
- // parent is visible, we need to update its ui
- notifyItemChanged(parentVisiblePos);
- // new entry insert ui
- int visiblePos = getVisiblePos(entry);
- notifyItemInserted(visiblePos);
- break;
- }
- } else {
- // do not show the new entry if its parent is not showing
- // descendants (this can be overridden by the user by
- // pressing the snack bar)
- break;
- }
- }
- }
- }
- }
-
- void scrollToEntry(ForumEntry entry) {
- int visiblePos = getVisiblePos(entry);
- if (visiblePos == NO_POSITION && entry.getParentId() != null) {
- // The entry is not visible due to being hidden by its parent entry.
- // Find the parent and make it visible and traverse up the parent
- // chain if necessary to make the entry visible
- MessageId parentId = entry.getParentId();
- for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
- ForumEntry higherEntry = forumEntries.get(i);
- if (higherEntry.getId().equals(parentId)) {
- // parent found
- showDescendants(higherEntry);
- int parentPos = getVisiblePos(higherEntry);
- if (parentPos != NO_POSITION) {
- // parent or ancestor is visible, entry's visibility
- // is ensured
- notifyItemChanged(parentPos);
- visiblePos = parentPos;
- break;
- }
- // parent or ancestor is hidden, we need to continue up the
- // dependency chain
- parentId = higherEntry.getParentId();
- }
- }
- }
- if (visiblePos != NO_POSITION)
- layoutManager.scrollToPositionWithOffset(visiblePos, 0);
- }
-
- private int getReplyCount(ForumEntry entry) {
- int counter = 0;
- int pos = forumEntries.indexOf(entry);
- if (pos >= 0) {
- int ancestorLvl = entry.getLevel();
- for (int i = pos + 1; i < forumEntries.size(); i++) {
- int descendantLvl = forumEntries.get(i).getLevel();
- if (descendantLvl <= ancestorLvl)
- break;
- if (descendantLvl == ancestorLvl + 1)
- counter++;
- }
- }
- return counter;
- }
-
- void setReplyEntryById(byte[] id) {
- MessageId messageId = new MessageId(id);
- for (ForumEntry entry : forumEntries) {
- if (entry.getId().equals(messageId)) {
- setReplyEntry(entry);
- break;
- }
- }
- }
-
- void setReplyEntry(@Nullable ForumEntry entry) {
- if (replyEntry != null) {
- notifyItemChanged(getVisiblePos(replyEntry));
- }
- replyEntry = entry;
- if (replyEntry != null) {
- notifyItemChanged(getVisiblePos(replyEntry));
- }
- }
-
- private List getSubTreeIndexes(int pos, int levelLimit) {
- List indexList = new ArrayList<>();
-
- for (int i = pos + 1; i < getItemCount(); i++) {
- ForumEntry entry = getVisibleEntry(i);
- if (entry != null && entry.getLevel() > levelLimit) {
- indexList.add(i);
- } else {
- break;
- }
- }
- return indexList;
- }
-
- void showDescendants(ForumEntry forumEntry) {
- forumEntry.setShowingDescendants(true);
- int visiblePos = getVisiblePos(forumEntry);
- List indexList =
- getSubTreeIndexes(visiblePos, forumEntry.getLevel());
- if (!indexList.isEmpty()) {
- if (indexList.size() == 1) {
- notifyItemInserted(indexList.get(0));
- } else {
- notifyItemRangeInserted(indexList.get(0),
- indexList.size());
- }
- }
- }
-
- void hideDescendants(ForumEntry forumEntry) {
- int visiblePos = getVisiblePos(forumEntry);
- List indexList =
- getSubTreeIndexes(visiblePos, forumEntry.getLevel());
- if (!indexList.isEmpty()) {
- // stop animating children
- for (int index : indexList) {
- ValueAnimator anim =
- animatingEntries.get(forumEntries.get(index));
- if (anim != null && anim.isRunning()) {
- anim.cancel();
- }
- }
- if (indexList.size() == 1) {
- notifyItemRemoved(indexList.get(0));
- } else {
- notifyItemRangeRemoved(indexList.get(0),
- indexList.size());
- }
- }
- forumEntry.setShowingDescendants(false);
- }
-
-
- /**
- *
- * @param position is visible entry index
- * @return the visible entry at index position from an ordered list of visible
- * entries, or null if position is larger then the number of visible entries.
- */
- @Nullable
- ForumEntry getVisibleEntry(int position) {
- int levelLimit = UNDEFINED;
- for (ForumEntry forumEntry : forumEntries) {
- if (levelLimit >= 0) {
- // skip hidden entries that their parent is hiding
- if (forumEntry.getLevel() > levelLimit) {
- continue;
- }
- levelLimit = UNDEFINED;
- }
- if (!forumEntry.isShowingDescendants()) {
- levelLimit = forumEntry.getLevel();
- }
- if (position-- == 0) {
- return forumEntry;
- }
- }
- return null;
- }
-
- private void animateFadeOut(final NestedForumHolder ui,
- final ForumEntry addedEntry) {
- ui.setIsRecyclable(false);
- ValueAnimator anim = new ValueAnimator();
- animatingEntries.put(addedEntry, anim);
- ColorDrawable viewColor = (ColorDrawable) ui.cell.getBackground();
- anim.setIntValues(viewColor.getColor(), ContextCompat
- .getColor(ctx, R.color.window_background));
- anim.setEvaluator(new ArgbEvaluator());
- anim.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
-
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- ui.setIsRecyclable(true);
- animatingEntries.remove(addedEntry);
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- ui.setIsRecyclable(true);
- animatingEntries.remove(addedEntry);
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
-
- }
- });
- anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- ui.cell.setBackgroundColor(
- (Integer) valueAnimator.getAnimatedValue());
- }
- });
- anim.setDuration(5000);
- anim.start();
+ super(listener, layoutManager);
}
@Override
@@ -294,159 +25,4 @@ public class NestedForumAdapter
return new NestedForumHolder(v);
}
- @Override
- public void onBindViewHolder(
- final NestedForumHolder ui, final int position) {
- final ForumEntry entry = getVisibleEntry(position);
- if (entry == null) return;
- listener.onEntryVisible(entry);
-
- ui.textView.setText(StringUtils.trim(entry.getText()));
-
- if (position == 0) {
- ui.topDivider.setVisibility(View.INVISIBLE);
- } else {
- ui.topDivider.setVisibility(View.VISIBLE);
- }
-
- for (int i = 0; i < ui.lvls.length; i++) {
- ui.lvls[i].setVisibility(i < entry.getLevel() ? VISIBLE : GONE);
- }
- if (entry.getLevel() > 5) {
- ui.lvlText.setVisibility(VISIBLE);
- ui.lvlText.setText("" + entry.getLevel());
- } else {
- ui.lvlText.setVisibility(GONE);
- }
- ui.author.setAuthor(entry.getAuthor());
- ui.author.setDate(entry.getTimestamp());
- ui.author.setAuthorStatus(entry.getStatus());
-
- int replies = getReplyCount(entry);
- if (replies == 0) {
- ui.repliesText.setText("");
- } else {
- ui.repliesText.setText(
- ctx.getResources()
- .getQuantityString(R.plurals.message_replies,
- replies, replies));
- }
-
- if (entry.hasDescendants()) {
- ui.chevron.setVisibility(VISIBLE);
- if (entry.isShowingDescendants()) {
- ui.chevron.setSelected(false);
- } else {
- ui.chevron.setSelected(true);
- }
- ui.chevron.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- ui.chevron.setSelected(!ui.chevron.isSelected());
- if (ui.chevron.isSelected()) {
- hideDescendants(entry);
- } else {
- showDescendants(entry);
- }
- }
- });
- } else {
- ui.chevron.setVisibility(INVISIBLE);
- }
- if (entry.equals(replyEntry)) {
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.forum_cell_highlight));
- } else if (entry.equals(addedEntry)) {
-
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.forum_cell_highlight));
- animateFadeOut(ui, addedEntry);
- addedEntry = null;
- } else {
- ui.cell.setBackgroundColor(ContextCompat
- .getColor(ctx, R.color.window_background));
- }
- ui.replyButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- listener.onReplyClick(entry);
- scrollToEntry(entry);
- }
- });
- }
-
- public boolean isVisible(ForumEntry entry) {
- return getVisiblePos(entry) != NO_POSITION;
- }
-
- /**
- * @param sEntry the ForumEntry to find the visible positoin of, or null to
- * return the total cound of visible elements
- * @return the visible position of sEntry, or the total number of visible
- * elements if sEntry is null. If sEntry is not visible a NO_POSITION is
- * returned.
- */
- private int getVisiblePos(@Nullable ForumEntry sEntry) {
- int visibleCounter = 0;
- int levelLimit = UNDEFINED;
- for (ForumEntry fEntry : forumEntries) {
- if (levelLimit >= 0) {
- if (fEntry.getLevel() > levelLimit) {
- // skip all the entries below a non visible branch
- continue;
- }
- levelLimit = UNDEFINED;
- }
- if (sEntry != null && sEntry.equals(fEntry)) {
- return visibleCounter;
- } else if (!fEntry.isShowingDescendants()) {
- levelLimit = fEntry.getLevel();
- }
- visibleCounter++;
- }
- return sEntry == null ? visibleCounter : NO_POSITION;
- }
-
- @Override
- public int getItemCount() {
- return getVisiblePos(null);
- }
-
- static class NestedForumHolder extends RecyclerView.ViewHolder {
-
- private final TextView textView, lvlText, repliesText;
- private final AuthorView author;
- private final View[] lvls;
- private final View chevron, replyButton;
- private final ViewGroup cell;
- private final View topDivider;
-
- private NestedForumHolder(View v) {
- super(v);
-
- textView = (TextView) v.findViewById(R.id.text);
- lvlText = (TextView) v.findViewById(R.id.nested_line_text);
- author = (AuthorView) v.findViewById(R.id.author);
- repliesText = (TextView) v.findViewById(R.id.replies);
- int[] nestedLineIds = {
- R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
- R.id.nested_line_4, R.id.nested_line_5
- };
- lvls = new View[nestedLineIds.length];
- for (int i = 0; i < lvls.length; i++) {
- lvls[i] = v.findViewById(nestedLineIds[i]);
- }
- chevron = v.findViewById(R.id.chevron);
- replyButton = v.findViewById(R.id.btn_reply);
- cell = (ViewGroup) v.findViewById(R.id.forum_cell);
- topDivider = v.findViewById(R.id.top_divider);
- }
-
- }
-
- interface OnNestedForumListener {
- void onEntryVisible(ForumEntry forumEntry);
-
- void onReplyClick(ForumEntry forumEntry);
- }
}
diff --git a/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
new file mode 100644
index 000000000..b73558ff5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/NestedForumHolder.java
@@ -0,0 +1,13 @@
+package org.briarproject.android.forum;
+
+import android.view.View;
+
+import org.briarproject.android.threaded.ThreadItemViewHolder;
+
+public class NestedForumHolder extends ThreadItemViewHolder {
+
+ public NestedForumHolder(View v) {
+ super(v);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
new file mode 100644
index 000000000..71a4865b5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java
@@ -0,0 +1,112 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.StringRes;
+import android.support.v7.app.ActionBar;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.threaded.ThreadListActivity;
+import org.briarproject.android.threaded.ThreadListController;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.PrivateGroup;
+
+import javax.inject.Inject;
+
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
+
+public class GroupActivity extends
+ ThreadListActivity {
+
+ @Inject
+ GroupController controller;
+
+ @Override
+ public void injectActivity(ActivityComponent component) {
+ component.inject(this);
+ }
+
+ @Override
+ protected ThreadListController getController() {
+ return controller;
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ Intent i = getIntent();
+ String groupName = i.getStringExtra(GROUP_NAME);
+ if (groupName != null) setTitle(groupName);
+ loadNamedGroup();
+
+ list.setEmptyText(R.string.groups_no_messages);
+ }
+
+ @Override
+ protected void onNamedGroupLoaded(PrivateGroup group) {
+ setTitle(group.getName());
+ // Created by
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setSubtitle(getString(R.string.groups_created_by,
+ group.getAuthor().getName()));
+ }
+ }
+
+ @Override
+ @LayoutRes
+ protected int getLayout() {
+ return R.layout.activity_forum;
+ }
+
+ @Override
+ protected GroupMessageAdapter createAdapter(
+ LinearLayoutManager layoutManager) {
+ return new GroupMessageAdapter(this, layoutManager);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu items for use in the action bar
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.group_actions, menu);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_group_compose_message:
+ showTextInput(null);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected int getMaxBodyLength() {
+ return MAX_GROUP_POST_BODY_LENGTH;
+ }
+
+ @Override
+ @StringRes
+ protected int getItemPostedString() {
+ return R.string.groups_message_sent;
+ }
+
+ @Override
+ @StringRes
+ protected int getItemReceivedString() {
+ return R.string.groups_message_received;
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupController.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupController.java
new file mode 100644
index 000000000..0292f2278
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupController.java
@@ -0,0 +1,11 @@
+package org.briarproject.android.privategroup.conversation;
+
+import org.briarproject.android.threaded.ThreadListController;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.PrivateGroup;
+
+public interface GroupController
+ extends
+ ThreadListController {
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
new file mode 100644
index 000000000..fb0cd8ac5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java
@@ -0,0 +1,126 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.support.annotation.Nullable;
+
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.threaded.ThreadListControllerImpl;
+import org.briarproject.api.clients.MessageTracker.GroupCount;
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.GroupMessageAddedEvent;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.privategroup.GroupMessage;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupManager;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+public class GroupControllerImpl
+ extends ThreadListControllerImpl
+ implements GroupController {
+
+ private static final Logger LOG =
+ Logger.getLogger(GroupControllerImpl.class.getName());
+
+ private final PrivateGroupManager privateGroupManager;
+
+ @Inject
+ GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
+ LifecycleManager lifecycleManager, IdentityManager identityManager,
+ @CryptoExecutor Executor cryptoExecutor,
+ PrivateGroupManager privateGroupManager, EventBus eventBus,
+ AndroidNotificationManager notificationManager, Clock clock) {
+ super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
+ eventBus, notificationManager, clock);
+ this.privateGroupManager = privateGroupManager;
+ }
+
+ @Override
+ public void onActivityResume() {
+ super.onActivityResume();
+ // TODO: Add new notification manager methods for private groups
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ super.eventOccurred(e);
+
+ if (e instanceof GroupMessageAddedEvent) {
+ GroupMessageAddedEvent gmae = (GroupMessageAddedEvent) e;
+ if (!gmae.isLocal() && gmae.getGroupId().equals(getGroupId())) {
+ LOG.info("Group message received, adding...");
+ final GroupMessageHeader h = gmae.getHeader();
+ listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+ @Override
+ public void run() {
+ listener.onHeaderReceived(h);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ protected PrivateGroup loadNamedGroup() throws DbException {
+ return privateGroupManager.getPrivateGroup(getGroupId());
+ }
+
+ @Override
+ protected Collection loadHeaders() throws DbException {
+ return privateGroupManager.getHeaders(getGroupId());
+ }
+
+ @Override
+ protected String loadMessageBody(MessageId id) throws DbException {
+ return privateGroupManager.getMessageBody(id);
+ }
+
+ @Override
+ protected void markRead(MessageId id) throws DbException {
+ privateGroupManager.setReadFlag(getGroupId(), id, true);
+ }
+
+ @Override
+ protected long getLatestTimestamp() throws DbException {
+ GroupCount count = privateGroupManager.getGroupCount(getGroupId());
+ return count.getLatestMsgTime();
+ }
+
+ @Override
+ protected GroupMessage createLocalMessage(String body, long timestamp,
+ @Nullable MessageId parentId, LocalAuthor author) {
+ return privateGroupManager
+ .createLocalMessage(getGroupId(), body, timestamp, parentId,
+ author);
+ }
+
+ @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 body) {
+ return new GroupMessageItem(header, body);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
new file mode 100644
index 000000000..19ee14adc
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageAdapter.java
@@ -0,0 +1,28 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.support.annotation.UiThread;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.ThreadItemAdapter;
+
+@UiThread
+public class GroupMessageAdapter extends ThreadItemAdapter {
+
+ public GroupMessageAdapter(ThreadItemListener listener,
+ LinearLayoutManager layoutManager) {
+ super(listener, layoutManager);
+ }
+
+ @Override
+ public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.list_item_forum_post, parent, false);
+ return new GroupMessageViewHolder(v);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
new file mode 100644
index 000000000..ee84c1fb0
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageItem.java
@@ -0,0 +1,22 @@
+package org.briarproject.android.privategroup.conversation;
+
+import org.briarproject.android.threaded.ThreadItem;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.Author.Status;
+import org.briarproject.api.privategroup.GroupMessageHeader;
+import org.briarproject.api.sync.MessageId;
+
+class GroupMessageItem extends ThreadItem {
+
+ public GroupMessageItem(MessageId messageId, MessageId parentId,
+ String text, long timestamp, Author author, Status status,
+ boolean isRead) {
+ super(messageId, parentId, text, timestamp, author, status, isRead);
+ }
+
+ public GroupMessageItem(GroupMessageHeader h, String text) {
+ this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
+ h.getAuthorStatus(), h.isRead());
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
new file mode 100644
index 000000000..11825b805
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupMessageViewHolder.java
@@ -0,0 +1,14 @@
+package org.briarproject.android.privategroup.conversation;
+
+import android.view.View;
+
+import org.briarproject.android.threaded.ThreadItemViewHolder;
+
+public class GroupMessageViewHolder
+ extends ThreadItemViewHolder {
+
+ public GroupMessageViewHolder(View v) {
+ super(v);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
index 20210a486..bae48022a 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListAdapter.java
@@ -1,7 +1,6 @@
package org.briarproject.android.privategroup.list;
import android.content.Context;
-import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -32,7 +31,7 @@ class GroupListAdapter extends BriarAdapter {
@Override
public void onBindViewHolder(GroupViewHolder ui, int position) {
- ui.bindView(ctx, getItemAt(position), listener);
+ ui.bindView(ctx, items.get(position), listener);
}
@Override
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
index 9ad7536eb..9db84cdca 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java
@@ -68,6 +68,7 @@ public class GroupListControllerImpl extends DbControllerImpl
throw new IllegalStateException(
"GroupListListener needs to be attached");
eventBus.addListener(this);
+ // TODO: Add new notification manager methods for private groups
}
@Override
diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
index 12b956df4..c7d59dd1f 100644
--- a/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
+++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupViewHolder.java
@@ -1,7 +1,8 @@
package org.briarproject.android.privategroup.list;
import android.content.Context;
-import android.support.annotation.Nullable;
+import android.content.Intent;
+import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.View.OnClickListener;
@@ -10,13 +11,18 @@ import android.widget.Button;
import android.widget.TextView;
import org.briarproject.R;
+import org.briarproject.android.privategroup.conversation.GroupActivity;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.android.view.TextAvatarView;
+import org.briarproject.api.sync.GroupId;
import org.jetbrains.annotations.NotNull;
import static android.support.v4.content.ContextCompat.getColor;
+import static android.support.v4.content.ContextCompat.startActivities;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.android.BriarActivity.GROUP_NAME;
class GroupViewHolder extends RecyclerView.ViewHolder {
@@ -44,10 +50,8 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
remove = (Button) v.findViewById(R.id.removeButton);
}
- void bindView(Context ctx, @Nullable final GroupItem group,
+ void bindView(final Context ctx, final GroupItem group,
@NotNull final OnGroupRemoveClickListener listener) {
- if (group == null) return;
-
// Avatar
avatar.setText(group.getName().substring(0, 1));
avatar.setBackgroundBytes(group.getId().getBytes());
@@ -115,15 +119,15 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
layout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
-/*
Intent i = new Intent(ctx, GroupActivity.class);
- GroupId id = item.getId();
+ GroupId id = group.getId();
i.putExtra(GROUP_ID, id.getBytes());
+ i.putExtra(GROUP_NAME, group.getName());
ActivityOptionsCompat options = ActivityOptionsCompat
.makeCustomAnimation(ctx, android.R.anim.fade_in,
android.R.anim.fade_out);
- ActivityCompat.startActivity(ctx, i, options.toBundle());
-*/
+ Intent[] intents = {i};
+ startActivities(ctx, intents, options.toBundle());
}
});
}
diff --git a/briar-android/src/org/briarproject/android/util/NestedTreeList.java b/briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
similarity index 96%
rename from briar-android/src/org/briarproject/android/util/NestedTreeList.java
rename to briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
index d190a9f6d..aec3c58be 100644
--- a/briar-android/src/org/briarproject/android/util/NestedTreeList.java
+++ b/briar-android/src/org/briarproject/android/threaded/NestedTreeList.java
@@ -1,4 +1,4 @@
-package org.briarproject.android.util;
+package org.briarproject.android.threaded;
import android.support.annotation.UiThread;
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
similarity index 62%
rename from briar-android/src/org/briarproject/android/forum/ForumEntry.java
rename to briar-android/src/org/briarproject/android/threaded/ThreadItem.java
index 49e1ee32f..a5a220759 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumEntry.java
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItem.java
@@ -1,41 +1,35 @@
-package org.briarproject.android.forum;
+package org.briarproject.android.threaded;
-import org.briarproject.api.clients.MessageTree;
-import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.clients.MessageTree.MessageNode;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.sync.MessageId;
-/* This class is not thread safe */
-public class ForumEntry implements MessageTree.MessageNode {
+import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
- public final static int LEVEL_UNDEFINED = -1;
+/* This class is not thread safe */
+public abstract class ThreadItem implements MessageNode {
private final MessageId messageId;
private final MessageId parentId;
private final String text;
private final long timestamp;
private final Author author;
- private Status status;
- private int level = LEVEL_UNDEFINED;
+ private final Status status;
+ private int level = UNDEFINED;
private boolean isShowingDescendants = true;
private int descendantCount = 0;
- private boolean isRead = true;
+ private boolean isRead;
- ForumEntry(ForumPostHeader h, String text) {
- this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
- h.getAuthorStatus());
- this.isRead = h.isRead();
- }
-
- public ForumEntry(MessageId messageId, MessageId parentId, String text,
- long timestamp, Author author, Status status) {
+ public ThreadItem(MessageId messageId, MessageId parentId, String text,
+ long timestamp, Author author, Status status, boolean isRead) {
this.messageId = messageId;
this.parentId = parentId;
this.text = text;
this.timestamp = timestamp;
this.author = author;
this.status = status;
+ this.isRead = isRead;
}
public String getText() {
@@ -56,6 +50,7 @@ public class ForumEntry implements MessageTree.MessageNode {
return parentId;
}
+ @Override
public long getTimestamp() {
return timestamp;
}
@@ -68,27 +63,24 @@ public class ForumEntry implements MessageTree.MessageNode {
return status;
}
- boolean isShowingDescendants() {
+ public boolean isShowingDescendants() {
return isShowingDescendants;
}
+ @Override
public void setLevel(int level) {
this.level = level;
}
- void setShowingDescendants(boolean showingDescendants) {
+ public void setShowingDescendants(boolean showingDescendants) {
this.isShowingDescendants = showingDescendants;
}
- MessageId getMessageId() {
- return messageId;
- }
-
public boolean isRead() {
return isRead;
}
- void setRead(boolean read) {
+ public void setRead(boolean read) {
isRead = read;
}
@@ -96,6 +88,7 @@ public class ForumEntry implements MessageTree.MessageNode {
return descendantCount > 0;
}
+ @Override
public void setDescendantCount(int descendantCount) {
this.descendantCount = descendantCount;
}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
new file mode 100644
index 000000000..129e53485
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemAdapter.java
@@ -0,0 +1,298 @@
+package org.briarproject.android.threaded;
+
+import android.animation.ValueAnimator;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+@UiThread
+public abstract class ThreadItemAdapter
+ extends RecyclerView.Adapter> {
+
+ static final int UNDEFINED = -1;
+
+ private final NestedTreeList items =
+ new NestedTreeList<>();
+ private final Map animatingItems =
+ new HashMap<>();
+ // highlight not dependant on time
+ private I replyItem;
+ // temporary highlight
+ private I addedEntry;
+
+ private final ThreadItemListener listener;
+ private final LinearLayoutManager layoutManager;
+
+ public ThreadItemAdapter(ThreadItemListener listener,
+ LinearLayoutManager layoutManager) {
+ this.listener = listener;
+ this.layoutManager = layoutManager;
+ }
+
+ @Override
+ public void onBindViewHolder(ThreadItemViewHolder ui, int position) {
+ I item = getVisibleItem(position);
+ if (item == null) return;
+ listener.onItemVisible(item);
+ ui.bind(this, listener, item, position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return getVisiblePos(null);
+ }
+
+ public I getReplyItem() {
+ return replyItem;
+ }
+
+ public void setItems(Collection items) {
+ this.items.clear();
+ this.items.addAll(items);
+ notifyDataSetChanged();
+ }
+
+ public void add(I item) {
+ items.add(item);
+ addedEntry = item;
+ if (item.getParentId() == null) {
+ notifyItemInserted(getVisiblePos(item));
+ } else {
+ // Try to find the item's parent and perform the proper ui update if
+ // it's present and visible.
+ for (int i = items.indexOf(item) - 1; i >= 0; i--) {
+ I higherItem = items.get(i);
+ if (higherItem.getLevel() < item.getLevel()) {
+ // parent found
+ if (higherItem.isShowingDescendants()) {
+ int parentVisiblePos = getVisiblePos(higherItem);
+ if (parentVisiblePos != NO_POSITION) {
+ // parent is visible, we need to update its ui
+ notifyItemChanged(parentVisiblePos);
+ // new item insert ui
+ int visiblePos = getVisiblePos(item);
+ notifyItemInserted(visiblePos);
+ break;
+ }
+ } else {
+ // do not show the new item if its parent is not showing
+ // descendants (this can be overridden by the user by
+ // pressing the snack bar)
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ public void scrollTo(I item) {
+ int visiblePos = getVisiblePos(item);
+ if (visiblePos == NO_POSITION && item.getParentId() != null) {
+ // The item is not visible due to being hidden by its parent item.
+ // Find the parent and make it visible and traverse up the parent
+ // chain if necessary to make the item visible
+ MessageId parentId = item.getParentId();
+ for (int i = items.indexOf(item) - 1; i >= 0; i--) {
+ I higherItem = items.get(i);
+ if (higherItem.getId().equals(parentId)) {
+ // parent found
+ showDescendants(higherItem);
+ int parentPos = getVisiblePos(higherItem);
+ if (parentPos != NO_POSITION) {
+ // parent or ancestor is visible, entry's visibility
+ // is ensured
+ notifyItemChanged(parentPos);
+ visiblePos = parentPos;
+ break;
+ }
+ // parent or ancestor is hidden, we need to continue up the
+ // dependency chain
+ parentId = higherItem.getParentId();
+ }
+ }
+ }
+ if (visiblePos != NO_POSITION)
+ layoutManager.scrollToPositionWithOffset(visiblePos, 0);
+ }
+
+ int getReplyCount(I item) {
+ int counter = 0;
+ int pos = items.indexOf(item);
+ if (pos >= 0) {
+ int ancestorLvl = item.getLevel();
+ for (int i = pos + 1; i < items.size(); i++) {
+ int descendantLvl = items.get(i).getLevel();
+ if (descendantLvl <= ancestorLvl)
+ break;
+ if (descendantLvl == ancestorLvl + 1)
+ counter++;
+ }
+ }
+ return counter;
+ }
+
+ public void setReplyItem(@Nullable I entry) {
+ if (replyItem != null) {
+ notifyItemChanged(getVisiblePos(replyItem));
+ }
+ replyItem = entry;
+ if (replyItem != null) {
+ notifyItemChanged(getVisiblePos(replyItem));
+ }
+ }
+
+ public void setReplyItemById(MessageId id) {
+ for (I item : items) {
+ if (item.getId().equals(id)) {
+ setReplyItem(item);
+ break;
+ }
+ }
+ }
+
+ private List getSubTreeIndexes(int pos, int levelLimit) {
+ List indexList = new ArrayList<>();
+
+ for (int i = pos + 1; i < getItemCount(); i++) {
+ I item = getVisibleItem(i);
+ if (item != null && item.getLevel() > levelLimit) {
+ indexList.add(i);
+ } else {
+ break;
+ }
+ }
+ return indexList;
+ }
+
+ public void showDescendants(I item) {
+ item.setShowingDescendants(true);
+ int visiblePos = getVisiblePos(item);
+ List indexList =
+ getSubTreeIndexes(visiblePos, item.getLevel());
+ if (!indexList.isEmpty()) {
+ if (indexList.size() == 1) {
+ notifyItemInserted(indexList.get(0));
+ } else {
+ notifyItemRangeInserted(indexList.get(0),
+ indexList.size());
+ }
+ }
+ }
+
+ public void hideDescendants(I item) {
+ int visiblePos = getVisiblePos(item);
+ List indexList =
+ getSubTreeIndexes(visiblePos, item.getLevel());
+ if (!indexList.isEmpty()) {
+ // stop animating children
+ for (int index : indexList) {
+ ValueAnimator anim = animatingItems.get(items.get(index));
+ if (anim != null && anim.isRunning()) {
+ anim.cancel();
+ }
+ }
+ if (indexList.size() == 1) {
+ notifyItemRemoved(indexList.get(0));
+ } else {
+ notifyItemRangeRemoved(indexList.get(0),
+ indexList.size());
+ }
+ }
+ item.setShowingDescendants(false);
+ }
+
+
+ /**
+ * Returns the visible item at the given position
+ * @param position is visible entry index
+ * @return the visible entry at index position from an ordered list of
+ * visible entries, or null if position is larger than
+ * the number of visible entries.
+ */
+ @Nullable
+ public I getVisibleItem(int position) {
+ int levelLimit = UNDEFINED;
+ for (I item : items) {
+ if (levelLimit >= 0) {
+ // skip hidden entries that their parent is hiding
+ if (item.getLevel() > levelLimit) {
+ continue;
+ }
+ levelLimit = UNDEFINED;
+ }
+ if (!item.isShowingDescendants()) {
+ levelLimit = item.getLevel();
+ }
+ if (position-- == 0) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ public boolean isVisible(I item) {
+ return getVisiblePos(item) != NO_POSITION;
+ }
+
+ /**
+ * Returns the visible position of the given ThreadItem
+ * @param item the ThreadItem to find the visible position of, or null to
+ * return the total count of visible elements
+ * @return the visible position of item, or the total number of visible
+ * elements if sEntry is null. If item is not visible NO_POSITION is
+ * returned.
+ */
+ private int getVisiblePos(@Nullable I item) {
+ int visibleCounter = 0;
+ int levelLimit = UNDEFINED;
+ for (I iItem : items) {
+ if (levelLimit >= 0) {
+ if (iItem.getLevel() > levelLimit) {
+ // skip all the entries below a non visible branch
+ continue;
+ }
+ levelLimit = UNDEFINED;
+ }
+ if (item != null && item.equals(iItem)) {
+ return visibleCounter;
+ } else if (!iItem.isShowingDescendants()) {
+ levelLimit = iItem.getLevel();
+ }
+ visibleCounter++;
+ }
+ return item == null ? visibleCounter : NO_POSITION;
+ }
+
+ I getAddedItem() {
+ return addedEntry;
+ }
+
+ void clearAddedItem() {
+ addedEntry = null;
+ }
+
+ void addAnimatingItem(I item, ValueAnimator anim) {
+ animatingItems.put(item, anim);
+ }
+
+ void removeAnimatingItem(I item) {
+ animatingItems.remove(item);
+ }
+
+ public interface ThreadItemListener {
+ void onItemVisible(I item);
+
+ void onReplyClick(I item);
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
new file mode 100644
index 000000000..0b5a5ddc5
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadItemViewHolder.java
@@ -0,0 +1,182 @@
+package org.briarproject.android.threaded;
+
+import android.animation.Animator;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.view.AuthorView;
+import org.briarproject.util.StringUtils;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+@UiThread
+public abstract class ThreadItemViewHolder
+ extends RecyclerView.ViewHolder {
+
+ private final static int ANIMATION_DURATION = 5000;
+
+ private final TextView textView, lvlText, repliesText;
+ private final AuthorView author;
+ private final View[] lvls;
+ private final View chevron, replyButton;
+ private final ViewGroup cell;
+ private final View topDivider;
+
+ public ThreadItemViewHolder(View v) {
+ super(v);
+
+ textView = (TextView) v.findViewById(R.id.text);
+ lvlText = (TextView) v.findViewById(R.id.nested_line_text);
+ author = (AuthorView) v.findViewById(R.id.author);
+ repliesText = (TextView) v.findViewById(R.id.replies);
+ int[] nestedLineIds = {
+ R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
+ R.id.nested_line_4, R.id.nested_line_5
+ };
+ lvls = new View[nestedLineIds.length];
+ for (int i = 0; i < lvls.length; i++) {
+ lvls[i] = v.findViewById(nestedLineIds[i]);
+ }
+ chevron = v.findViewById(R.id.chevron);
+ replyButton = v.findViewById(R.id.btn_reply);
+ cell = (ViewGroup) v.findViewById(R.id.forum_cell);
+ topDivider = v.findViewById(R.id.top_divider);
+ }
+
+ // TODO improve encapsulation, so we don't need to pass the adapter here
+ public void bind(final ThreadItemAdapter adapter,
+ final ThreadItemListener listener, final I item, int pos) {
+
+ textView.setText(StringUtils.trim(item.getText()));
+
+ if (pos == 0) {
+ topDivider.setVisibility(View.INVISIBLE);
+ } else {
+ topDivider.setVisibility(View.VISIBLE);
+ }
+
+ for (int i = 0; i < lvls.length; i++) {
+ lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
+ }
+ if (item.getLevel() > 5) {
+ lvlText.setVisibility(VISIBLE);
+ lvlText.setText("" + item.getLevel());
+ } else {
+ lvlText.setVisibility(GONE);
+ }
+ author.setAuthor(item.getAuthor());
+ author.setDate(item.getTimestamp());
+ author.setAuthorStatus(item.getStatus());
+
+ int replies = adapter.getReplyCount(item);
+ if (replies == 0) {
+ repliesText.setText("");
+ } else {
+ repliesText.setText(getContext().getResources()
+ .getQuantityString(R.plurals.message_replies, replies,
+ replies));
+ }
+
+ if (item.hasDescendants()) {
+ chevron.setVisibility(VISIBLE);
+ if (item.isShowingDescendants()) {
+ chevron.setSelected(false);
+ } else {
+ chevron.setSelected(true);
+ }
+ chevron.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ chevron.setSelected(!chevron.isSelected());
+ if (chevron.isSelected()) {
+ adapter.hideDescendants(item);
+ } else {
+ adapter.showDescendants(item);
+ }
+ }
+ });
+ } else {
+ chevron.setVisibility(INVISIBLE);
+ }
+ if (item.equals(adapter.getReplyItem())) {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.forum_cell_highlight));
+ } else if (item.equals(adapter.getAddedItem())) {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.forum_cell_highlight));
+ animateFadeOut(adapter, adapter.getAddedItem());
+ adapter.clearAddedItem();
+ } else {
+ cell.setBackgroundColor(ContextCompat
+ .getColor(getContext(), R.color.window_background));
+ }
+ replyButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onReplyClick(item);
+ adapter.scrollTo(item);
+ }
+ });
+ }
+
+ private void animateFadeOut(final ThreadItemAdapter adapter,
+ final I addedItem) {
+
+ setIsRecyclable(false);
+ ValueAnimator anim = new ValueAnimator();
+ adapter.addAnimatingItem(addedItem, anim);
+ ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
+ anim.setIntValues(viewColor.getColor(), ContextCompat
+ .getColor(getContext(), R.color.window_background));
+ anim.setEvaluator(new ArgbEvaluator());
+ anim.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setIsRecyclable(true);
+ adapter.removeAnimatingItem(addedItem);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ setIsRecyclable(true);
+ adapter.removeAnimatingItem(addedItem);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
+ anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ cell.setBackgroundColor(
+ (Integer) valueAnimator.getAnimatedValue());
+ }
+ });
+ anim.setDuration(ANIMATION_DURATION);
+ anim.start();
+ }
+
+ private Context getContext() {
+ return textView.getContext();
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
new file mode 100644
index 000000000..889399525
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java
@@ -0,0 +1,304 @@
+package org.briarproject.android.threaded;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.UiThread;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.briarproject.R;
+import org.briarproject.android.BriarActivity;
+import org.briarproject.android.controller.handler.UiResultExceptionHandler;
+import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
+import org.briarproject.android.threaded.ThreadListController.ThreadListListener;
+import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.android.view.TextInputView;
+import org.briarproject.android.view.TextInputView.TextInputListener;
+import org.briarproject.api.clients.NamedGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
+
+import java.util.Collection;
+
+import static android.support.design.widget.Snackbar.make;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public abstract class ThreadListActivity>
+ extends BriarActivity
+ implements ThreadListListener, TextInputListener,
+ ThreadItemListener {
+
+ protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
+ protected static final String KEY_REPLY_ID = "replyId";
+
+ protected A adapter;
+ protected BriarRecyclerView list;
+ protected TextInputView textInput;
+ protected GroupId groupId;
+ private MessageId replyId;
+
+ protected abstract ThreadListController getController();
+
+ @CallSuper
+ @Override
+ @SuppressWarnings("ConstantConditions")
+ public void onCreate(final Bundle state) {
+ super.onCreate(state);
+
+ setContentView(getLayout());
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra(GROUP_ID);
+ if (b == null) throw new IllegalStateException("No GroupId in intent.");
+ groupId = new GroupId(b);
+ getController().setGroupId(groupId);
+
+ textInput = (TextInputView) findViewById(R.id.text_input_container);
+ textInput.setVisibility(GONE);
+ textInput.setListener(this);
+ list = (BriarRecyclerView) findViewById(R.id.list);
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
+ list.setLayoutManager(linearLayoutManager);
+ adapter = createAdapter(linearLayoutManager);
+ list.setAdapter(adapter);
+
+ if (state != null) {
+ byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID);
+ if(replyIdBytes != null) replyId = new MessageId(replyIdBytes);
+ }
+
+ loadItems();
+ }
+
+ @LayoutRes
+ protected abstract int getLayout();
+
+ 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) {
+ // TODO Proper error handling
+ finish();
+ }
+ });
+ }
+
+ @UiThread
+ protected abstract void onNamedGroupLoaded(G groupItem);
+
+ private void loadItems() {
+ getController().loadItems(
+ new UiResultExceptionHandler, DbException>(
+ this) {
+ @Override
+ public void onResultUi(Collection items) {
+ if (items.isEmpty()) {
+ list.showData();
+ } else {
+ adapter.setItems(items);
+ list.showData();
+ if (replyId != null)
+ adapter.setReplyItemById(replyId);
+ }
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO Proper error handling
+ finish();
+ }
+ });
+ }
+
+ @CallSuper
+ @Override
+ public void onResume() {
+ super.onResume();
+ list.startPeriodicUpdate();
+ }
+
+ @CallSuper
+ @Override
+ public void onPause() {
+ super.onPause();
+ list.stopPeriodicUpdate();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ textInput.setVisibility(
+ savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
+ VISIBLE : GONE);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_INPUT_VISIBILITY,
+ textInput.getVisibility() == VISIBLE);
+ ThreadItem replyItem = adapter.getReplyItem();
+ if (replyItem != null) {
+ outState.putByteArray(KEY_REPLY_ID,
+ replyItem.getId().getBytes());
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (textInput.isKeyboardOpen()) textInput.hideSoftKeyboard();
+ supportFinishAfterTransition();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (textInput.getVisibility() == VISIBLE) {
+ textInput.setVisibility(GONE);
+ adapter.setReplyItem(null);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onItemVisible(I item) {
+ if (!item.isRead()) {
+ item.setRead(true);
+ getController().markItemRead(item);
+ }
+ }
+
+ @Override
+ public void onReplyClick(I item) {
+ showTextInput(item);
+ }
+
+ protected void displaySnackbarShort(@StringRes int stringId) {
+ Snackbar snackbar = make(list, stringId, Snackbar.LENGTH_SHORT);
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.show();
+ }
+
+ protected void showTextInput(@Nullable I replyItem) {
+ // An animation here would be an overkill because of the keyboard
+ // popping up.
+ // only clear the text when the input container was not visible
+ if (textInput.getVisibility() != VISIBLE) {
+ textInput.setVisibility(VISIBLE);
+ textInput.setText("");
+ }
+ textInput.requestFocus();
+ textInput.showSoftKeyboard();
+ textInput.setHint(replyItem == null ? R.string.forum_new_message_hint :
+ R.string.forum_message_reply_hint);
+ adapter.setReplyItem(replyItem);
+ }
+
+ @Override
+ public void onSendClick(String text) {
+ if (text.trim().length() == 0)
+ return;
+ if (StringUtils.isTooLong(text, getMaxBodyLength())) {
+ displaySnackbarShort(R.string.text_too_long);
+ return;
+ }
+ I replyItem = adapter.getReplyItem();
+ UiResultExceptionHandler handler =
+ new UiResultExceptionHandler(this) {
+ @Override
+ public void onResultUi(I result) {
+ addItem(result, true);
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO add proper exception handling
+ finish();
+ }
+ };
+ getController().createAndStoreMessage(text,
+ replyItem != null ? replyItem.getId() : null, handler);
+ textInput.hideSoftKeyboard();
+ textInput.setVisibility(GONE);
+ adapter.setReplyItem(null);
+ }
+
+ protected abstract int getMaxBodyLength();
+
+ @Override
+ public void onHeaderReceived(H header) {
+ getController().loadItem(header,
+ new UiResultExceptionHandler(this) {
+ @Override
+ public void onResultUi(final I result) {
+ addItem(result, false);
+ }
+
+ @Override
+ public void onExceptionUi(DbException exception) {
+ // TODO add proper exception handling
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onGroupRemoved() {
+ supportFinishAfterTransition();
+ }
+
+ protected void addItem(final I item, boolean isLocal) {
+ adapter.add(item);
+ if (isLocal && adapter.isVisible(item)) {
+ displaySnackbarShort(getItemPostedString());
+ } else {
+ Snackbar snackbar = Snackbar.make(list,
+ isLocal ? getItemPostedString() : getItemReceivedString(),
+ Snackbar.LENGTH_LONG);
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.setActionTextColor(ContextCompat
+ .getColor(ThreadListActivity.this,
+ R.color.briar_button_positive));
+ snackbar.setAction(R.string.show, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ adapter.scrollTo(item);
+ }
+ });
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.show();
+ }
+ }
+
+ @StringRes
+ protected abstract int getItemPostedString();
+
+ @StringRes
+ protected abstract int getItemReceivedString();
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
new file mode 100644
index 000000000..a7731d8d4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java
@@ -0,0 +1,45 @@
+package org.briarproject.android.threaded;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.briarproject.android.DestroyableContext;
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.NamedGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.Collection;
+
+public interface ThreadListController
+ extends ActivityLifecycleController {
+
+ void setGroupId(GroupId groupId);
+
+ void loadNamedGroup(ResultExceptionHandler handler);
+
+ void loadItem(H header, ResultExceptionHandler handler);
+
+ void loadItems(ResultExceptionHandler, DbException> handler);
+
+ void markItemRead(I item);
+
+ void markItemsRead(Collection items);
+
+ void createAndStoreMessage(String body, @Nullable MessageId parentId,
+ ResultExceptionHandler handler);
+
+ void deleteNamedGroup(ResultExceptionHandler handler);
+
+ interface ThreadListListener extends DestroyableContext {
+ @UiThread
+ void onHeaderReceived(H header);
+
+ @UiThread
+ void onGroupRemoved();
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
new file mode 100644
index 000000000..ac968758f
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java
@@ -0,0 +1,363 @@
+package org.briarproject.android.threaded;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+
+import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.ResultExceptionHandler;
+import org.briarproject.api.clients.BaseMessage;
+import org.briarproject.api.clients.NamedGroup;
+import org.briarproject.api.clients.PostHeader;
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public abstract class ThreadListControllerImpl
+ extends DbControllerImpl
+ implements ThreadListController, EventListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(ThreadListControllerImpl.class.getName());
+
+ private final IdentityManager identityManager;
+ private final Executor cryptoExecutor;
+ protected final AndroidNotificationManager notificationManager;
+ private final EventBus eventBus;
+ private final Clock clock;
+
+ private final Map bodyCache =
+ new ConcurrentHashMap<>();
+
+ private volatile GroupId groupId;
+
+ protected ThreadListListener listener;
+
+ protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
+ LifecycleManager lifecycleManager, IdentityManager identityManager,
+ @CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
+ AndroidNotificationManager notificationManager, Clock clock) {
+ super(dbExecutor, lifecycleManager);
+ this.identityManager = identityManager;
+ this.cryptoExecutor = cryptoExecutor;
+ this.eventBus = eventBus;
+ this.notificationManager = notificationManager;
+ this.clock = clock;
+ }
+
+ @Override
+ public void setGroupId(GroupId groupId) {
+ this.groupId = groupId;
+ }
+
+ @CallSuper
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onActivityCreate(Activity activity) {
+ listener = (ThreadListListener) activity;
+ }
+
+ @CallSuper
+ @Override
+ public void onActivityResume() {
+ notificationManager.blockNotification(getGroupId());
+ eventBus.addListener(this);
+ }
+
+ @CallSuper
+ @Override
+ public void onActivityPause() {
+ notificationManager.unblockNotification(getGroupId());
+ eventBus.removeListener(this);
+ }
+
+ @Override
+ public void onActivityDestroy() {
+ }
+
+ @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.runOnUiThreadUnlessDestroyed(new Runnable() {
+ @Override
+ public void run() {
+ listener.onGroupRemoved();
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void loadNamedGroup(
+ final ResultExceptionHandler handler) {
+ checkGroupId();
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ G groupItem = loadNamedGroup();
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info(
+ "Loading named group took " + duration + " ms");
+ handler.onResult(groupItem);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract G loadNamedGroup() throws DbException;
+
+ @Override
+ public void loadItems(
+ final ResultExceptionHandler, DbException> handler) {
+ checkGroupId();
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ LOG.info("Loading items...");
+ try {
+ // Load headers
+ long now = System.currentTimeMillis();
+ Collection headers = loadHeaders();
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading headers took " + duration + " ms");
+
+ // Load bodies into cache
+ now = System.currentTimeMillis();
+ for (H header : headers) {
+ if (!bodyCache.containsKey(header.getId())) {
+ bodyCache.put(header.getId(),
+ loadMessageBody(header.getId()));
+ }
+ }
+ duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading bodies took " + duration + " ms");
+
+ // Build and hand over items
+ handler.onResult(buildItems(headers));
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract Collection loadHeaders() throws DbException;
+
+ @DatabaseExecutor
+ protected abstract String loadMessageBody(MessageId id) throws DbException;
+
+ @Override
+ public void loadItem(final H header,
+ final ResultExceptionHandler handler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ LOG.info("Loading item...");
+ try {
+ String body;
+ if (!bodyCache.containsKey(header.getId())) {
+ body = loadMessageBody(header.getId());
+ bodyCache.put(header.getId(), body);
+ } else {
+ body = bodyCache.get(header.getId());
+ }
+ I item = buildItem(header, body);
+ handler.onResult(item);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void markItemRead(I item) {
+ markItemsRead(Collections.singletonList(item));
+ }
+
+ @Override
+ public void markItemsRead(final Collection items) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ for (I i : items) {
+ markRead(i.getId());
+ }
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Marking read took " + duration + " ms");
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract void markRead(MessageId id) throws DbException;
+
+ @Override
+ public void createAndStoreMessage(final String body,
+ @Nullable final MessageId parentId,
+ final ResultExceptionHandler handler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ LocalAuthor author = identityManager.getLocalAuthor();
+ long timestamp = getLatestTimestamp();
+ timestamp =
+ Math.max(timestamp, clock.currentTimeMillis());
+ createMessage(body, timestamp, parentId, author,
+ handler);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract long getLatestTimestamp() throws DbException;
+
+ private void createMessage(final String body, final long timestamp,
+ final @Nullable MessageId parentId, final LocalAuthor author,
+ final ResultExceptionHandler handler) {
+ cryptoExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ LOG.info("Creating message...");
+ M msg = createLocalMessage(body, timestamp, parentId, author);
+ storePost(msg, body, handler);
+ }
+ });
+ }
+
+ @CryptoExecutor
+ protected abstract M createLocalMessage(String body, long timestamp,
+ @Nullable MessageId parentId, LocalAuthor author);
+
+ private void storePost(final M msg, final String body,
+ final ResultExceptionHandler resultHandler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ LOG.info("Store message...");
+ long now = System.currentTimeMillis();
+ H header = addLocalMessage(msg);
+ bodyCache.put(msg.getMessage().getId(), body);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Storing message took " + duration + " ms");
+ resultHandler.onResult(buildItem(header, body));
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onException(e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract H addLocalMessage(M message) throws DbException;
+
+ @Override
+ public void deleteNamedGroup(
+ final ResultExceptionHandler handler) {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ G groupItem = loadNamedGroup();
+ deleteNamedGroup(groupItem);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Removing group took " + duration + " ms");
+ //noinspection ConstantConditions
+ handler.onResult(null);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ handler.onException(e);
+ }
+ }
+ });
+ }
+
+ @DatabaseExecutor
+ protected abstract void deleteNamedGroup(G groupItem) throws DbException;
+
+ private List buildItems(Collection headers) {
+ List entries = new ArrayList<>();
+ for (H h : headers) {
+ entries.add(buildItem(h, bodyCache.get(h.getId())));
+ }
+ return entries;
+ }
+
+ protected abstract I buildItem(H header, String body);
+
+ protected GroupId getGroupId() {
+ checkGroupId();
+ return groupId;
+ }
+
+ private void checkGroupId() {
+ if (groupId == null) {
+ throw new IllegalStateException(
+ "You must set the GroupId before the controller is started.");
+ }
+ }
+
+}
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 3e5431ac9..e6290e971 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -11,14 +11,12 @@ import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
@@ -26,6 +24,7 @@ import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import static junit.framework.Assert.assertEquals;
@@ -81,7 +80,7 @@ public class ForumActivityTest {
private TestForumActivity forumActivity;
@Captor
- private ArgumentCaptor, DbException>>
+ private ArgumentCaptor, DbException>>
rc;
@Before
@@ -93,14 +92,14 @@ public class ForumActivityTest {
.withIntent(intent).create().resume().get();
}
- private List getDummyData() {
- ForumEntry[] forumEntries = new ForumEntry[6];
+ private List getDummyData() {
+ ForumItem[] forumEntries = new ForumItem[6];
for (int i = 0; i < forumEntries.length; i++) {
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
Author author = new Author(authorId, AUTHORS[i], publicKey);
forumEntries[i] =
- new ForumEntry(AUTHOR_IDS[i], PARENT_AUTHOR_IDS[i],
+ new ForumItem(AUTHOR_IDS[i], PARENT_AUTHOR_IDS[i],
AUTHORS[i], System.currentTimeMillis(), author,
UNKNOWN);
forumEntries[i].setLevel(LEVELS[i]);
@@ -111,9 +110,8 @@ public class ForumActivityTest {
@Test
public void testNestedEntries() {
ForumController mc = forumActivity.getController();
- List dummyData = getDummyData();
- verify(mc, times(1))
- .loadForum(Mockito.any(GroupId.class), rc.capture());
+ List dummyData = getDummyData();
+ verify(mc, times(1)).loadItems(rc.capture());
rc.getValue().onResult(dummyData);
NestedForumAdapter adapter = forumActivity.getAdapter();
Assert.assertNotNull(adapter);
@@ -126,9 +124,9 @@ public class ForumActivityTest {
adapter.hideDescendants(dummyData.get(0));
assertEquals(2, adapter.getItemCount());
assertTrue(dummyData.get(0).getText()
- .equals(adapter.getVisibleEntry(0).getText()));
+ .equals(adapter.getVisibleItem(0).getText()));
assertTrue(dummyData.get(5).getText()
- .equals(adapter.getVisibleEntry(1).getText()));
+ .equals(adapter.getVisibleItem(1).getText()));
// Cascade re-open
adapter.showDescendants(dummyData.get(0));
assertEquals(4, adapter.getItemCount());
@@ -137,8 +135,8 @@ public class ForumActivityTest {
adapter.showDescendants(dummyData.get(2));
assertEquals(6, adapter.getItemCount());
assertTrue(dummyData.get(2).getText()
- .equals(adapter.getVisibleEntry(2).getText()));
+ .equals(adapter.getVisibleItem(2).getText()));
assertTrue(dummyData.get(4).getText()
- .equals(adapter.getVisibleEntry(4).getText()));
+ .equals(adapter.getVisibleItem(4).getText()));
}
}
diff --git a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
index 8acbf3b8d..806fee299 100644
--- a/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
+++ b/briar-android/test/java/org/briarproject/android/forum/TestForumActivity.java
@@ -16,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
}
public NestedForumAdapter getAdapter() {
- return forumAdapter;
+ return adapter;
}
@Override
diff --git a/briar-api/src/org/briarproject/api/blogs/Blog.java b/briar-api/src/org/briarproject/api/blogs/Blog.java
index 5de781e7d..153eb6e27 100644
--- a/briar-api/src/org/briarproject/api/blogs/Blog.java
+++ b/briar-api/src/org/briarproject/api/blogs/Blog.java
@@ -2,19 +2,22 @@ package org.briarproject.api.blogs;
import org.briarproject.api.clients.BaseGroup;
import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sharing.Shareable;
import org.briarproject.api.sync.Group;
import org.jetbrains.annotations.NotNull;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
public class Blog extends BaseGroup implements Shareable {
private final String description;
private final Author author;
- public Blog(@NotNull Group group, @NotNull String name,
- @NotNull String description, @NotNull Author author) {
- super(group, name, null);
-
+ public Blog(Group group, String name, String description, Author author) {
+ super(group, name);
this.description = description;
this.author = author;
}
diff --git a/briar-api/src/org/briarproject/api/clients/BaseGroup.java b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
index fe8a282f5..f101106e3 100644
--- a/briar-api/src/org/briarproject/api/clients/BaseGroup.java
+++ b/briar-api/src/org/briarproject/api/clients/BaseGroup.java
@@ -1,19 +1,22 @@
package org.briarproject.api.clients;
+import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.jetbrains.annotations.NotNull;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
public abstract class BaseGroup {
private final Group group;
private final String name;
- private final byte[] salt;
- public BaseGroup(@NotNull Group group, @NotNull String name, byte[] salt) {
+ public BaseGroup(Group group, String name) {
this.group = group;
this.name = name;
- this.salt = salt;
}
@NotNull
@@ -31,10 +34,6 @@ public abstract class BaseGroup {
return name;
}
- public byte[] getSalt() {
- return salt;
- }
-
@Override
public int hashCode() {
return group.hashCode();
diff --git a/briar-api/src/org/briarproject/api/clients/NamedGroup.java b/briar-api/src/org/briarproject/api/clients/NamedGroup.java
new file mode 100644
index 000000000..f4b95fdc5
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/clients/NamedGroup.java
@@ -0,0 +1,29 @@
+package org.briarproject.api.clients;
+
+import org.briarproject.api.nullsafety.NotNullByDefault;
+import org.briarproject.api.sync.Group;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public abstract class NamedGroup extends BaseGroup {
+
+ private final byte[] salt;
+
+ public NamedGroup(@NotNull Group group, @NotNull String name, byte[] salt) {
+ super(group, name);
+ this.salt = salt;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof NamedGroup && super.equals(o);
+ }
+
+}
diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java
index cdaa091b9..f73cd049a 100644
--- a/briar-api/src/org/briarproject/api/forum/Forum.java
+++ b/briar-api/src/org/briarproject/api/forum/Forum.java
@@ -1,10 +1,15 @@
package org.briarproject.api.forum;
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
+import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sharing.Shareable;
import org.briarproject.api.sync.Group;
-public class Forum extends BaseGroup implements Shareable {
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class Forum extends NamedGroup implements Shareable {
public Forum(Group group, String name, byte[] salt) {
super(group, name, salt);
diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java
index 8c35d05e3..7561fc963 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumManager.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java
@@ -1,11 +1,14 @@
package org.briarproject.api.forum;
import org.briarproject.api.clients.MessageTracker;
+import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
+import org.jetbrains.annotations.Nullable;
import java.util.Collection;
@@ -20,8 +23,13 @@ public interface ForumManager extends MessageTracker {
/** Unsubscribes from a forum. */
void removeForum(Forum f) throws DbException;
+ /** Creates a local forum post. */
+ @CryptoExecutor
+ ForumPost createLocalPost(GroupId groupId, String body, long timestamp,
+ @Nullable MessageId parentId, LocalAuthor author);
+
/** Stores a local forum post. */
- void addLocalPost(ForumPost p) throws DbException;
+ ForumPostHeader addLocalPost(ForumPost p) throws DbException;
/** Returns the forum with the given ID. */
Forum getForum(GroupId g) throws DbException;
diff --git a/briar-api/src/org/briarproject/api/forum/ForumPost.java b/briar-api/src/org/briarproject/api/forum/ForumPost.java
index aef48c78c..bbc23e9db 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumPost.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumPost.java
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
public class ForumPost extends BaseMessage {
+ @Nullable
private final Author author;
public ForumPost(@NotNull Message message, @Nullable MessageId parent,
diff --git a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
index e05edc883..1d6aa9e76 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
@@ -1,8 +1,7 @@
package org.briarproject.api.forum;
import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.PrivateKey;
-import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
@@ -15,7 +14,6 @@ public interface ForumPostFactory {
throws FormatException;
ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
- MessageId parent, Author author, String contentType, byte[] body,
- PrivateKey privateKey) throws FormatException,
- GeneralSecurityException;
+ MessageId parent, LocalAuthor author, String body)
+ throws FormatException, GeneralSecurityException;
}
diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
index 8ffe6c6a4..22457de2e 100644
--- a/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
+++ b/briar-api/src/org/briarproject/api/privategroup/GroupMessage.java
@@ -1,17 +1,24 @@
package org.briarproject.api.privategroup;
-import org.briarproject.api.forum.ForumPost;
+import org.briarproject.api.clients.BaseMessage;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-public class GroupMessage extends ForumPost {
+public class GroupMessage extends BaseMessage {
+
+ private final Author author;
public GroupMessage(@NotNull Message message, @Nullable MessageId parent,
@NotNull Author author) {
- super(message, parent, author);
+ super(message, parent);
+ this.author = author;
+ }
+
+ public Author getAuthor() {
+ return author;
}
}
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
index f6d5862c7..310611361 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java
@@ -1,11 +1,16 @@
package org.briarproject.api.privategroup;
-import org.briarproject.api.clients.BaseGroup;
+import org.briarproject.api.clients.NamedGroup;
import org.briarproject.api.identity.Author;
+import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Group;
import org.jetbrains.annotations.NotNull;
-public class PrivateGroup extends BaseGroup {
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class PrivateGroup extends NamedGroup {
private final Author author;
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupFactory.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupFactory.java
index 492eabc86..6a27552fa 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupFactory.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupFactory.java
@@ -1,6 +1,8 @@
package org.briarproject.api.privategroup;
+import org.briarproject.api.FormatException;
import org.briarproject.api.identity.Author;
+import org.briarproject.api.sync.Group;
import org.jetbrains.annotations.NotNull;
public interface PrivateGroupFactory {
@@ -17,4 +19,9 @@ public interface PrivateGroupFactory {
@NotNull
PrivateGroup createPrivateGroup(String name, Author author, byte[] salt);
+ /**
+ * Parses a group and returns the corresponding PrivateGroup.
+ */
+ PrivateGroup parsePrivateGroup(Group group) throws FormatException;
+
}
diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
index a7fbd0cb1..956a42344 100644
--- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java
@@ -3,10 +3,12 @@ package org.briarproject.api.privategroup;
import org.briarproject.api.clients.MessageTracker;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.Collection;
@@ -19,8 +21,12 @@ public interface PrivateGroupManager extends MessageTracker {
/** Removes a dissolved private group. */
void removePrivateGroup(GroupId g) throws DbException;
+ /** Creates a local group message. */
+ GroupMessage createLocalMessage(GroupId groupId, String body,
+ long timestamp, @Nullable MessageId parentId, LocalAuthor author);
+
/** Stores (and sends) a local group message. */
- void addLocalMessage(GroupMessage p) throws DbException;
+ GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
/** Returns the private group with the given ID. */
@NotNull
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 77a7712e5..8d1511f7d 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -13,11 +13,13 @@ import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumFactory;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPost;
+import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
@@ -25,7 +27,9 @@ import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils;
+import org.jetbrains.annotations.Nullable;
+import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -47,6 +51,7 @@ import static org.briarproject.api.forum.ForumConstants.KEY_PARENT;
import static org.briarproject.api.forum.ForumConstants.KEY_PUBLIC_NAME;
import static org.briarproject.api.forum.ForumConstants.KEY_TIMESTAMP;
import static org.briarproject.api.identity.Author.Status.ANONYMOUS;
+import static org.briarproject.api.identity.Author.Status.OURSELVES;
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
@@ -57,16 +62,18 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
private final IdentityManager identityManager;
private final ForumFactory forumFactory;
+ private final ForumPostFactory forumPostFactory;
private final List removeHooks;
@Inject
ForumManagerImpl(DatabaseComponent db, IdentityManager identityManager,
ClientHelper clientHelper, MetadataParser metadataParser,
- ForumFactory forumFactory) {
+ ForumFactory forumFactory, ForumPostFactory forumPostFactory) {
super(db, clientHelper, metadataParser);
this.identityManager = identityManager;
this.forumFactory = forumFactory;
+ this.forumPostFactory = forumPostFactory;
removeHooks = new CopyOnWriteArrayList();
}
@@ -118,7 +125,24 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
}
@Override
- public void addLocalPost(ForumPost p) throws DbException {
+ public ForumPost createLocalPost(final GroupId groupId, final String body,
+ final long timestamp, final @Nullable MessageId parentId,
+ final LocalAuthor author) {
+ ForumPost p;
+ try {
+ p = forumPostFactory
+ .createPseudonymousPost(groupId, timestamp, parentId,
+ author, body);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (FormatException e) {
+ throw new RuntimeException(e);
+ }
+ return p;
+ }
+
+ @Override
+ public ForumPostHeader addLocalPost(ForumPost p) throws DbException {
Transaction txn = db.startTransaction(false);
try {
BdfDictionary meta = new BdfDictionary();
@@ -142,6 +166,8 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
} finally {
db.endTransaction(txn);
}
+ return new ForumPostHeader(p.getMessage().getId(), p.getParent(),
+ p.getMessage().getTimestamp(), p.getAuthor(), OURSELVES, true);
}
@Override
diff --git a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
index a6c0b226a..7c0d1e46d 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
@@ -3,12 +3,13 @@ package org.briarproject.forum;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.crypto.Signature;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
@@ -49,9 +50,10 @@ class ForumPostFactoryImpl implements ForumPostFactory {
@Override
public ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
- MessageId parent, Author author, String contentType, byte[] body,
- PrivateKey privateKey) throws FormatException,
- GeneralSecurityException {
+ MessageId parent, LocalAuthor author, String bodyStr)
+ throws FormatException, GeneralSecurityException {
+ String contentType = "text/plain";
+ byte[] body = StringUtils.toUtf8(bodyStr);
// Validate the arguments
if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
throw new IllegalArgumentException();
@@ -62,6 +64,10 @@ class ForumPostFactoryImpl implements ForumPostFactory {
author.getPublicKey());
BdfList signed = BdfList.of(groupId, timestamp, parent, authorList,
contentType, body);
+ // Get private key
+ KeyParser keyParser = crypto.getSignatureKeyParser();
+ byte[] k = author.getPrivateKey();
+ PrivateKey privateKey = keyParser.parsePrivateKey(k);
// Generate the signature
Signature signature = crypto.getSignature();
signature.initSign(privateKey);
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupFactoryImpl.java
index 429fd56b5..7389f2fd6 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupFactoryImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupFactoryImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.sync.Group;
@@ -22,14 +23,17 @@ class PrivateGroupFactoryImpl implements PrivateGroupFactory {
private final GroupFactory groupFactory;
private final ClientHelper clientHelper;
+ private final AuthorFactory authorFactory;
private final SecureRandom random;
@Inject
PrivateGroupFactoryImpl(GroupFactory groupFactory,
- ClientHelper clientHelper, SecureRandom random) {
+ ClientHelper clientHelper, AuthorFactory authorFactory,
+ SecureRandom random) {
this.groupFactory = groupFactory;
this.clientHelper = clientHelper;
+ this.authorFactory = authorFactory;
this.random = random;
}
@@ -66,4 +70,13 @@ class PrivateGroupFactoryImpl implements PrivateGroupFactory {
}
}
+ @Override
+ public PrivateGroup parsePrivateGroup(Group group) throws FormatException {
+ byte[] descriptor = group.getDescriptor();
+ BdfList list = clientHelper.toList(descriptor);
+ Author a =
+ authorFactory.createAuthor(list.getString(1), list.getRaw(2));
+ return new PrivateGroup(group, list.getString(0), a, list.getRaw(3));
+ }
+
}
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index 3c9f5814b..6f40bd9c6 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -8,27 +8,34 @@ import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
-import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.privategroup.GroupMessage;
+import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Collections;
import java.util.logging.Logger;
import javax.inject.Inject;
+import static org.briarproject.api.identity.Author.Status.OURSELVES;
+
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
PrivateGroupManager {
@@ -40,16 +47,21 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
private final IdentityManager identityManager;
private final PrivateGroupFactory privateGroupFactory;
+ private final GroupMessageFactory groupMessageFactory;
+ private final Clock clock;
@Inject
PrivateGroupManagerImpl(ClientHelper clientHelper,
MetadataParser metadataParser, DatabaseComponent db,
IdentityManager identityManager,
- PrivateGroupFactory privateGroupFactory) {
+ PrivateGroupFactory privateGroupFactory,
+ GroupMessageFactory groupMessageFactory, Clock clock) {
super(db, clientHelper, metadataParser);
this.identityManager = identityManager;
this.privateGroupFactory = privateGroupFactory;
+ this.groupMessageFactory = groupMessageFactory;
+ this.clock = clock;
}
@NotNull
@@ -64,7 +76,22 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
}
@Override
- public void addLocalMessage(GroupMessage m) throws DbException {
+ public GroupMessage createLocalMessage(GroupId groupId, String body,
+ long timestamp, @Nullable MessageId parentId, LocalAuthor author) {
+ try {
+ return groupMessageFactory
+ .createGroupMessage(groupId, timestamp, parentId, author,
+ body);
+ } catch (FormatException e) {
+ throw new RuntimeException(e);
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public GroupMessageHeader addLocalMessage(GroupMessage m)
+ throws DbException {
Transaction txn = db.startTransaction(false);
try {
BdfDictionary meta = new BdfDictionary();
@@ -76,21 +103,35 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
} finally {
db.endTransaction(txn);
}
+ return new GroupMessageHeader(m.getMessage().getGroupId(),
+ m.getMessage().getId(), m.getParent(),
+ m.getMessage().getTimestamp(), m.getAuthor(), OURSELVES, true);
}
@NotNull
@Override
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
- Author a = identityManager.getLocalAuthor();
- return privateGroupFactory.createPrivateGroup("todo", a);
+ PrivateGroup privateGroup;
+ Transaction txn = db.startTransaction(true);
+ try {
+ privateGroup = getPrivateGroup(txn, g);
+ txn.setComplete();
+ } finally {
+ db.endTransaction(txn);
+ }
+ return privateGroup;
}
@NotNull
@Override
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
throws DbException {
- Author a = identityManager.getLocalAuthor(txn);
- return privateGroupFactory.createPrivateGroup("todo", a);
+ try {
+ Group group = db.getGroup(txn, g);
+ return privateGroupFactory.parsePrivateGroup(group);
+ } catch (FormatException e) {
+ throw new DbException(e);
+ }
}
@NotNull
diff --git a/briar-core/src/org/briarproject/util/StringUtils.java b/briar-core/src/org/briarproject/util/StringUtils.java
index f8a97a6d6..ec43eebf7 100644
--- a/briar-core/src/org/briarproject/util/StringUtils.java
+++ b/briar-core/src/org/briarproject/util/StringUtils.java
@@ -86,4 +86,11 @@ public class StringUtils {
public static String trim(String s) {
return s.trim();
}
+
+ /**
+ * Returns true if the string is longer than maxLength
+ */
+ public static boolean isTooLong(String s, int maxLength) {
+ return toUtf8(s).length > maxLength;
+ }
}