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 4c4ba9b965365bce29cad3c7f88e03d5faeb3fe6..ae3e770a4e944e48fc201490cee7e8965c0876e0 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 5e5b877cc86364d8fefdffcefac2c0fd6647426d..45fb9d7237bd38f3dcb8b575d5783c8fb736773c 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 86c5239e549e81befe949a9b2b32ce3a7e4c0db8..1bb12bfff447a50c72d83e6b30d5323d9a464d08 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -100,6 +100,17 @@ /> </activity> + <activity + android:name=".android.privategroup.conversation.GroupActivity" + android:label="@string/app_name" + android:parentActivityName=".android.NavDrawerActivity" + android:windowSoftInputMode="adjustResize|stateHidden"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.NavDrawerActivity" + /> + </activity> + <activity android:name=".android.sharing.InvitationsForumActivity" android:label="@string/forum_invitations_title" diff --git a/briar-android/res/drawable/ic_group_white.xml b/briar-android/res/drawable/ic_group_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..f6e32475bd91839f3d140ff1afc791f660edaacb --- /dev/null +++ b/briar-android/res/drawable/ic_group_white.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/> +</vector> diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml index b796da899f8744256b77dbdc225f18689970cf83..81c7e4a2bb220efd271238321ef1a84d91e4b8b2 100644 --- a/briar-android/res/layout/activity_forum.xml +++ b/briar-android/res/layout/activity_forum.xml @@ -7,12 +7,12 @@ android:orientation="vertical"> <org.briarproject.android.view.BriarRecyclerView - android:id="@+id/forum_discussion_list" + android:id="@+id/list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - app:scrollToEnd="false" - app:emptyText="@string/no_forum_posts"/> + app:emptyText="@string/no_forum_posts" + app:scrollToEnd="false"/> <org.briarproject.android.view.TextInputView android:id="@+id/text_input_container" diff --git a/briar-android/res/menu/forum_actions.xml b/briar-android/res/menu/forum_actions.xml index 2a262cbfa3b53e43ffcd04dab63b7887c7bddbf5..cd4e4573c50e29dddeb025f5c7980800665ca4bb 100644 --- a/briar-android/res/menu/forum_actions.xml +++ b/briar-android/res/menu/forum_actions.xml @@ -7,7 +7,7 @@ android:id="@+id/action_forum_compose_post" android:icon="@drawable/forum_item_create_white" android:title="@string/forum_compose_post" - app:showAsAction="ifRoom"/> + app:showAsAction="always"/> <item android:id="@+id/action_forum_share" diff --git a/briar-android/res/menu/group_actions.xml b/briar-android/res/menu/group_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..8ebe58a1b345f9c4b68b9be15f2c98dc467aba40 --- /dev/null +++ b/briar-android/res/menu/group_actions.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_group_compose_message" + android:icon="@drawable/forum_item_create_white" + android:title="@string/groups_compose_message" + app:showAsAction="always"/> + + <item + android:id="@+id/action_group_member_list" + android:enabled="false" + android:icon="@drawable/ic_group_white" + android:title="@string/groups_member_list" + app:showAsAction="ifRoom"/> + + <item + android:id="@+id/action_group_invite" + android:enabled="false" + android:icon="@drawable/ic_add_white" + android:title="@string/groups_invite_members" + app:showAsAction="ifRoom"/> + + <item + android:id="@+id/action_group_leave" + android:enabled="false" + android:icon="@drawable/action_delete_white" + android:title="@string/groups_leave" + android:visible="false" + app:showAsAction="never"/> + + <item + android:id="@+id/action_group_dissolve" + android:enabled="false" + android:icon="@drawable/action_delete_white" + android:title="@string/groups_dissolve" + app:showAsAction="never"/> + +</menu> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 883dceee683a5640a16a3c49887b1589278b7aa5..b1652f5fb7c1ea51c54367e5d0137ac4bdc99d83 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -75,6 +75,7 @@ <string name="send">Send</string> <string name="no_data">No data</string> <string name="ellipsis">…</string> + <string name="text_too_long">The entered text is too long</string> <!-- Contacts and Private Conversations--> <string name="no_contacts">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.</string> @@ -155,6 +156,14 @@ <string name="groups_group_is_dissolved">This group is dissolved</string> <string name="groups_remove">Remove</string> <string name="groups_add_group_title">Add Private Group</string> + <string name="groups_no_messages">This group is empty.\n\nYou can use the pen icon at the top to compose the first message.</string> + <string name="groups_compose_message">Compose Message</string> + <string name="groups_message_sent">Message sent</string> + <string name="groups_message_received">Message received</string> + <string name="groups_member_list">Member List</string> + <string name="groups_invite_members">Invite Members</string> + <string name="groups_leave">Leave Group</string> + <string name="groups_dissolve">Dissolve Group</string> <!-- Forums --> <string name="no_forums">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.</string> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index d7cab786be65373e4c5efec208e210b539fdabb5..4e66c87aba6e1fbb891dc9e669d55348a8df974a 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 3a81fc60ffcd42e5e3a47ef2b6e8592222bf2f78..07cace566205890ee8a101bd04e8a9f651baa94b 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 9b7e6edbaf0b64aecdf3ab00eab40732aeb3d419..ceb3cd9e0b94be42eaa86acd8e2e24df8658b982 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 49dc574a858a2590b120bba32c74ae28a6291294..4c01808af7cba45255b7d091657471145c1d3112 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 ff1644b1c95052d0d28d3849d7bf3be891d6ee94..4c902459998fd68ce540933aa72f1295869b2c34 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 1cb6662fecc991a625582037eb89810b6c3d2eaa..81b41f99fed3dbc19467fd6ce6e56dc7c6067ee4 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<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> { 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; + ForumController forumController; - // Protected access for testing - protected NestedForumAdapter forumAdapter; - - private BriarRecyclerView recyclerView; - private TextInputView textInput; + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } - private volatile GroupId groupId = null; + @Override + protected ThreadListController<Forum, ForumItem, ForumPostHeader> getController() { + return forumController; + } @Override - public void onCreate(final Bundle state) { + public void onCreate(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<List<ForumEntry>, DbException>( - this) { - @Override - public void onResultUi(List<ForumEntry> result) { - Forum forum = forumController.getForum(); - if (forum != null) setTitle(forum.getName()); - List<ForumEntry> 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(); - } - }); + String groupName = i.getStringExtra(GROUP_NAME); + if (groupName != null) setTitle(groupName); + else loadNamedGroup(); } @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - textInput.setVisibility( - savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ? - VISIBLE : GONE); + protected void onNamedGroupLoaded(Forum forum) { + setTitle(forum.getName()); } - @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()); - } + @LayoutRes + protected int getLayout() { + return R.layout.activity_forum; } @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(); + 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<ForumEntry, DbException> resultHandler = - new UiResultExceptionHandler<ForumEntry, DbException>(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<Boolean>( - 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<ForumEntry, DbException>(this) { + private void deleteNamedGroup() { + forumController.deleteNamedGroup( + new UiResultExceptionHandler<Void, DbException>( + 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 ad08dbc4dd534a918736c43486fcfc6fa44917ce..9b1fba5641b032c862b4fb9ba1ffcf254b773fd6 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<List<ForumEntry>, DbException> resultHandler); - - @Nullable - Forum getForum(); - - void loadPost(ForumPostHeader header, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - void unsubscribe(ResultHandler<Boolean> resultHandler); - - void entryRead(ForumEntry forumEntry); - - void entriesRead(Collection<ForumEntry> messageIds); - - void createPost(byte[] body, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - void createPost(byte[] body, MessageId parentId, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - interface ForumPostListener extends DestroyableContext { - - @UiThread - void onForumPostReceived(ForumPostHeader header); - @UiThread - void onForumRemoved(); - } +public interface ForumController + extends ThreadListController<Forum, ForumItem, ForumPostHeader> { } diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java index b6a8ddce527efa65d6ff3585fb59e1716b8e5ef8..fa62f1b292740855de64345b1b2e9866c0404dca 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<Forum, ForumItem, ForumPostHeader, ForumPost> + 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<MessageId, byte[]> 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"); - } - - /** - * This should only be run from the DbThread. - * - * @throws DbException - */ - private Collection<ForumPostHeader> loadHeaders() throws DbException { - if (forum == null) - throw new RuntimeException("Forum has not been initialized"); - - // Get Headers - long now = System.currentTimeMillis(); - Collection<ForumPostHeader> headers = - forumManager.getPostHeaders(forum.getId()); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading headers took " + duration + " ms"); - return headers; - } - - /** - * This should only be run from the DbThread. - * - * @throws DbException - */ - private void loadBodies(Collection<ForumPostHeader> headers) - 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<ForumEntry> buildForumEntries( - Collection<ForumPostHeader> headers) { - List<ForumEntry> 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<ForumPostHeader> headers) { - for (ForumPostHeader h : headers) { - updateNewestTimestamp(h.getTimestamp()); - } - } - @Override - public void loadForum(final GroupId groupId, - final ResultExceptionHandler<List<ForumEntry>, 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<ForumPostHeader> 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 Forum loadNamedGroup() throws DbException { + return forumManager.getForum(getGroupId()); } @Override - @Nullable - public Forum getForum() { - return forum; + protected Collection<ForumPostHeader> loadHeaders() throws DbException { + return forumManager.getPostHeaders(getGroupId()); } @Override - public void loadPost(final ForumPostHeader header, - final ResultExceptionHandler<ForumEntry, DbException> 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); - } - } - }); + protected String loadMessageBody(MessageId id) throws DbException { + return StringUtils.fromUtf8(forumManager.getPostBody(id)); } @Override - public void unsubscribe(final ResultHandler<Boolean> 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); - } - } - }); + protected void markRead(MessageId id) throws DbException { + forumManager.setReadFlag(getGroupId(), id, true); } @Override - public void entryRead(ForumEntry forumEntry) { - entriesRead(Collections.singletonList(forumEntry)); + protected long getLatestTimestamp() throws DbException { + GroupCount count = forumManager.getGroupCount(getGroupId()); + return count.getLatestMsgTime(); } @Override - public void entriesRead(final Collection<ForumEntry> 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); - } - } - }); + protected ForumPost createLocalMessage(String body, long timestamp, + @Nullable MessageId parentId, LocalAuthor author) { + return forumManager + .createLocalPost(getGroupId(), body, timestamp, parentId, + author); } @Override - public void createPost(byte[] body, - ResultExceptionHandler<ForumEntry, DbException> resultHandler) { - createPost(body, null, resultHandler); + protected ForumPostHeader addLocalMessage(ForumPost p) + throws DbException { + return forumManager.addLocalPost(p); } @Override - public void createPost(final byte[] body, final MessageId parentId, - final ResultExceptionHandler<ForumEntry, DbException> 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); - } - }); + protected void deleteNamedGroup(Forum forum) throws DbException { + forumManager.removeForum(forum); } - private void storePost(final ForumPost p, - final ResultExceptionHandler<ForumEntry, DbException> 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); - } - } - }); + @Override + protected ForumItem buildItem(ForumPostHeader header, String body) { + return new ForumItem(header, body); } - 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 0000000000000000000000000000000000000000..80f28e2d3aaf508e8d0581db1e23cb3d3051efbc --- /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 ca237eb35e8847002c3aec56dd3894a0cd285911..040b81662b884d6394d67fdcdc8b16ea31ea489d 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<ForumListItem, ForumListAdapter.ForumViewHolder> { @@ -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 f1e2f5de2637a7f53bea3913b603aade60769df9..f690df90afea7c5222b737d08afd3cfd9372f349 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<ForumItem> { -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<NestedForumAdapter.NestedForumHolder> { - - private static final int UNDEFINED = -1; - - private final NestedTreeList<ForumEntry> forumEntries = - new NestedTreeList<>(); - private final Map<ForumEntry, ValueAnimator> 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<ForumItem> listener, LinearLayoutManager layoutManager) { - this.ctx = ctx; - this.listener = listener; - this.layoutManager = layoutManager; - } - - ForumEntry getReplyEntry() { - return replyEntry; - } - - void setEntries(List<ForumEntry> 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<Integer> getSubTreeIndexes(int pos, int levelLimit) { - List<Integer> 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<Integer> 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<Integer> 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 0000000000000000000000000000000000000000..b73558ff5d668c1adf37f0e8e69a9f971411967c --- /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<ForumItem> { + + 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 0000000000000000000000000000000000000000..71a4865b516bf1e14e8b5d1ef9c6317179092c2a --- /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<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> { + + @Inject + GroupController controller; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + @Override + protected ThreadListController<PrivateGroup, GroupMessageItem, GroupMessageHeader> 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 0000000000000000000000000000000000000000..0292f22787bba631e676d6eed41945ca4bbf865e --- /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<PrivateGroup, GroupMessageItem, GroupMessageHeader> { + +} 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 0000000000000000000000000000000000000000..fb0cd8ac59136aa84e4fc01910f5c5ecffb5072a --- /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<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage> + 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<GroupMessageHeader> 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 0000000000000000000000000000000000000000..19ee14adce62158808c7f795e9eb505ec889cd2f --- /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<GroupMessageItem> { + + public GroupMessageAdapter(ThreadItemListener<GroupMessageItem> 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 0000000000000000000000000000000000000000..ee84c1fb00b756fc07098cf0b1fefcaab9399f41 --- /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 0000000000000000000000000000000000000000..11825b8056e6ee3002338956a6599c2a4b949b21 --- /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<GroupMessageItem> { + + 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 20210a486c9a903cd63d1f686d92a390a0d89716..bae48022a7e8599cf4bbeef00f069ee88ed7651c 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<GroupItem, GroupViewHolder> { @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 9ad7536eb70f6544a47282797050577ae4be63b7..9db84cdcadfa143748c975dce863815075ba0d1c 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 12b956df4f688597947569e31fb25df1b5f0302a..c7d59dd1ff12611509016cd1d77a99d17dffa4dd 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 d190a9f6dec35496cdd6dae2b9dd71a8ae80bf96..aec3c58beb0f33dae8a671c55755821cfc6aa6cd 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 49e1ee32f59cb3d0e95c6e0bc3622213e60afcbb..a5a22075995246e5859671bc7b64995324bb75ea 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 0000000000000000000000000000000000000000..129e5348593003ac3e4783b2b43324455ea4ad58 --- /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<I extends ThreadItem> + extends RecyclerView.Adapter<ThreadItemViewHolder<I>> { + + static final int UNDEFINED = -1; + + private final NestedTreeList<I> items = + new NestedTreeList<>(); + private final Map<I, ValueAnimator> animatingItems = + new HashMap<>(); + // highlight not dependant on time + private I replyItem; + // temporary highlight + private I addedEntry; + + private final ThreadItemListener<I> listener; + private final LinearLayoutManager layoutManager; + + public ThreadItemAdapter(ThreadItemListener<I> listener, + LinearLayoutManager layoutManager) { + this.listener = listener; + this.layoutManager = layoutManager; + } + + @Override + public void onBindViewHolder(ThreadItemViewHolder<I> 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<I> 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<Integer> getSubTreeIndexes(int pos, int levelLimit) { + List<Integer> 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<Integer> 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<Integer> 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<I> { + 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 0000000000000000000000000000000000000000..0b5a5ddc5b74cc34bee5bb6d63b2534ce2aa0227 --- /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<I extends ThreadItem> + 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<I> adapter, + final ThreadItemListener<I> 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<I> 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 0000000000000000000000000000000000000000..889399525de6ab9a880511dd43789533d65b835d --- /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<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>> + extends BriarActivity + implements ThreadListListener<H>, TextInputListener, + ThreadItemListener<I> { + + 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<G, I, H> 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<G, DbException>(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<Collection<I>, DbException>( + this) { + @Override + public void onResultUi(Collection<I> 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<I, DbException> handler = + new UiResultExceptionHandler<I, DbException>(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<I, DbException>(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 0000000000000000000000000000000000000000..a7731d8d48ed2046f39c962e8781b86c4c9c396d --- /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<G extends NamedGroup, I extends ThreadItem, H extends PostHeader> + extends ActivityLifecycleController { + + void setGroupId(GroupId groupId); + + void loadNamedGroup(ResultExceptionHandler<G, DbException> handler); + + void loadItem(H header, ResultExceptionHandler<I, DbException> handler); + + void loadItems(ResultExceptionHandler<Collection<I>, DbException> handler); + + void markItemRead(I item); + + void markItemsRead(Collection<I> items); + + void createAndStoreMessage(String body, @Nullable MessageId parentId, + ResultExceptionHandler<I, DbException> handler); + + void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler); + + interface ThreadListListener<H> 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 0000000000000000000000000000000000000000..ac968758fc0dd462eeb2770d6b5bf932a227787c --- /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<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends BaseMessage> + extends DbControllerImpl + implements ThreadListController<G, I, H>, 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<MessageId, String> bodyCache = + new ConcurrentHashMap<>(); + + private volatile GroupId groupId; + + protected ThreadListListener<H> 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<H>) 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<G, DbException> 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<Collection<I>, DbException> handler) { + checkGroupId(); + runOnDbThread(new Runnable() { + @Override + public void run() { + LOG.info("Loading items..."); + try { + // Load headers + long now = System.currentTimeMillis(); + Collection<H> 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<H> loadHeaders() throws DbException; + + @DatabaseExecutor + protected abstract String loadMessageBody(MessageId id) throws DbException; + + @Override + public void loadItem(final H header, + final ResultExceptionHandler<I, DbException> 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<I> 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<I, DbException> 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<I, DbException> 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<I, DbException> 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<Void, DbException> 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<I> buildItems(Collection<H> headers) { + List<I> 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 3e5431ac9483ee409f930e150327a8957b422f4a..e6290e9716af40673203e0cfba28027469a8b249 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<UiResultExceptionHandler<List<ForumEntry>, DbException>> + private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumItem>, DbException>> rc; @Before @@ -93,14 +92,14 @@ public class ForumActivityTest { .withIntent(intent).create().resume().get(); } - private List<ForumEntry> getDummyData() { - ForumEntry[] forumEntries = new ForumEntry[6]; + private List<ForumItem> 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<ForumEntry> dummyData = getDummyData(); - verify(mc, times(1)) - .loadForum(Mockito.any(GroupId.class), rc.capture()); + List<ForumItem> 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 8acbf3b8df67f46659570d38ffb60ff5daf697c8..806fee299bf73a5bb23a034dfd8541765b1b29d3 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 5de781e7de8875d3ee305466b59432594fb7cd6f..153eb6e27ccef2ccf6ca0d73d6aae7935f9287df 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 fe8a282f5b7fb50770be89e170dd3bf889d6171d..f101106e38c1d836e3f1c0d1366e867bdcebb924 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 0000000000000000000000000000000000000000..f4b95fdc5cd5a8ced6eb8f18fe9f1b2540d4ce12 --- /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 cdaa091b98960d65cce7d150e0994a785ab8e28a..f73cd049afff86ea54c15bb905b9dd9bb4ccdd0f 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 8c35d05e368420f38e6356b77728dc3d3e2f39a6..7561fc9636ab1a0544ee8d4212c00a47d3067c50 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 aef48c78c4e695b14ff4a8fe0e4ea8af5fafc8a5..bbc23e9db045b12a25ae05cfaf5527c89a756e8a 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 e05edc8831b3538458c44175e8a005d59d56d878..1d6aa9e76d9379b1aaa78ba28f34a849153d25dc 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 8ffe6c6a46c753ca4feea98c1c9541319151059e..22457de2eba5053e7fc189637bf1f22360061bf5 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 f6d5862c78da7a15f9f81adaf41d62c501d6b551..310611361b960acc44ef6b5e9f6d30207f488360 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 492eabc866df02bc8be162aa02c79165ab6f717f..6a27552fa286c13c9c7e12c683c7af44707e18db 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 a7fbd0cb12a5421e04d3c1c37eacebb09bf43c19..956a42344649b2d88cdbf0924d4c5ab1cda0986d 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 77a7712e57ae0b4769e0a4ccee6c560981a08cce..8d1511f7d8a8e2665fe4c5781d9c1e95b91a4542 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<RemoveForumHook> 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<RemoveForumHook>(); } @@ -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 a6c0b226aea4109724a72ebb36ae02ca6299a937..7c0d1e46ddc88a7fefb999340f96185cc99d22be 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 429fd56b5f34383f80ae8f308543581cbdc46566..7389f2fd691718ee4c84ae88b8f505df97d79b98 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 3c9f5814bc580c50a3bb1822855000993d613e82..6f40bd9c6c9c069c4e9de80d9cbfa7c887626b53 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 f8a97a6d635bd403c59eb9ea659decdde3c68f5a..ec43eebf75e42e3d8bb480eee52f55b2e3b098dd 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; + } }