mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-22 15:49:53 +01:00
Merge branch '663-implement-ux-for-displaying-message-threads-in-private-groups' into 'master'
Private Group Threaded Conversation This MR refactors the forum activity, its controller, its adapter and view holder so *most* of the code can be re-used for private groups by making heavy use of generics. The refactoring has 1383 additions and 1087 deletions, so just grows the code-base slightly and adding the private group conversation just takes an additional 400 lines. The MR also includes one commit that moves post/message creation more into clients, so the UI doesn't need to keep track of timestamps. This commit can of course be split out into a separate MR if desired. Closes #662, #663 See merge request !350
This commit is contained in:
@@ -3,16 +3,14 @@ package org.briarproject;
|
|||||||
import net.jodah.concurrentunit.Waiter;
|
import net.jodah.concurrentunit.Waiter;
|
||||||
|
|
||||||
import org.briarproject.api.Bytes;
|
import org.briarproject.api.Bytes;
|
||||||
import org.briarproject.api.clients.MessageQueueManager;
|
|
||||||
import org.briarproject.api.clients.ContactGroupFactory;
|
import org.briarproject.api.clients.ContactGroupFactory;
|
||||||
|
import org.briarproject.api.clients.MessageQueueManager;
|
||||||
import org.briarproject.api.clients.SessionId;
|
import org.briarproject.api.clients.SessionId;
|
||||||
import org.briarproject.api.contact.Contact;
|
import org.briarproject.api.contact.Contact;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
import org.briarproject.api.contact.ContactManager;
|
import org.briarproject.api.contact.ContactManager;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
import org.briarproject.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.api.crypto.KeyPair;
|
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.crypto.SecretKey;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.db.DatabaseComponent;
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
@@ -820,12 +818,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
|
|||||||
|
|
||||||
// sharer posts into the forum
|
// sharer posts into the forum
|
||||||
long time = clock.currentTimeMillis();
|
long time = clock.currentTimeMillis();
|
||||||
byte[] body = TestUtils.getRandomBytes(42);
|
String body = TestUtils.getRandomString(42);
|
||||||
KeyParser keyParser = cryptoComponent.getSignatureKeyParser();
|
|
||||||
PrivateKey key = keyParser.parsePrivateKey(author0.getPrivateKey());
|
|
||||||
ForumPost p = forumPostFactory
|
ForumPost p = forumPostFactory
|
||||||
.createPseudonymousPost(forum0.getId(), time, null, author0,
|
.createPseudonymousPost(forum0.getId(), time, null, author0,
|
||||||
"text/plain", body, key);
|
body);
|
||||||
forumManager0.addLocalPost(p);
|
forumManager0.addLocalPost(p);
|
||||||
|
|
||||||
// sync forum post
|
// sync forum post
|
||||||
@@ -841,11 +837,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
|
|||||||
|
|
||||||
// now invitee creates a post
|
// now invitee creates a post
|
||||||
time = clock.currentTimeMillis();
|
time = clock.currentTimeMillis();
|
||||||
body = TestUtils.getRandomBytes(42);
|
body = TestUtils.getRandomString(42);
|
||||||
key = keyParser.parsePrivateKey(author1.getPrivateKey());
|
|
||||||
p = forumPostFactory
|
p = forumPostFactory
|
||||||
.createPseudonymousPost(forum0.getId(), time, null, author1,
|
.createPseudonymousPost(forum0.getId(), time, null, author1,
|
||||||
"text/plain", body, key);
|
body);
|
||||||
forumManager1.addLocalPost(p);
|
forumManager1.addLocalPost(p);
|
||||||
|
|
||||||
// sync forum post
|
// sync forum post
|
||||||
@@ -886,11 +881,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
|
|||||||
|
|
||||||
// now invitee creates a post
|
// now invitee creates a post
|
||||||
time = clock.currentTimeMillis();
|
time = clock.currentTimeMillis();
|
||||||
body = TestUtils.getRandomBytes(42);
|
body = TestUtils.getRandomString(42);
|
||||||
key = keyParser.parsePrivateKey(author1.getPrivateKey());
|
|
||||||
p = forumPostFactory
|
p = forumPostFactory
|
||||||
.createPseudonymousPost(forum0.getId(), time, null, author1,
|
.createPseudonymousPost(forum0.getId(), time, null, author1,
|
||||||
"text/plain", body, key);
|
body);
|
||||||
forumManager1.addLocalPost(p);
|
forumManager1.addLocalPost(p);
|
||||||
|
|
||||||
// sync forum post
|
// sync forum post
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import org.briarproject.api.crypto.PrivateKey;
|
|||||||
import org.briarproject.api.forum.ForumConstants;
|
import org.briarproject.api.forum.ForumConstants;
|
||||||
import org.briarproject.api.forum.ForumPost;
|
import org.briarproject.api.forum.ForumPost;
|
||||||
import org.briarproject.api.forum.ForumPostFactory;
|
import org.briarproject.api.forum.ForumPostFactory;
|
||||||
import org.briarproject.api.identity.Author;
|
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.messaging.MessagingConstants;
|
import org.briarproject.api.messaging.MessagingConstants;
|
||||||
import org.briarproject.api.messaging.PrivateMessage;
|
import org.briarproject.api.messaging.PrivateMessage;
|
||||||
import org.briarproject.api.messaging.PrivateMessageFactory;
|
import org.briarproject.api.messaging.PrivateMessageFactory;
|
||||||
@@ -68,17 +68,17 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
|
|||||||
String authorName = TestUtils.getRandomString(
|
String authorName = TestUtils.getRandomString(
|
||||||
MAX_AUTHOR_NAME_LENGTH);
|
MAX_AUTHOR_NAME_LENGTH);
|
||||||
byte[] authorPublic = new byte[MAX_PUBLIC_KEY_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
|
// Create a maximum-length forum post
|
||||||
GroupId groupId = new GroupId(TestUtils.getRandomId());
|
GroupId groupId = new GroupId(TestUtils.getRandomId());
|
||||||
long timestamp = Long.MAX_VALUE;
|
long timestamp = Long.MAX_VALUE;
|
||||||
MessageId parent = new MessageId(TestUtils.getRandomId());
|
MessageId parent = new MessageId(TestUtils.getRandomId());
|
||||||
String contentType = TestUtils.getRandomString(
|
String body = TestUtils.getRandomString(MAX_FORUM_POST_BODY_LENGTH);
|
||||||
ForumConstants.MAX_CONTENT_TYPE_LENGTH);
|
|
||||||
byte[] body = new byte[MAX_FORUM_POST_BODY_LENGTH];
|
|
||||||
PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate();
|
|
||||||
ForumPost post = forumPostFactory.createPseudonymousPost(groupId,
|
ForumPost post = forumPostFactory.createPseudonymousPost(groupId,
|
||||||
timestamp, parent, author, contentType, body, privateKey);
|
timestamp, parent, author, body);
|
||||||
// Check the size of the serialised message
|
// Check the size of the serialised message
|
||||||
int length = post.getMessage().getRaw().length;
|
int length = post.getMessage().getRaw().length;
|
||||||
assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH
|
assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH
|
||||||
|
|||||||
@@ -100,6 +100,17 @@
|
|||||||
/>
|
/>
|
||||||
</activity>
|
</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
|
<activity
|
||||||
android:name=".android.sharing.InvitationsForumActivity"
|
android:name=".android.sharing.InvitationsForumActivity"
|
||||||
android:label="@string/forum_invitations_title"
|
android:label="@string/forum_invitations_title"
|
||||||
|
|||||||
9
briar-android/res/drawable/ic_group_white.xml
Normal file
9
briar-android/res/drawable/ic_group_white.xml
Normal file
@@ -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>
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<org.briarproject.android.view.BriarRecyclerView
|
<org.briarproject.android.view.BriarRecyclerView
|
||||||
android:id="@+id/forum_discussion_list"
|
android:id="@+id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
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
|
<org.briarproject.android.view.TextInputView
|
||||||
android:id="@+id/text_input_container"
|
android:id="@+id/text_input_container"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
android:id="@+id/action_forum_compose_post"
|
android:id="@+id/action_forum_compose_post"
|
||||||
android:icon="@drawable/forum_item_create_white"
|
android:icon="@drawable/forum_item_create_white"
|
||||||
android:title="@string/forum_compose_post"
|
android:title="@string/forum_compose_post"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="always"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_forum_share"
|
android:id="@+id/action_forum_share"
|
||||||
|
|||||||
41
briar-android/res/menu/group_actions.xml
Normal file
41
briar-android/res/menu/group_actions.xml
Normal file
@@ -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>
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
<string name="send">Send</string>
|
<string name="send">Send</string>
|
||||||
<string name="no_data">No data</string>
|
<string name="no_data">No data</string>
|
||||||
<string name="ellipsis">…</string>
|
<string name="ellipsis">…</string>
|
||||||
|
<string name="text_too_long">The entered text is too long</string>
|
||||||
|
|
||||||
<!-- Contacts and Private Conversations-->
|
<!-- 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>
|
<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_group_is_dissolved">This group is dissolved</string>
|
||||||
<string name="groups_remove">Remove</string>
|
<string name="groups_remove">Remove</string>
|
||||||
<string name="groups_add_group_title">Add Private Group</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 -->
|
<!-- 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>
|
<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>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import org.briarproject.android.keyagreement.KeyAgreementActivity;
|
|||||||
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
|
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
|
||||||
import org.briarproject.android.panic.PanicPreferencesActivity;
|
import org.briarproject.android.panic.PanicPreferencesActivity;
|
||||||
import org.briarproject.android.panic.PanicResponderActivity;
|
import org.briarproject.android.panic.PanicResponderActivity;
|
||||||
|
import org.briarproject.android.privategroup.conversation.GroupActivity;
|
||||||
import org.briarproject.android.privategroup.list.GroupListFragment;
|
import org.briarproject.android.privategroup.list.GroupListFragment;
|
||||||
import org.briarproject.android.sharing.ContactSelectorFragment;
|
import org.briarproject.android.sharing.ContactSelectorFragment;
|
||||||
import org.briarproject.android.sharing.InvitationsBlogActivity;
|
import org.briarproject.android.sharing.InvitationsBlogActivity;
|
||||||
@@ -72,6 +73,8 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(InvitationsBlogActivity activity);
|
void inject(InvitationsBlogActivity activity);
|
||||||
|
|
||||||
|
void inject(GroupActivity activity);
|
||||||
|
|
||||||
void inject(CreateForumActivity activity);
|
void inject(CreateForumActivity activity);
|
||||||
|
|
||||||
void inject(ShareForumActivity activity);
|
void inject(ShareForumActivity activity);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import org.briarproject.android.controller.SetupController;
|
|||||||
import org.briarproject.android.controller.SetupControllerImpl;
|
import org.briarproject.android.controller.SetupControllerImpl;
|
||||||
import org.briarproject.android.forum.ForumController;
|
import org.briarproject.android.forum.ForumController;
|
||||||
import org.briarproject.android.forum.ForumControllerImpl;
|
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.GroupListController;
|
||||||
import org.briarproject.android.privategroup.list.GroupListControllerImpl;
|
import org.briarproject.android.privategroup.list.GroupListControllerImpl;
|
||||||
|
|
||||||
@@ -99,6 +101,13 @@ public class ActivityModule {
|
|||||||
return groupListController;
|
return groupListController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ActivityScope
|
||||||
|
@Provides
|
||||||
|
protected GroupController provideGroupController(
|
||||||
|
GroupControllerImpl groupController) {
|
||||||
|
return groupController;
|
||||||
|
}
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Provides
|
@Provides
|
||||||
protected ForumController provideForumController(
|
protected ForumController provideForumController(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.briarproject.api.db.DatabaseExecutor;
|
|||||||
import org.briarproject.api.event.EventBus;
|
import org.briarproject.api.event.EventBus;
|
||||||
import org.briarproject.api.feed.FeedManager;
|
import org.briarproject.api.feed.FeedManager;
|
||||||
import org.briarproject.api.forum.ForumManager;
|
import org.briarproject.api.forum.ForumManager;
|
||||||
import org.briarproject.api.forum.ForumPostFactory;
|
|
||||||
import org.briarproject.api.forum.ForumSharingManager;
|
import org.briarproject.api.forum.ForumSharingManager;
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
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.plugins.PluginManager;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.settings.SettingsManager;
|
import org.briarproject.api.settings.SettingsManager;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
import org.briarproject.plugins.AndroidPluginsModule;
|
import org.briarproject.plugins.AndroidPluginsModule;
|
||||||
import org.briarproject.system.AndroidSystemModule;
|
import org.briarproject.system.AndroidSystemModule;
|
||||||
|
|
||||||
@@ -102,8 +102,6 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
|||||||
|
|
||||||
BlogSharingManager blogSharingManager();
|
BlogSharingManager blogSharingManager();
|
||||||
|
|
||||||
ForumPostFactory forumPostFactory();
|
|
||||||
|
|
||||||
BlogManager blogManager();
|
BlogManager blogManager();
|
||||||
|
|
||||||
BlogPostFactory blogPostFactory();
|
BlogPostFactory blogPostFactory();
|
||||||
@@ -124,6 +122,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
|||||||
|
|
||||||
FeedManager feedManager();
|
FeedManager feedManager();
|
||||||
|
|
||||||
|
Clock clock();
|
||||||
|
|
||||||
@IoExecutor
|
@IoExecutor
|
||||||
Executor ioExecutor();
|
Executor ioExecutor();
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public abstract class BriarActivity extends BaseActivity {
|
|||||||
"briar.LOCAL_AUTHOR_HANDLE";
|
"briar.LOCAL_AUTHOR_HANDLE";
|
||||||
public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED";
|
public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED";
|
||||||
public static final String GROUP_ID = "briar.GROUP_ID";
|
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;
|
public static final int REQUEST_PASSWORD = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import static android.view.View.VISIBLE;
|
|||||||
import static android.widget.Toast.LENGTH_LONG;
|
import static android.widget.Toast.LENGTH_LONG;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
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;
|
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
|
||||||
|
|
||||||
public class CreateForumActivity extends BriarActivity
|
public class CreateForumActivity extends BriarActivity
|
||||||
@@ -150,7 +149,7 @@ public class CreateForumActivity extends BriarActivity
|
|||||||
Intent i = new Intent(CreateForumActivity.this,
|
Intent i = new Intent(CreateForumActivity.this,
|
||||||
ForumActivity.class);
|
ForumActivity.class);
|
||||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||||
i.putExtra(FORUM_NAME, f.getName());
|
i.putExtra(GROUP_NAME, f.getName());
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
Toast.makeText(CreateForumActivity.this,
|
Toast.makeText(CreateForumActivity.this,
|
||||||
R.string.forum_created_toast, LENGTH_LONG).show();
|
R.string.forum_created_toast, LENGTH_LONG).show();
|
||||||
|
|||||||
@@ -3,155 +3,79 @@ package org.briarproject.android.forum;
|
|||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.annotation.StringRes;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.ActivityComponent;
|
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.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.ShareForumActivity;
|
||||||
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
||||||
import org.briarproject.android.view.BriarRecyclerView;
|
import org.briarproject.android.threaded.ThreadListActivity;
|
||||||
import org.briarproject.android.view.TextInputView;
|
import org.briarproject.android.threaded.ThreadListController;
|
||||||
import org.briarproject.android.view.TextInputView.TextInputListener;
|
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
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 javax.inject.Inject;
|
||||||
|
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
||||||
import static android.view.View.GONE;
|
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
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
|
public class ForumActivity extends
|
||||||
ForumPostListener, TextInputListener, OnNestedForumListener {
|
ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
|
||||||
|
|
||||||
static final String FORUM_NAME = "briar.FORUM_NAME";
|
|
||||||
|
|
||||||
private static final int REQUEST_FORUM_SHARED = 3;
|
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
|
@Inject
|
||||||
AndroidNotificationManager notificationManager;
|
ForumController forumController;
|
||||||
|
|
||||||
@Inject
|
|
||||||
protected ForumController forumController;
|
|
||||||
|
|
||||||
// Protected access for testing
|
|
||||||
protected NestedForumAdapter forumAdapter;
|
|
||||||
|
|
||||||
private BriarRecyclerView recyclerView;
|
|
||||||
private TextInputView textInput;
|
|
||||||
|
|
||||||
private volatile GroupId groupId = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle state) {
|
|
||||||
super.onCreate(state);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_forum);
|
|
||||||
|
|
||||||
Intent i = getIntent();
|
|
||||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
|
||||||
if (b == null) throw new IllegalStateException();
|
|
||||||
groupId = new GroupId(b);
|
|
||||||
String forumName = i.getStringExtra(FORUM_NAME);
|
|
||||||
if (forumName != null) setTitle(forumName);
|
|
||||||
|
|
||||||
textInput = (TextInputView) findViewById(R.id.text_input_container);
|
|
||||||
textInput.setVisibility(GONE);
|
|
||||||
textInput.setListener(this);
|
|
||||||
recyclerView =
|
|
||||||
(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
|
|
||||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
|
|
||||||
recyclerView.setLayoutManager(linearLayoutManager);
|
|
||||||
forumAdapter = new NestedForumAdapter(this, this, linearLayoutManager);
|
|
||||||
recyclerView.setAdapter(forumAdapter);
|
|
||||||
|
|
||||||
forumController.loadForum(groupId,
|
|
||||||
new UiResultExceptionHandler<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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
|
||||||
textInput.setVisibility(
|
|
||||||
savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
|
|
||||||
VISIBLE : GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putBoolean(KEY_INPUT_VISIBILITY,
|
|
||||||
textInput.getVisibility() == VISIBLE);
|
|
||||||
ForumEntry replyEntry = forumAdapter.getReplyEntry();
|
|
||||||
if (replyEntry != null) {
|
|
||||||
outState.putByteArray(KEY_REPLY_ID,
|
|
||||||
replyEntry.getMessageId().getBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void injectActivity(ActivityComponent component) {
|
public void injectActivity(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displaySnackbarShort(int stringId) {
|
@Override
|
||||||
Snackbar snackbar =
|
protected ThreadListController<Forum, ForumItem, ForumPostHeader> getController() {
|
||||||
Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
|
return forumController;
|
||||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
}
|
||||||
snackbar.show();
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle state) {
|
||||||
|
super.onCreate(state);
|
||||||
|
|
||||||
|
Intent i = getIntent();
|
||||||
|
String groupName = i.getStringExtra(GROUP_NAME);
|
||||||
|
if (groupName != null) setTitle(groupName);
|
||||||
|
else loadNamedGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNamedGroupLoaded(Forum forum) {
|
||||||
|
setTitle(forum.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@LayoutRes
|
||||||
|
protected int getLayout() {
|
||||||
|
return R.layout.activity_forum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected NestedForumAdapter createAdapter(
|
||||||
|
LinearLayoutManager layoutManager) {
|
||||||
|
return new NestedForumAdapter(this, layoutManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -172,34 +96,10 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
return super.onCreateOptionsMenu(menu);
|
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
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
ActivityOptionsCompat options = ActivityOptionsCompat
|
ActivityOptionsCompat options =
|
||||||
.makeCustomAnimation(this, android.R.anim.slide_in_left,
|
makeCustomAnimation(this, android.R.anim.slide_in_left,
|
||||||
android.R.anim.slide_out_right);
|
android.R.anim.slide_out_right);
|
||||||
// Handle presses on the action bar items
|
// Handle presses on the action bar items
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
@@ -229,69 +129,29 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
protected int getMaxBodyLength() {
|
||||||
super.onResume();
|
return MAX_FORUM_POST_BODY_LENGTH;
|
||||||
notificationManager.blockNotification(groupId);
|
|
||||||
notificationManager.clearForumPostNotification(groupId);
|
|
||||||
recyclerView.startPeriodicUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
@StringRes
|
||||||
super.onPause();
|
protected int getItemPostedString() {
|
||||||
notificationManager.unblockNotification(groupId);
|
return R.string.forum_new_entry_posted;
|
||||||
recyclerView.stopPeriodicUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(String text) {
|
@StringRes
|
||||||
if (text.trim().length() == 0)
|
protected int getItemReceivedString() {
|
||||||
return;
|
return R.string.forum_new_entry_received;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showUnsubscribeDialog() {
|
private void showUnsubscribeDialog() {
|
||||||
DialogInterface.OnClickListener okListener =
|
DialogInterface.OnClickListener okListener =
|
||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(final DialogInterface dialog,
|
||||||
forumController.unsubscribe(
|
int which) {
|
||||||
new UiResultHandler<Boolean>(
|
deleteNamedGroup();
|
||||||
ForumActivity.this) {
|
|
||||||
@Override
|
|
||||||
public void onResultUi(Boolean result) {
|
|
||||||
if (result) {
|
|
||||||
Toast.makeText(ForumActivity.this,
|
|
||||||
R.string.forum_left_toast,
|
|
||||||
LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
AlertDialog.Builder builder =
|
AlertDialog.Builder builder =
|
||||||
@@ -299,66 +159,30 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
R.style.BriarDialogTheme);
|
R.style.BriarDialogTheme);
|
||||||
builder.setTitle(getString(R.string.dialog_title_leave_forum));
|
builder.setTitle(getString(R.string.dialog_title_leave_forum));
|
||||||
builder.setMessage(getString(R.string.dialog_message_leave_forum));
|
builder.setMessage(getString(R.string.dialog_message_leave_forum));
|
||||||
builder.setPositiveButton(R.string.dialog_button_leave, okListener);
|
builder.setNegativeButton(R.string.dialog_button_leave, okListener);
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setPositiveButton(R.string.cancel, null);
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void deleteNamedGroup() {
|
||||||
public void onEntryVisible(ForumEntry forumEntry) {
|
forumController.deleteNamedGroup(
|
||||||
if (!forumEntry.isRead()) {
|
new UiResultExceptionHandler<Void, DbException>(
|
||||||
forumEntry.setRead(true);
|
ForumActivity.this) {
|
||||||
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) {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResultUi(final ForumEntry result) {
|
public void onResultUi(Void v) {
|
||||||
onForumEntryAdded(result, false);
|
Toast.makeText(ForumActivity.this,
|
||||||
|
R.string.forum_left_toast,
|
||||||
|
LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(
|
||||||
// TODO add proper exception handling
|
DbException exception) {
|
||||||
|
// TODO proper error handling
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onForumRemoved() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,10 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import org.briarproject.android.threaded.ThreadListController;
|
||||||
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.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.sync.GroupId;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
public interface ForumController
|
||||||
import java.util.List;
|
extends ThreadListController<Forum, ForumItem, ForumPostHeader> {
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,388 +1,126 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.android.controller.DbControllerImpl;
|
import org.briarproject.android.api.AndroidNotificationManager;
|
||||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
||||||
import org.briarproject.android.controller.handler.ResultHandler;
|
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
||||||
import org.briarproject.api.FormatException;
|
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
|
||||||
import org.briarproject.api.crypto.CryptoExecutor;
|
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.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.event.Event;
|
import org.briarproject.api.event.Event;
|
||||||
import org.briarproject.api.event.EventBus;
|
import org.briarproject.api.event.EventBus;
|
||||||
import org.briarproject.api.event.EventListener;
|
|
||||||
import org.briarproject.api.event.ForumPostReceivedEvent;
|
import org.briarproject.api.event.ForumPostReceivedEvent;
|
||||||
import org.briarproject.api.event.GroupRemovedEvent;
|
|
||||||
import org.briarproject.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
import org.briarproject.api.forum.ForumManager;
|
import org.briarproject.api.forum.ForumManager;
|
||||||
import org.briarproject.api.forum.ForumPost;
|
import org.briarproject.api.forum.ForumPost;
|
||||||
import org.briarproject.api.forum.ForumPostFactory;
|
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.api.sync.GroupId;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
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.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.INFO;
|
public class ForumControllerImpl
|
||||||
import static java.util.logging.Level.WARNING;
|
extends ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
|
||||||
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
implements ForumController {
|
||||||
|
|
||||||
public class ForumControllerImpl extends DbControllerImpl
|
|
||||||
implements ForumController, EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(ForumControllerImpl.class.getName());
|
Logger.getLogger(ForumControllerImpl.class.getName());
|
||||||
|
|
||||||
private final Executor cryptoExecutor;
|
|
||||||
private final ForumPostFactory forumPostFactory;
|
|
||||||
private final CryptoComponent crypto;
|
|
||||||
private final ForumManager forumManager;
|
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
|
@Inject
|
||||||
ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager,
|
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||||
@CryptoExecutor Executor cryptoExecutor,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
ForumPostFactory forumPostFactory, CryptoComponent crypto,
|
|
||||||
ForumManager forumManager, EventBus eventBus,
|
ForumManager forumManager, EventBus eventBus,
|
||||||
IdentityManager identityManager) {
|
AndroidNotificationManager notificationManager, Clock clock) {
|
||||||
super(dbExecutor, lifecycleManager);
|
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||||
this.cryptoExecutor = cryptoExecutor;
|
eventBus, notificationManager, clock);
|
||||||
this.forumPostFactory = forumPostFactory;
|
|
||||||
this.crypto = crypto;
|
|
||||||
this.forumManager = forumManager;
|
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
|
@Override
|
||||||
public void onActivityResume() {
|
public void onActivityResume() {
|
||||||
eventBus.addListener(this);
|
super.onActivityResume();
|
||||||
}
|
notificationManager.clearForumPostNotification(getGroupId());
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityPause() {
|
|
||||||
eventBus.removeListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityDestroy() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void eventOccurred(Event e) {
|
public void eventOccurred(Event e) {
|
||||||
if (forum == null) return;
|
super.eventOccurred(e);
|
||||||
|
|
||||||
if (e instanceof ForumPostReceivedEvent) {
|
if (e instanceof ForumPostReceivedEvent) {
|
||||||
final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
|
final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
|
||||||
if (pe.getGroupId().equals(forum.getId())) {
|
if (pe.getGroupId().equals(getGroupId())) {
|
||||||
LOG.info("Forum post received, adding...");
|
LOG.info("Forum post received, adding...");
|
||||||
final ForumPostHeader fph = pe.getForumPostHeader();
|
final ForumPostHeader fph = pe.getForumPostHeader();
|
||||||
updateNewestTimestamp(fph.getTimestamp());
|
|
||||||
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
|
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
listener.onForumPostReceived(fph);
|
listener.onHeaderReceived(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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* This should only be run from the DbThread.
|
protected Forum loadNamedGroup() throws DbException {
|
||||||
*
|
return forumManager.getForum(getGroupId());
|
||||||
* @throws DbException
|
|
||||||
*/
|
|
||||||
private void loadForum(GroupId groupId) throws DbException {
|
|
||||||
// Get Forum
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
forum = forumManager.getForum(groupId);
|
|
||||||
long duration = System.currentTimeMillis() - now;
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Loading forum took " + duration + " ms");
|
|
||||||
|
|
||||||
// Get First Identity
|
|
||||||
now = System.currentTimeMillis();
|
|
||||||
localAuthor = identityManager.getLocalAuthor();
|
|
||||||
duration = System.currentTimeMillis() - now;
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Loading author took " + duration + " ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* This should only be run from the DbThread.
|
protected Collection<ForumPostHeader> loadHeaders() throws DbException {
|
||||||
*
|
return forumManager.getPostHeaders(getGroupId());
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* This should only be run from the DbThread.
|
protected String loadMessageBody(MessageId id) throws DbException {
|
||||||
*
|
return StringUtils.fromUtf8(forumManager.getPostBody(id));
|
||||||
* @throws DbException
|
}
|
||||||
*/
|
|
||||||
private void loadBodies(Collection<ForumPostHeader> headers)
|
@Override
|
||||||
|
protected void markRead(MessageId id) throws DbException {
|
||||||
|
forumManager.setReadFlag(getGroupId(), id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected long getLatestTimestamp() throws DbException {
|
||||||
|
GroupCount count = forumManager.getGroupCount(getGroupId());
|
||||||
|
return count.getLatestMsgTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ForumPost createLocalMessage(String body, long timestamp,
|
||||||
|
@Nullable MessageId parentId, LocalAuthor author) {
|
||||||
|
return forumManager
|
||||||
|
.createLocalPost(getGroupId(), body, timestamp, parentId,
|
||||||
|
author);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ForumPostHeader addLocalMessage(ForumPost p)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
// Get Bodies
|
return forumManager.addLocalPost(p);
|
||||||
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
|
@Override
|
||||||
public void loadForum(final GroupId groupId,
|
protected void deleteNamedGroup(Forum forum) throws DbException {
|
||||||
final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) {
|
forumManager.removeForum(forum);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
protected ForumItem buildItem(ForumPostHeader header, String body) {
|
||||||
public Forum getForum() {
|
return new ForumItem(header, body);
|
||||||
return forum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void entryRead(ForumEntry forumEntry) {
|
|
||||||
entriesRead(Collections.singletonList(forumEntry));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPost(byte[] body,
|
|
||||||
ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
|
||||||
createPost(body, null, resultHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateNewestTimestamp(long update) {
|
|
||||||
long newest = newestTimeStamp.get();
|
|
||||||
while (newest < update) {
|
|
||||||
if (newestTimeStamp.compareAndSet(newest, update)) return;
|
|
||||||
newest = newestTimeStamp.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package org.briarproject.android.forum;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
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.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static org.briarproject.android.BriarActivity.GROUP_ID;
|
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
|
class ForumListAdapter
|
||||||
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
|
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
|
||||||
@@ -84,7 +83,7 @@ class ForumListAdapter
|
|||||||
Intent i = new Intent(ctx, ForumActivity.class);
|
Intent i = new Intent(ctx, ForumActivity.class);
|
||||||
Forum f = item.getForum();
|
Forum f = item.getForum();
|
||||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||||
i.putExtra(FORUM_NAME, f.getName());
|
i.putExtra(GROUP_NAME, f.getName());
|
||||||
ctx.startActivity(i);
|
ctx.startActivity(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -115,17 +114,6 @@ class ForumListAdapter
|
|||||||
return a.getForum().equals(b.getForum());
|
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 findItemPosition(GroupId g) {
|
||||||
int count = getItemCount();
|
int count = getItemCount();
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
|||||||
@@ -1,289 +1,20 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.support.annotation.UiThread;
|
||||||
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.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.util.NestedTreeList;
|
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||||
import org.briarproject.android.view.AuthorView;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
|
||||||
import org.briarproject.util.StringUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
@UiThread
|
||||||
import java.util.HashMap;
|
public class NestedForumAdapter extends ThreadItemAdapter<ForumItem> {
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
public NestedForumAdapter(ThreadItemListener<ForumItem> listener,
|
||||||
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,
|
|
||||||
LinearLayoutManager layoutManager) {
|
LinearLayoutManager layoutManager) {
|
||||||
this.ctx = ctx;
|
super(listener, layoutManager);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -294,159 +25,4 @@ public class NestedForumAdapter
|
|||||||
return new NestedForumHolder(v);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.briarproject.android.privategroup.list;
|
package org.briarproject.android.privategroup.list;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -32,7 +31,7 @@ class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(GroupViewHolder ui, int position) {
|
public void onBindViewHolder(GroupViewHolder ui, int position) {
|
||||||
ui.bindView(ctx, getItemAt(position), listener);
|
ui.bindView(ctx, items.get(position), listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ public class GroupListControllerImpl extends DbControllerImpl
|
|||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"GroupListListener needs to be attached");
|
"GroupListListener needs to be attached");
|
||||||
eventBus.addListener(this);
|
eventBus.addListener(this);
|
||||||
|
// TODO: Add new notification manager methods for private groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package org.briarproject.android.privategroup.list;
|
package org.briarproject.android.privategroup.list;
|
||||||
|
|
||||||
import android.content.Context;
|
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.support.v7.widget.RecyclerView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
@@ -10,13 +11,18 @@ import android.widget.Button;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
|
import org.briarproject.android.privategroup.conversation.GroupActivity;
|
||||||
import org.briarproject.android.util.AndroidUtils;
|
import org.briarproject.android.util.AndroidUtils;
|
||||||
import org.briarproject.android.view.TextAvatarView;
|
import org.briarproject.android.view.TextAvatarView;
|
||||||
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import static android.support.v4.content.ContextCompat.getColor;
|
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.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
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 {
|
class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
@@ -44,10 +50,8 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
|||||||
remove = (Button) v.findViewById(R.id.removeButton);
|
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) {
|
@NotNull final OnGroupRemoveClickListener listener) {
|
||||||
if (group == null) return;
|
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
avatar.setText(group.getName().substring(0, 1));
|
avatar.setText(group.getName().substring(0, 1));
|
||||||
avatar.setBackgroundBytes(group.getId().getBytes());
|
avatar.setBackgroundBytes(group.getId().getBytes());
|
||||||
@@ -115,15 +119,15 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
|||||||
layout.setOnClickListener(new OnClickListener() {
|
layout.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
/*
|
|
||||||
Intent i = new Intent(ctx, GroupActivity.class);
|
Intent i = new Intent(ctx, GroupActivity.class);
|
||||||
GroupId id = item.getId();
|
GroupId id = group.getId();
|
||||||
i.putExtra(GROUP_ID, id.getBytes());
|
i.putExtra(GROUP_ID, id.getBytes());
|
||||||
|
i.putExtra(GROUP_NAME, group.getName());
|
||||||
ActivityOptionsCompat options = ActivityOptionsCompat
|
ActivityOptionsCompat options = ActivityOptionsCompat
|
||||||
.makeCustomAnimation(ctx, android.R.anim.fade_in,
|
.makeCustomAnimation(ctx, android.R.anim.fade_in,
|
||||||
android.R.anim.fade_out);
|
android.R.anim.fade_out);
|
||||||
ActivityCompat.startActivity(ctx, i, options.toBundle());
|
Intent[] intents = {i};
|
||||||
*/
|
startActivities(ctx, intents, options.toBundle());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.briarproject.android.util;
|
package org.briarproject.android.threaded;
|
||||||
|
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
|
|
||||||
@@ -1,41 +1,35 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.threaded;
|
||||||
|
|
||||||
import org.briarproject.api.clients.MessageTree;
|
import org.briarproject.api.clients.MessageTree.MessageNode;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
/* This class is not thread safe */
|
import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
|
||||||
public class ForumEntry implements MessageTree.MessageNode {
|
|
||||||
|
|
||||||
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 messageId;
|
||||||
private final MessageId parentId;
|
private final MessageId parentId;
|
||||||
private final String text;
|
private final String text;
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final Author author;
|
private final Author author;
|
||||||
private Status status;
|
private final Status status;
|
||||||
private int level = LEVEL_UNDEFINED;
|
private int level = UNDEFINED;
|
||||||
private boolean isShowingDescendants = true;
|
private boolean isShowingDescendants = true;
|
||||||
private int descendantCount = 0;
|
private int descendantCount = 0;
|
||||||
private boolean isRead = true;
|
private boolean isRead;
|
||||||
|
|
||||||
ForumEntry(ForumPostHeader h, String text) {
|
public ThreadItem(MessageId messageId, MessageId parentId, String text,
|
||||||
this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
long timestamp, Author author, Status status, boolean isRead) {
|
||||||
h.getAuthorStatus());
|
|
||||||
this.isRead = h.isRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ForumEntry(MessageId messageId, MessageId parentId, String text,
|
|
||||||
long timestamp, Author author, Status status) {
|
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
|
this.isRead = isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getText() {
|
public String getText() {
|
||||||
@@ -56,6 +50,7 @@ public class ForumEntry implements MessageTree.MessageNode {
|
|||||||
return parentId;
|
return parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
@@ -68,27 +63,24 @@ public class ForumEntry implements MessageTree.MessageNode {
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isShowingDescendants() {
|
public boolean isShowingDescendants() {
|
||||||
return isShowingDescendants;
|
return isShowingDescendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setLevel(int level) {
|
public void setLevel(int level) {
|
||||||
this.level = level;
|
this.level = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setShowingDescendants(boolean showingDescendants) {
|
public void setShowingDescendants(boolean showingDescendants) {
|
||||||
this.isShowingDescendants = showingDescendants;
|
this.isShowingDescendants = showingDescendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageId getMessageId() {
|
|
||||||
return messageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRead() {
|
public boolean isRead() {
|
||||||
return isRead;
|
return isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setRead(boolean read) {
|
public void setRead(boolean read) {
|
||||||
isRead = read;
|
isRead = read;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +88,7 @@ public class ForumEntry implements MessageTree.MessageNode {
|
|||||||
return descendantCount > 0;
|
return descendantCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setDescendantCount(int descendantCount) {
|
public void setDescendantCount(int descendantCount) {
|
||||||
this.descendantCount = descendantCount;
|
this.descendantCount = descendantCount;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,14 +11,12 @@ import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
|||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.AuthorId;
|
import org.briarproject.api.identity.AuthorId;
|
||||||
import org.briarproject.api.sync.GroupId;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
import org.robolectric.RobolectricGradleTestRunner;
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
@@ -26,6 +24,7 @@ import org.robolectric.annotation.Config;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static junit.framework.Assert.assertEquals;
|
import static junit.framework.Assert.assertEquals;
|
||||||
@@ -81,7 +80,7 @@ public class ForumActivityTest {
|
|||||||
|
|
||||||
private TestForumActivity forumActivity;
|
private TestForumActivity forumActivity;
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>>
|
private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumItem>, DbException>>
|
||||||
rc;
|
rc;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -93,14 +92,14 @@ public class ForumActivityTest {
|
|||||||
.withIntent(intent).create().resume().get();
|
.withIntent(intent).create().resume().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ForumEntry> getDummyData() {
|
private List<ForumItem> getDummyData() {
|
||||||
ForumEntry[] forumEntries = new ForumEntry[6];
|
ForumItem[] forumEntries = new ForumItem[6];
|
||||||
for (int i = 0; i < forumEntries.length; i++) {
|
for (int i = 0; i < forumEntries.length; i++) {
|
||||||
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
|
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
|
||||||
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
|
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
|
||||||
Author author = new Author(authorId, AUTHORS[i], publicKey);
|
Author author = new Author(authorId, AUTHORS[i], publicKey);
|
||||||
forumEntries[i] =
|
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,
|
AUTHORS[i], System.currentTimeMillis(), author,
|
||||||
UNKNOWN);
|
UNKNOWN);
|
||||||
forumEntries[i].setLevel(LEVELS[i]);
|
forumEntries[i].setLevel(LEVELS[i]);
|
||||||
@@ -111,9 +110,8 @@ public class ForumActivityTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testNestedEntries() {
|
public void testNestedEntries() {
|
||||||
ForumController mc = forumActivity.getController();
|
ForumController mc = forumActivity.getController();
|
||||||
List<ForumEntry> dummyData = getDummyData();
|
List<ForumItem> dummyData = getDummyData();
|
||||||
verify(mc, times(1))
|
verify(mc, times(1)).loadItems(rc.capture());
|
||||||
.loadForum(Mockito.any(GroupId.class), rc.capture());
|
|
||||||
rc.getValue().onResult(dummyData);
|
rc.getValue().onResult(dummyData);
|
||||||
NestedForumAdapter adapter = forumActivity.getAdapter();
|
NestedForumAdapter adapter = forumActivity.getAdapter();
|
||||||
Assert.assertNotNull(adapter);
|
Assert.assertNotNull(adapter);
|
||||||
@@ -126,9 +124,9 @@ public class ForumActivityTest {
|
|||||||
adapter.hideDescendants(dummyData.get(0));
|
adapter.hideDescendants(dummyData.get(0));
|
||||||
assertEquals(2, adapter.getItemCount());
|
assertEquals(2, adapter.getItemCount());
|
||||||
assertTrue(dummyData.get(0).getText()
|
assertTrue(dummyData.get(0).getText()
|
||||||
.equals(adapter.getVisibleEntry(0).getText()));
|
.equals(adapter.getVisibleItem(0).getText()));
|
||||||
assertTrue(dummyData.get(5).getText()
|
assertTrue(dummyData.get(5).getText()
|
||||||
.equals(adapter.getVisibleEntry(1).getText()));
|
.equals(adapter.getVisibleItem(1).getText()));
|
||||||
// Cascade re-open
|
// Cascade re-open
|
||||||
adapter.showDescendants(dummyData.get(0));
|
adapter.showDescendants(dummyData.get(0));
|
||||||
assertEquals(4, adapter.getItemCount());
|
assertEquals(4, adapter.getItemCount());
|
||||||
@@ -137,8 +135,8 @@ public class ForumActivityTest {
|
|||||||
adapter.showDescendants(dummyData.get(2));
|
adapter.showDescendants(dummyData.get(2));
|
||||||
assertEquals(6, adapter.getItemCount());
|
assertEquals(6, adapter.getItemCount());
|
||||||
assertTrue(dummyData.get(2).getText()
|
assertTrue(dummyData.get(2).getText()
|
||||||
.equals(adapter.getVisibleEntry(2).getText()));
|
.equals(adapter.getVisibleItem(2).getText()));
|
||||||
assertTrue(dummyData.get(4).getText()
|
assertTrue(dummyData.get(4).getText()
|
||||||
.equals(adapter.getVisibleEntry(4).getText()));
|
.equals(adapter.getVisibleItem(4).getText()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public NestedForumAdapter getAdapter() {
|
public NestedForumAdapter getAdapter() {
|
||||||
return forumAdapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,19 +2,22 @@ package org.briarproject.api.blogs;
|
|||||||
|
|
||||||
import org.briarproject.api.clients.BaseGroup;
|
import org.briarproject.api.clients.BaseGroup;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sharing.Shareable;
|
import org.briarproject.api.sharing.Shareable;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
public class Blog extends BaseGroup implements Shareable {
|
public class Blog extends BaseGroup implements Shareable {
|
||||||
|
|
||||||
private final String description;
|
private final String description;
|
||||||
private final Author author;
|
private final Author author;
|
||||||
|
|
||||||
public Blog(@NotNull Group group, @NotNull String name,
|
public Blog(Group group, String name, String description, Author author) {
|
||||||
@NotNull String description, @NotNull Author author) {
|
super(group, name);
|
||||||
super(group, name, null);
|
|
||||||
|
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
package org.briarproject.api.clients;
|
package org.briarproject.api.clients;
|
||||||
|
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
public abstract class BaseGroup {
|
public abstract class BaseGroup {
|
||||||
|
|
||||||
private final Group group;
|
private final Group group;
|
||||||
private final String name;
|
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.group = group;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.salt = salt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -31,10 +34,6 @@ public abstract class BaseGroup {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getSalt() {
|
|
||||||
return salt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return group.hashCode();
|
return group.hashCode();
|
||||||
|
|||||||
29
briar-api/src/org/briarproject/api/clients/NamedGroup.java
Normal file
29
briar-api/src/org/briarproject/api/clients/NamedGroup.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package org.briarproject.api.forum;
|
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.sharing.Shareable;
|
||||||
import org.briarproject.api.sync.Group;
|
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) {
|
public Forum(Group group, String name, byte[] salt) {
|
||||||
super(group, name, salt);
|
super(group, name, salt);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package org.briarproject.api.forum;
|
package org.briarproject.api.forum;
|
||||||
|
|
||||||
import org.briarproject.api.clients.MessageTracker;
|
import org.briarproject.api.clients.MessageTracker;
|
||||||
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
@@ -20,8 +23,13 @@ public interface ForumManager extends MessageTracker {
|
|||||||
/** Unsubscribes from a forum. */
|
/** Unsubscribes from a forum. */
|
||||||
void removeForum(Forum f) throws DbException;
|
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. */
|
/** Stores a local forum post. */
|
||||||
void addLocalPost(ForumPost p) throws DbException;
|
ForumPostHeader addLocalPost(ForumPost p) throws DbException;
|
||||||
|
|
||||||
/** Returns the forum with the given ID. */
|
/** Returns the forum with the given ID. */
|
||||||
Forum getForum(GroupId g) throws DbException;
|
Forum getForum(GroupId g) throws DbException;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
public class ForumPost extends BaseMessage {
|
public class ForumPost extends BaseMessage {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private final Author author;
|
private final Author author;
|
||||||
|
|
||||||
public ForumPost(@NotNull Message message, @Nullable MessageId parent,
|
public ForumPost(@NotNull Message message, @Nullable MessageId parent,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package org.briarproject.api.forum;
|
package org.briarproject.api.forum;
|
||||||
|
|
||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.crypto.PrivateKey;
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.identity.Author;
|
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ public interface ForumPostFactory {
|
|||||||
throws FormatException;
|
throws FormatException;
|
||||||
|
|
||||||
ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
|
ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
|
||||||
MessageId parent, Author author, String contentType, byte[] body,
|
MessageId parent, LocalAuthor author, String body)
|
||||||
PrivateKey privateKey) throws FormatException,
|
throws FormatException, GeneralSecurityException;
|
||||||
GeneralSecurityException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
package org.briarproject.api.privategroup;
|
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.identity.Author;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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,
|
public GroupMessage(@NotNull Message message, @Nullable MessageId parent,
|
||||||
@NotNull Author author) {
|
@NotNull Author author) {
|
||||||
super(message, parent, author);
|
super(message, parent);
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Author getAuthor() {
|
||||||
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package org.briarproject.api.privategroup;
|
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.identity.Author;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
import org.jetbrains.annotations.NotNull;
|
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;
|
private final Author author;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.briarproject.api.privategroup;
|
package org.briarproject.api.privategroup;
|
||||||
|
|
||||||
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.sync.Group;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public interface PrivateGroupFactory {
|
public interface PrivateGroupFactory {
|
||||||
@@ -17,4 +19,9 @@ public interface PrivateGroupFactory {
|
|||||||
@NotNull
|
@NotNull
|
||||||
PrivateGroup createPrivateGroup(String name, Author author, byte[] salt);
|
PrivateGroup createPrivateGroup(String name, Author author, byte[] salt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a group and returns the corresponding PrivateGroup.
|
||||||
|
*/
|
||||||
|
PrivateGroup parsePrivateGroup(Group group) throws FormatException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package org.briarproject.api.privategroup;
|
|||||||
import org.briarproject.api.clients.MessageTracker;
|
import org.briarproject.api.clients.MessageTracker;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
@@ -19,8 +21,12 @@ public interface PrivateGroupManager extends MessageTracker {
|
|||||||
/** Removes a dissolved private group. */
|
/** Removes a dissolved private group. */
|
||||||
void removePrivateGroup(GroupId g) throws DbException;
|
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. */
|
/** 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. */
|
/** Returns the private group with the given ID. */
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import org.briarproject.api.forum.Forum;
|
|||||||
import org.briarproject.api.forum.ForumFactory;
|
import org.briarproject.api.forum.ForumFactory;
|
||||||
import org.briarproject.api.forum.ForumManager;
|
import org.briarproject.api.forum.ForumManager;
|
||||||
import org.briarproject.api.forum.ForumPost;
|
import org.briarproject.api.forum.ForumPost;
|
||||||
|
import org.briarproject.api.forum.ForumPostFactory;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
import org.briarproject.api.identity.AuthorId;
|
import org.briarproject.api.identity.AuthorId;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
import org.briarproject.api.sync.GroupId;
|
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.api.sync.MessageId;
|
||||||
import org.briarproject.clients.BdfIncomingMessageHook;
|
import org.briarproject.clients.BdfIncomingMessageHook;
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
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_PUBLIC_NAME;
|
||||||
import static org.briarproject.api.forum.ForumConstants.KEY_TIMESTAMP;
|
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.ANONYMOUS;
|
||||||
|
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
||||||
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||||
|
|
||||||
class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
||||||
@@ -57,16 +62,18 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
|||||||
|
|
||||||
private final IdentityManager identityManager;
|
private final IdentityManager identityManager;
|
||||||
private final ForumFactory forumFactory;
|
private final ForumFactory forumFactory;
|
||||||
|
private final ForumPostFactory forumPostFactory;
|
||||||
private final List<RemoveForumHook> removeHooks;
|
private final List<RemoveForumHook> removeHooks;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ForumManagerImpl(DatabaseComponent db, IdentityManager identityManager,
|
ForumManagerImpl(DatabaseComponent db, IdentityManager identityManager,
|
||||||
ClientHelper clientHelper, MetadataParser metadataParser,
|
ClientHelper clientHelper, MetadataParser metadataParser,
|
||||||
ForumFactory forumFactory) {
|
ForumFactory forumFactory, ForumPostFactory forumPostFactory) {
|
||||||
super(db, clientHelper, metadataParser);
|
super(db, clientHelper, metadataParser);
|
||||||
|
|
||||||
this.identityManager = identityManager;
|
this.identityManager = identityManager;
|
||||||
this.forumFactory = forumFactory;
|
this.forumFactory = forumFactory;
|
||||||
|
this.forumPostFactory = forumPostFactory;
|
||||||
removeHooks = new CopyOnWriteArrayList<RemoveForumHook>();
|
removeHooks = new CopyOnWriteArrayList<RemoveForumHook>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +125,24 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
Transaction txn = db.startTransaction(false);
|
||||||
try {
|
try {
|
||||||
BdfDictionary meta = new BdfDictionary();
|
BdfDictionary meta = new BdfDictionary();
|
||||||
@@ -142,6 +166,8 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction(txn);
|
db.endTransaction(txn);
|
||||||
}
|
}
|
||||||
|
return new ForumPostHeader(p.getMessage().getId(), p.getParent(),
|
||||||
|
p.getMessage().getTimestamp(), p.getAuthor(), OURSELVES, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package org.briarproject.forum;
|
|||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
import org.briarproject.api.crypto.CryptoComponent;
|
||||||
|
import org.briarproject.api.crypto.KeyParser;
|
||||||
import org.briarproject.api.crypto.PrivateKey;
|
import org.briarproject.api.crypto.PrivateKey;
|
||||||
import org.briarproject.api.crypto.Signature;
|
import org.briarproject.api.crypto.Signature;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.forum.ForumPost;
|
import org.briarproject.api.forum.ForumPost;
|
||||||
import org.briarproject.api.forum.ForumPostFactory;
|
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.GroupId;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
@@ -49,9 +50,10 @@ class ForumPostFactoryImpl implements ForumPostFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
|
public ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
|
||||||
MessageId parent, Author author, String contentType, byte[] body,
|
MessageId parent, LocalAuthor author, String bodyStr)
|
||||||
PrivateKey privateKey) throws FormatException,
|
throws FormatException, GeneralSecurityException {
|
||||||
GeneralSecurityException {
|
String contentType = "text/plain";
|
||||||
|
byte[] body = StringUtils.toUtf8(bodyStr);
|
||||||
// Validate the arguments
|
// Validate the arguments
|
||||||
if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
|
if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
@@ -62,6 +64,10 @@ class ForumPostFactoryImpl implements ForumPostFactory {
|
|||||||
author.getPublicKey());
|
author.getPublicKey());
|
||||||
BdfList signed = BdfList.of(groupId, timestamp, parent, authorList,
|
BdfList signed = BdfList.of(groupId, timestamp, parent, authorList,
|
||||||
contentType, body);
|
contentType, body);
|
||||||
|
// Get private key
|
||||||
|
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||||
|
byte[] k = author.getPrivateKey();
|
||||||
|
PrivateKey privateKey = keyParser.parsePrivateKey(k);
|
||||||
// Generate the signature
|
// Generate the signature
|
||||||
Signature signature = crypto.getSignature();
|
Signature signature = crypto.getSignature();
|
||||||
signature.initSign(privateKey);
|
signature.initSign(privateKey);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.briarproject.api.FormatException;
|
|||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.sync.Group;
|
import org.briarproject.api.sync.Group;
|
||||||
@@ -22,14 +23,17 @@ class PrivateGroupFactoryImpl implements PrivateGroupFactory {
|
|||||||
|
|
||||||
private final GroupFactory groupFactory;
|
private final GroupFactory groupFactory;
|
||||||
private final ClientHelper clientHelper;
|
private final ClientHelper clientHelper;
|
||||||
|
private final AuthorFactory authorFactory;
|
||||||
private final SecureRandom random;
|
private final SecureRandom random;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PrivateGroupFactoryImpl(GroupFactory groupFactory,
|
PrivateGroupFactoryImpl(GroupFactory groupFactory,
|
||||||
ClientHelper clientHelper, SecureRandom random) {
|
ClientHelper clientHelper, AuthorFactory authorFactory,
|
||||||
|
SecureRandom random) {
|
||||||
|
|
||||||
this.groupFactory = groupFactory;
|
this.groupFactory = groupFactory;
|
||||||
this.clientHelper = clientHelper;
|
this.clientHelper = clientHelper;
|
||||||
|
this.authorFactory = authorFactory;
|
||||||
this.random = random;
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,34 @@ import org.briarproject.api.data.MetadataParser;
|
|||||||
import org.briarproject.api.db.DatabaseComponent;
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
import org.briarproject.api.identity.Author;
|
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.privategroup.GroupMessage;
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
|
import org.briarproject.api.sync.Group;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
import org.briarproject.clients.BdfIncomingMessageHook;
|
import org.briarproject.clients.BdfIncomingMessageHook;
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
||||||
|
|
||||||
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||||
PrivateGroupManager {
|
PrivateGroupManager {
|
||||||
|
|
||||||
@@ -40,16 +47,21 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
|
|
||||||
private final IdentityManager identityManager;
|
private final IdentityManager identityManager;
|
||||||
private final PrivateGroupFactory privateGroupFactory;
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PrivateGroupManagerImpl(ClientHelper clientHelper,
|
PrivateGroupManagerImpl(ClientHelper clientHelper,
|
||||||
MetadataParser metadataParser, DatabaseComponent db,
|
MetadataParser metadataParser, DatabaseComponent db,
|
||||||
IdentityManager identityManager,
|
IdentityManager identityManager,
|
||||||
PrivateGroupFactory privateGroupFactory) {
|
PrivateGroupFactory privateGroupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory, Clock clock) {
|
||||||
super(db, clientHelper, metadataParser);
|
super(db, clientHelper, metadataParser);
|
||||||
|
|
||||||
this.identityManager = identityManager;
|
this.identityManager = identityManager;
|
||||||
this.privateGroupFactory = privateGroupFactory;
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -64,7 +76,22 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
Transaction txn = db.startTransaction(false);
|
||||||
try {
|
try {
|
||||||
BdfDictionary meta = new BdfDictionary();
|
BdfDictionary meta = new BdfDictionary();
|
||||||
@@ -76,21 +103,35 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction(txn);
|
db.endTransaction(txn);
|
||||||
}
|
}
|
||||||
|
return new GroupMessageHeader(m.getMessage().getGroupId(),
|
||||||
|
m.getMessage().getId(), m.getParent(),
|
||||||
|
m.getMessage().getTimestamp(), m.getAuthor(), OURSELVES, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
|
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
|
||||||
Author a = identityManager.getLocalAuthor();
|
PrivateGroup privateGroup;
|
||||||
return privateGroupFactory.createPrivateGroup("todo", a);
|
Transaction txn = db.startTransaction(true);
|
||||||
|
try {
|
||||||
|
privateGroup = getPrivateGroup(txn, g);
|
||||||
|
txn.setComplete();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction(txn);
|
||||||
|
}
|
||||||
|
return privateGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
|
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
Author a = identityManager.getLocalAuthor(txn);
|
try {
|
||||||
return privateGroupFactory.createPrivateGroup("todo", a);
|
Group group = db.getGroup(txn, g);
|
||||||
|
return privateGroupFactory.parsePrivateGroup(group);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|||||||
@@ -86,4 +86,11 @@ public class StringUtils {
|
|||||||
public static String trim(String s) {
|
public static String trim(String s) {
|
||||||
return s.trim();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user