Merge branch '707-implement-ux-for-showing-and-answering-private-group-invitations' into 'master'

Implement UX for showing and answering private group invitations

As usual, this MR contains several logically separate commits that could be split out into smaller MRs if desired. It consists of two main parts:
* Showing open invitations in the list of private groups with a snackbar
* Showing invitations and responses in the private conversation

For both parts, the existing code was refactored to allow for a smooth implementation and to leave maintainable code behind.

![device-2016-10-18-101549](/uploads/66582dbe97736fdcd2498e87e1c7dfd1/device-2016-10-18-101549.png)
![device-2016-10-18-101612](/uploads/8c25eff8171f330796a55cb27cdb2552/device-2016-10-18-101612.png)
![device-2016-10-18-101534](/uploads/ebba4c0a2c0f727dcadac8c2ec57b48f/device-2016-10-18-101534.png)

Closes #707

See merge request !357
This commit is contained in:
akwizgran
2016-10-31 12:02:22 +00:00
89 changed files with 2299 additions and 1705 deletions

View File

@@ -547,7 +547,7 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
BlogInvitationReceivedEvent event =
(BlogInvitationReceivedEvent) e;
eventWaiter.assertEquals(contactId1, event.getContactId());
Blog b = event.getBlog();
Blog b = event.getShareable();
try {
Contact c = contactManager0.getContact(contactId1);
blogSharingManager0.respondToInvitation(b, c, true);
@@ -589,7 +589,7 @@ public class BlogSharingIntegrationTest extends BriarIntegrationTest {
(BlogInvitationReceivedEvent) e;
requestReceived = true;
if (!answer) return;
Blog b = event.getBlog();
Blog b = event.getShareable();
try {
eventWaiter.assertEquals(1,
blogSharingManager1.getInvitations().size());

View File

@@ -34,7 +34,7 @@ import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sharing.SharingInvitationItem;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.SyncSession;
@@ -762,7 +762,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
"Sharer2 to Invitee");
// make sure we now have two invitations to the same forum available
Collection<InvitationItem> forums =
Collection<SharingInvitationItem> forums =
forumSharingManager1.getInvitations();
assertEquals(1, forums.size());
assertEquals(2, forums.iterator().next().getNewSharers().size());
@@ -939,7 +939,7 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
(ForumInvitationReceivedEvent) e;
eventWaiter.assertEquals(contactId1, event.getContactId());
requestReceived = true;
Forum f = event.getForum();
Forum f = event.getShareable();
try {
Contact c = contactManager0.getContact(contactId1);
forumSharingManager0.respondToInvitation(f, c, true);
@@ -982,11 +982,11 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
(ForumInvitationReceivedEvent) e;
requestReceived = true;
if (!answer) return;
Forum f = event.getForum();
Forum f = event.getShareable();
try {
eventWaiter.assertEquals(1,
forumSharingManager1.getInvitations().size());
InvitationItem invitation =
SharingInvitationItem invitation =
forumSharingManager1.getInvitations().iterator()
.next();
eventWaiter.assertEquals(f, invitation.getShareable());

View File

@@ -123,7 +123,17 @@
</activity>
<activity
android:name=".android.sharing.InvitationsForumActivity"
android:name=".android.privategroup.invitation.GroupInvitationActivity"
android:label="@string/groups_invitations_title"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity"
/>
</activity>
<activity
android:name=".android.sharing.ForumInvitationActivity"
android:label="@string/forum_invitations_title"
android:parentActivityName=".android.NavDrawerActivity">
<meta-data
@@ -133,7 +143,7 @@
</activity>
<activity
android:name=".android.sharing.InvitationsBlogActivity"
android:name=".android.sharing.BlogInvitationActivity"
android:label="@string/blogs_sharing_invitations_title"
android:parentActivityName=".android.contact.ConversationActivity">
<meta-data
@@ -187,7 +197,7 @@
</activity>
<activity
android:name=".android.sharing.SharingStatusForumActivity"
android:name=".android.sharing.ForumSharingStatusActivity"
android:label="@string/sharing_status"
android:parentActivityName=".android.forum.ForumActivity">
<meta-data
@@ -197,7 +207,7 @@
</activity>
<activity
android:name=".android.sharing.SharingStatusBlogActivity"
android:name=".android.sharing.BlogSharingStatusActivity"
android:label="@string/sharing_status"
android:parentActivityName=".android.blogs.BlogActivity">
<meta-data

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/msgLayout"
android:id="@+id/layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
@@ -11,7 +11,7 @@
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/msgBody"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/briar_text_primary"
@@ -20,7 +20,7 @@
tools:text="Short message"/>
<TextView
android:id="@+id/msgTime"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"

View File

@@ -7,7 +7,7 @@
android:orientation="vertical">
<RelativeLayout
android:id="@+id/msgLayout"
android:id="@+id/layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
@@ -16,7 +16,7 @@
android:background="@drawable/msg_out">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/msgBody"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/briar_text_primary_inverse"
@@ -25,12 +25,12 @@
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
<TextView
android:id="@+id/msgTime"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/msgBody"
android:layout_below="@+id/text"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:maxLines="1"
android:textColor="@color/private_message_date_inverse"
@@ -38,13 +38,13 @@
tools:text="Dec 24, 13:37"/>
<ImageView
android:id="@+id/msgStatus"
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/msgTime"
android:layout_alignBottom="@+id/time"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@+id/msgTime"
android:layout_toRightOf="@+id/msgTime"
android:layout_toEndOf="@+id/time"
android:layout_toRightOf="@+id/time"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered_white"/>

View File

@@ -7,7 +7,7 @@
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/msgBody"
android:id="@+id/msgText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="left|start"
@@ -20,7 +20,7 @@
tools:text="Short message"/>
<RelativeLayout
android:id="@+id/noticeLayout"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -28,7 +28,7 @@
android:background="@drawable/notice_in_bottom">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/introductionText"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="80dp"
@@ -39,28 +39,17 @@
tools:text="@string/forum_invitation_received"/>
<TextView
android:id="@+id/introductionTime"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/showInvitationsButton"
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/text"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<Button
android:id="@+id/showInvitationsButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/introductionText"
android:layout_marginBottom="-15dp"
tools:text="@string/forum_show_invitations"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -7,7 +7,7 @@
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/msgBody"
android:id="@+id/msgText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
@@ -19,7 +19,7 @@
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
<RelativeLayout
android:id="@+id/noticeLayout"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
@@ -27,7 +27,7 @@
android:background="@drawable/notice_out_bottom">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/introductionText"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/briar_text_secondary"
@@ -37,25 +37,25 @@
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/introductionText"
android:layout_below="@+id/text"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<ImageView
android:id="@+id/introductionStatus"
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/introductionTime"
android:layout_alignBottom="@+id/time"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@+id/introductionTime"
android:layout_toRightOf="@+id/introductionTime"
android:layout_toEndOf="@+id/time"
android:layout_toRightOf="@+id/time"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered"/>

View File

@@ -7,7 +7,7 @@
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/msgBody"
android:id="@+id/msgText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -19,7 +19,7 @@
tools:text="Short message"/>
<RelativeLayout
android:id="@+id/noticeLayout"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
@@ -27,7 +27,7 @@
android:background="@drawable/notice_in_bottom">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/introductionText"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="80dp"
@@ -38,11 +38,11 @@
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/declineButton"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
@@ -54,9 +54,9 @@
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/introductionText"
android:layout_alignEnd="@+id/text"
android:layout_alignRight="@+id/text"
android:layout_below="@+id/text"
android:text="@string/accept"/>
<Button
@@ -64,7 +64,7 @@
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/introductionText"
android:layout_below="@+id/text"
android:layout_marginBottom="-15dp"
android:layout_toLeftOf="@+id/acceptButton"
android:layout_toStartOf="@+id/acceptButton"

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/notice_in"
android:orientation="vertical"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/noticeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
android:textColor="@color/briar_text_secondary"
tools:text="@string/introduction_response_accepted_received"/>
<TextView
android:id="@+id/noticeTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:maxLines="1"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
</LinearLayout>

View File

@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:background="@drawable/notice_out"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/noticeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
android:textColor="@color/briar_text_secondary"
tools:text="@string/introduction_response_accepted_sent"/>
<TextView
android:id="@+id/noticeTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/noticeText"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<ImageView
android:id="@+id/noticeStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/noticeTime"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@+id/noticeTime"
android:layout_toRightOf="@+id/noticeTime"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -133,7 +133,6 @@
<string name="introduction_request_sent">You have asked to introduce %1$s to %2$s.</string>
<string name="introduction_request_received">%1$s has asked to introduce you to %2$s. Do you want to add %2$s to your contact list?</string>
<string name="introduction_request_exists_received">%1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:</string>
<string name="introduction_request_for_our_identity_received">%1$s has asked to introduce you to %2$s, but %2$s is one of your other identities, so you cannot accept the introduction:</string>
<string name="introduction_request_answered_received">%1$s has asked to introduce you to %2$s.</string>
<string name="introduction_response_accepted_sent">You accepted the introduction to %1$s.</string>
<string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string>
@@ -168,6 +167,21 @@
<string name="groups_leave">Leave Group</string>
<string name="groups_dissolve">Dissolve Group</string>
<!-- Private Group Invitations -->
<string name="groups_invitations_title">Group Invitations</string>
<string name="groups_invitations_invitation_sent">You have invited %1$s to your group "%2$s".</string>
<string name="groups_invitations_invitation_received">%1$s has invited you to join the group "%2$s".</string>
<string name="groups_invitations_joined">Joined group</string>
<string name="groups_invitations_declined">Group invitation declined</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d open group invitation</item>
<item quantity="other">%d open group invitations</item>
</plurals>
<string name="groups_invitations_response_accepted_sent">You accepted the group invitation from %s.</string>
<string name="groups_invitations_response_declined_sent">You declined the group invitation from %s.</string>
<string name="groups_invitations_response_accepted_received">%s accepted your group invitation.</string>
<string name="groups_invitations_response_declined_received">%s declined your group invitation.</string>
<!-- Forums -->
<string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string>
<string name="create_forum_title">New Forum</string>
@@ -206,7 +220,6 @@
<string name="forum_share_error">There was an error sharing this forum.</string>
<string name="forum_invitation_received">%1$s has shared the forum \"%2$s\" with you.</string>
<string name="forum_invitation_sent">You have shared the forum \"%1$s\" with %2$s.</string>
<string name="forum_show_invitations">Show Forum Invitations</string>
<string name="forum_invitations_title">Forum Invitations</string>
<string name="forum_invitation_exists">You accepted an invitation to this forum already. Accepting more invitations will grow and strengthen the communication in the forum.</string>
<string name="forum_joined_toast">Joined Forum</string>
@@ -270,7 +283,6 @@
<string name="blogs_sharing_response_declined_received">%s declined the blog invitation.</string>
<string name="blogs_sharing_invitation_received">%1$s has shared the personal blog of %2$s with you.</string>
<string name="blogs_sharing_invitation_sent">You have shared the personal blog of %1$s with %2$s.</string>
<string name="blogs_sharing_show_invitations">Show Blog Invitations</string>
<string name="blogs_sharing_invitations_title">Blog Invitations</string>
<string name="blogs_sharing_exists">You are subscribed to this blog already. Accepting again can lead to faster blog post delivery.</string>
<string name="blogs_sharing_joined_toast">Subscribed to Blog</string>

View File

@@ -33,15 +33,16 @@ import org.briarproject.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.android.privategroup.conversation.GroupActivity;
import org.briarproject.android.privategroup.creation.CreateGroupMessageFragment;
import org.briarproject.android.privategroup.list.GroupListFragment;
import org.briarproject.android.sharing.ContactSelectorFragment;
import org.briarproject.android.sharing.InvitationsBlogActivity;
import org.briarproject.android.sharing.InvitationsForumActivity;
import org.briarproject.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.android.sharing.ShareBlogActivity;
import org.briarproject.android.sharing.ShareBlogMessageFragment;
import org.briarproject.android.sharing.BlogSharingStatusActivity;
import org.briarproject.android.sharing.ContactSelectorFragment;
import org.briarproject.android.sharing.BlogInvitationActivity;
import org.briarproject.android.sharing.ForumInvitationActivity;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.sharing.ShareForumMessageFragment;
import org.briarproject.android.sharing.SharingStatusBlogActivity;
import org.briarproject.android.sharing.SharingStatusForumActivity;
import org.briarproject.android.sharing.ForumSharingStatusActivity;
import org.briarproject.android.sharing.ShareBlogMessageFragment;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
@@ -72,13 +73,13 @@ public interface ActivityComponent {
void inject(ConversationActivity activity);
void inject(InvitationsForumActivity activity);
void inject(ForumInvitationActivity activity);
void inject(InvitationsBlogActivity activity);
void inject(BlogInvitationActivity activity);
void inject(CreateGroupActivity activity);
void inject(GroupActivity activity);
void inject(GroupInvitationActivity activity);
void inject(CreateForumActivity activity);
@@ -86,9 +87,9 @@ public interface ActivityComponent {
void inject(ShareBlogActivity activity);
void inject(SharingStatusForumActivity activity);
void inject(ForumSharingStatusActivity activity);
void inject(SharingStatusBlogActivity activity);
void inject(BlogSharingStatusActivity activity);
void inject(ForumActivity activity);

View File

@@ -25,8 +25,14 @@ import org.briarproject.android.privategroup.conversation.GroupController;
import org.briarproject.android.privategroup.conversation.GroupControllerImpl;
import org.briarproject.android.privategroup.creation.CreateGroupController;
import org.briarproject.android.privategroup.creation.CreateGroupControllerImpl;
import org.briarproject.android.privategroup.invitation.GroupInvitationController;
import org.briarproject.android.privategroup.invitation.GroupInvitationControllerImpl;
import org.briarproject.android.privategroup.list.GroupListController;
import org.briarproject.android.privategroup.list.GroupListControllerImpl;
import org.briarproject.android.sharing.BlogInvitationController;
import org.briarproject.android.sharing.BlogInvitationControllerImpl;
import org.briarproject.android.sharing.ForumInvitationController;
import org.briarproject.android.sharing.ForumInvitationControllerImpl;
import dagger.Module;
import dagger.Provides;
@@ -117,6 +123,13 @@ public class ActivityModule {
return groupController;
}
@ActivityScope
@Provides
protected GroupInvitationController provideInvitationGroupController(
GroupInvitationControllerImpl groupInvitationController) {
return groupInvitationController;
}
@ActivityScope
@Provides
protected ForumController provideForumController(
@@ -125,6 +138,22 @@ public class ActivityModule {
return forumController;
}
@ActivityScope
@Provides
protected ForumInvitationController provideInvitationForumController(
ForumInvitationControllerImpl forumInvitationController) {
activity.addLifecycleController(forumInvitationController);
return forumInvitationController;
}
@ActivityScope
@Provides
protected BlogInvitationController provideInvitationBlogController(
BlogInvitationControllerImpl blogInvitationController) {
activity.addLifecycleController(blogInvitationController);
return blogInvitationController;
}
@ActivityScope
@Provides
BlogController provideBlogController(BlogControllerImpl blogController) {

View File

@@ -35,6 +35,7 @@ import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.plugins.PluginManager;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.settings.SettingsManager;
import org.briarproject.api.system.Clock;
import org.briarproject.plugins.AndroidPluginsModule;
@@ -96,6 +97,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
PrivateGroupManager privateGroupManager();
GroupInvitationManager groupInvitationManager();
ForumManager forumManager();
ForumSharingManager forumSharingManager();

View File

@@ -24,7 +24,7 @@ import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.sharing.ShareBlogActivity;
import org.briarproject.android.sharing.SharingStatusBlogActivity;
import org.briarproject.android.sharing.BlogSharingStatusActivity;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
@@ -164,7 +164,7 @@ public class BlogFragment extends BaseFragment implements
return true;
case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(),
SharingStatusBlogActivity.class);
BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3, options.toBundle());

View File

@@ -22,6 +22,7 @@ import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.api.clients.BaseMessageHeader;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
@@ -270,34 +271,35 @@ public class ContactListFragment extends BaseFragment implements EventListener {
LOG.info("Private message received, updating item");
PrivateMessageReceivedEvent p = (PrivateMessageReceivedEvent) e;
PrivateMessageHeader h = p.getMessageHeader();
updateItem(p.getContactId(), ConversationItem.from(h));
updateItem(p.getContactId(), h);
} else if (e instanceof IntroductionRequestReceivedEvent) {
LOG.info("Introduction request received, updating item");
IntroductionRequestReceivedEvent m =
(IntroductionRequestReceivedEvent) e;
IntroductionRequest ir = m.getIntroductionRequest();
updateItem(m.getContactId(), ConversationItem.from(ir));
updateItem(m.getContactId(), ir);
} else if (e instanceof IntroductionResponseReceivedEvent) {
LOG.info("Introduction response received, updating item");
IntroductionResponseReceivedEvent m =
(IntroductionResponseReceivedEvent) e;
IntroductionResponse ir = m.getIntroductionResponse();
updateItem(m.getContactId(), ConversationItem.from(ir));
updateItem(m.getContactId(), ir);
} else if (e instanceof InvitationRequestReceivedEvent) {
LOG.info("Invitation request received, updating item");
InvitationRequestReceivedEvent m = (InvitationRequestReceivedEvent) e;
LOG.info("Invitation Request received, update item");
InvitationRequestReceivedEvent m =
(InvitationRequestReceivedEvent) e;
InvitationRequest ir = m.getRequest();
updateItem(m.getContactId(), ConversationItem.from(ir));
updateItem(m.getContactId(), ir);
} else if (e instanceof InvitationResponseReceivedEvent) {
LOG.info("Invitation response received, updating item");
InvitationResponseReceivedEvent m =
(InvitationResponseReceivedEvent) e;
InvitationResponse ir = m.getResponse();
updateItem(m.getContactId(), ConversationItem.from(ir));
updateItem(m.getContactId(), ir);
}
}
private void updateItem(final ContactId c, final ConversationItem m) {
private void updateItem(final ContactId c, final BaseMessageHeader h) {
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
@@ -305,7 +307,8 @@ public class ContactListFragment extends BaseFragment implements EventListener {
int position = adapter.findItemPosition(c);
ContactListItem item = adapter.getItemAt(position);
if (item != null) {
item.addMessage(m);
ConversationItem i = ConversationItem.from(getContext(), h);
item.addMessage(i);
adapter.updateItemAt(position, item);
}
}

View File

@@ -6,9 +6,9 @@ import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.jetbrains.annotations.NotNull;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
import javax.annotation.concurrent.NotThreadSafe;
// This class is NOT thread-safe
@NotThreadSafe
public class ContactListItem {
private final Contact contact;
@@ -34,8 +34,7 @@ public class ContactListItem {
empty = empty && message == null;
if (message != null) {
if (message.getTime() > timestamp) timestamp = message.getTime();
if (message instanceof IncomingItem &&
!((IncomingItem) message).isRead())
if (!message.isRead())
unread++;
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.android.contact;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
@@ -27,7 +28,7 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.contact.ConversationAdapter.IntroductionHandler;
import org.briarproject.android.contact.ConversationAdapter.RequestListener;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.android.view.TextInputView;
@@ -39,6 +40,7 @@ import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactConnectedEvent;
@@ -64,6 +66,7 @@ import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.settings.Settings;
import org.briarproject.api.settings.SettingsManager;
import org.briarproject.api.sharing.InvitationMessage;
@@ -72,6 +75,7 @@ import org.briarproject.api.sharing.InvitationResponse;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
@@ -92,16 +96,15 @@ import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.OnHidePromptListener;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
public class ConversationActivity extends BriarActivity
implements EventListener, IntroductionHandler, TextInputListener {
implements EventListener, RequestListener, TextInputListener {
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
@@ -117,7 +120,7 @@ public class ConversationActivity extends BriarActivity
@CryptoExecutor
Executor cryptoExecutor;
private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
private ConversationAdapter adapter;
private Toolbar toolbar;
@@ -144,6 +147,8 @@ public class ConversationActivity extends BriarActivity
volatile ForumSharingManager forumSharingManager;
@Inject
volatile BlogSharingManager blogSharingManager;
@Inject
volatile GroupInvitationManager groupInvitationManager;
private volatile GroupId groupId = null;
private volatile ContactId contactId = null;
@@ -325,7 +330,6 @@ public class ConversationActivity extends BriarActivity
toolbarStatus
.setContentDescription(getString(R.string.offline));
}
adapter.setContactName(contactName);
}
});
}
@@ -350,10 +354,14 @@ public class ConversationActivity extends BriarActivity
Collection<InvitationMessage> blogInvitations =
blogSharingManager
.getInvitationMessages(contactId);
Collection<InvitationMessage> groupInvitations =
groupInvitationManager
.getInvitationMessages(contactId);
List<InvitationMessage> invitations = new ArrayList<>(
forumInvitations.size() + blogInvitations.size());
invitations.addAll(forumInvitations);
invitations.addAll(blogInvitations);
invitations.addAll(groupInvitations);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading messages took " + duration + " ms");
@@ -397,34 +405,41 @@ public class ConversationActivity extends BriarActivity
Collection<PrivateMessageHeader> headers,
Collection<IntroductionMessage> introductions,
Collection<InvitationMessage> invitations) {
int size = headers.size() + introductions.size() + invitations.size();
int size =
headers.size() + introductions.size() + invitations.size();
List<ConversationItem> items = new ArrayList<>(size);
for (PrivateMessageHeader h : headers) {
ConversationMessageItem item = ConversationItem.from(h);
byte[] body = bodyCache.get(h.getId());
ConversationItem item = ConversationItem.from(h);
String body = bodyCache.get(h.getId());
if (body == null) loadMessageBody(h.getId());
else item.setBody(body);
items.add(item);
}
for (IntroductionMessage im : introductions) {
if (im instanceof IntroductionRequest) {
IntroductionRequest ir = (IntroductionRequest) im;
items.add(ConversationItem.from(ir));
for (IntroductionMessage m : introductions) {
ConversationItem item;
if (m instanceof IntroductionRequest) {
IntroductionRequest i = (IntroductionRequest) m;
item = ConversationItem
.from(ConversationActivity.this, contactName, i);
} else {
IntroductionResponse ir = (IntroductionResponse) im;
items.add(ConversationItem.from(ConversationActivity.this,
contactName, ir));
IntroductionResponse i = (IntroductionResponse) m;
item = ConversationItem
.from(ConversationActivity.this, contactName, i);
}
items.add(item);
}
for (InvitationMessage im : invitations) {
if (im instanceof InvitationRequest) {
InvitationRequest ir = (InvitationRequest) im;
items.add(ConversationItem.from(ir));
} else if (im instanceof InvitationResponse) {
InvitationResponse ir = (InvitationResponse) im;
items.add(ConversationItem.from(ConversationActivity.this,
contactName, ir));
for (InvitationMessage i : invitations) {
ConversationItem item;
if (i instanceof InvitationRequest) {
InvitationRequest r = (InvitationRequest) i;
item = ConversationItem
.from(ConversationActivity.this, contactName, r);
} else {
InvitationResponse r = (InvitationResponse) i;
item = ConversationItem
.from(ConversationActivity.this, contactName, r);
}
items.add(item);
}
return items;
}
@@ -439,7 +454,7 @@ public class ConversationActivity extends BriarActivity
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading body took " + duration + " ms");
displayMessageBody(m, body);
displayMessageBody(m, StringUtils.fromUtf8(body));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
@@ -448,15 +463,15 @@ public class ConversationActivity extends BriarActivity
});
}
private void displayMessageBody(final MessageId m, final byte[] body) {
private void displayMessageBody(final MessageId m, final String body) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
bodyCache.put(m, body);
SparseArray<ConversationMessageItem> messages =
SparseArray<ConversationItem> messages =
adapter.getPrivateMessages();
for (int i = 0; i < messages.size(); i++) {
ConversationMessageItem item = messages.valueAt(i);
ConversationItem item = messages.valueAt(i);
if (item.getId().equals(m)) {
item.setBody(body);
adapter.notifyItemChanged(messages.keyAt(i));
@@ -482,9 +497,9 @@ public class ConversationActivity extends BriarActivity
private void markMessagesRead() {
Map<MessageId, GroupId> unread = new HashMap<>();
SparseArray<IncomingItem> list = adapter.getIncomingMessages();
SparseArray<ConversationItem> list = adapter.getIncomingMessages();
for (int i = 0; i < list.size(); i++) {
IncomingItem item = list.valueAt(i);
ConversationItem item = list.valueAt(i);
if (!item.isRead())
unread.put(item.getId(), item.getGroupId());
}
@@ -561,7 +576,8 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Introduction request received, adding...");
IntroductionRequest ir = event.getIntroductionRequest();
ConversationItem item = new ConversationIntroductionInItem(ir);
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
@@ -580,7 +596,8 @@ public class ConversationActivity extends BriarActivity
if (event.getContactId().equals(contactId)) {
LOG.info("Invitation received, adding...");
InvitationRequest ir = event.getRequest();
ConversationItem item = ConversationItem.from(ir);
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addConversationItem(item);
}
} else if (e instanceof InvitationResponseReceivedEvent) {
@@ -603,9 +620,10 @@ public class ConversationActivity extends BriarActivity
public void run() {
adapter.incrementRevision();
Set<MessageId> messages = new HashSet<>(messageIds);
SparseArray<OutgoingItem> list = adapter.getOutgoingMessages();
SparseArray<ConversationOutItem> list =
adapter.getOutgoingMessages();
for (int i = 0; i < list.size(); i++) {
OutgoingItem item = list.valueAt(i);
ConversationOutItem item = list.valueAt(i);
if (messages.contains(item.getId())) {
item.setSent(sent);
item.setSeen(seen);
@@ -622,7 +640,7 @@ public class ConversationActivity extends BriarActivity
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_BODY_LENGTH);
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
createMessage(StringUtils.toUtf8(text), timestamp);
createMessage(text, timestamp);
textInputView.setText("");
}
@@ -632,14 +650,14 @@ public class ConversationActivity extends BriarActivity
return item == null ? 0 : item.getTime() + 1;
}
private void createMessage(final byte[] body, final long timestamp) {
private void createMessage(final String body, final long timestamp) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
try {
storeMessage(privateMessageFactory.createPrivateMessage(
groupId, timestamp, null, "text/plain", body),
body);
groupId, timestamp, null, "text/plain",
StringUtils.toUtf8(body)), body);
} catch (FormatException e) {
throw new RuntimeException(e);
}
@@ -647,7 +665,7 @@ public class ConversationActivity extends BriarActivity
});
}
private void storeMessage(final PrivateMessage m, final byte[] body) {
private void storeMessage(final PrivateMessage m, final String body) {
runOnDbThread(new Runnable() {
@Override
public void run() {
@@ -661,7 +679,7 @@ public class ConversationActivity extends BriarActivity
PrivateMessageHeader h = new PrivateMessageHeader(id,
groupId, m.getMessage().getTimestamp(),
m.getContentType(), true, false, false, false);
ConversationMessageItem item = ConversationItem.from(h);
ConversationItem item = ConversationItem.from(h);
item.setBody(body);
bodyCache.put(id, body);
addConversationItem(item);
@@ -812,21 +830,37 @@ public class ConversationActivity extends BriarActivity
});
}
@UiThread
@Override
public void respondToIntroduction(final SessionId sessionId,
public void respondToRequest(@NotNull final ConversationRequestItem item,
final boolean accept) {
int position = adapter.findItemPosition(item);
if (position != INVALID_POSITION) {
adapter.notifyItemChanged(position, item);
}
runOnDbThread(new Runnable() {
@Override
public void run() {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try {
if (accept) {
introductionManager.acceptIntroduction(contactId,
sessionId, timestamp);
} else {
introductionManager.declineIntroduction(contactId,
sessionId, timestamp);
switch (item.getRequestType()) {
case INTRODUCTION:
respondToIntroductionRequest(item.getSessionId(),
accept, timestamp);
break;
case FORUM:
respondToForumRequest(item.getSessionId(), accept);
break;
case BLOG:
respondToBlogRequest(item.getSessionId(), accept);
break;
case GROUP:
respondToGroupRequest(item.getSessionId(), accept);
break;
default:
throw new IllegalArgumentException(
"Unknown Request Type");
}
loadMessages();
} catch (DbException | FormatException e) {
@@ -839,6 +873,34 @@ public class ConversationActivity extends BriarActivity
});
}
@DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException, FormatException {
if (accept) {
introductionManager.acceptIntroduction(contactId, sessionId, time);
} else {
introductionManager.declineIntroduction(contactId, sessionId, time);
}
}
@DatabaseExecutor
private void respondToForumRequest(SessionId id, boolean accept)
throws DbException {
forumSharingManager.respondToInvitation(id, accept);
}
@DatabaseExecutor
private void respondToBlogRequest(SessionId id, boolean accept)
throws DbException {
blogSharingManager.respondToInvitation(id, accept);
}
@DatabaseExecutor
private void respondToGroupRequest(SessionId id, boolean accept)
throws DbException {
groupInvitationManager.respondToInvitation(id, accept);
}
private void introductionResponseError() {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override

View File

@@ -1,367 +1,66 @@
package org.briarproject.android.contact;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.annotation.UiThread;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.sharing.InvitationsBlogActivity;
import org.briarproject.android.sharing.InvitationsForumActivity;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.android.util.BriarAdapter;
import org.briarproject.api.blogs.BlogInvitationRequest;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.forum.ForumInvitationRequest;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sharing.InvitationRequest;
import org.briarproject.util.StringUtils;
import org.briarproject.api.nullsafety.NotNullByDefault;
import static android.support.v7.widget.RecyclerView.ViewHolder;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.android.contact.ConversationItem.BLOG_INVITATION_IN;
import static org.briarproject.android.contact.ConversationItem.BLOG_INVITATION_OUT;
import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_IN;
import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_OUT;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
import static org.briarproject.android.contact.ConversationItem.MSG_IN_UNREAD;
import static org.briarproject.android.contact.ConversationItem.MSG_OUT;
import static org.briarproject.android.contact.ConversationItem.NOTICE_IN;
import static org.briarproject.android.contact.ConversationItem.NOTICE_OUT;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
class ConversationAdapter
extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
class ConversationAdapter extends BriarAdapter<ConversationItem, ViewHolder> {
private RequestListener listener;
private IntroductionHandler intro;
private String contactName;
ConversationAdapter(Context ctx, IntroductionHandler introductionHandler) {
ConversationAdapter(Context ctx, RequestListener requestListener) {
super(ctx, ConversationItem.class);
intro = introductionHandler;
}
void setContactName(String contactName) {
this.contactName = contactName;
notifyDataSetChanged();
listener = requestListener;
}
@LayoutRes
@Override
public int getItemViewType(int position) {
return items.get(position).getType();
ConversationItem item = items.get(position);
return item.getLayout();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v;
// outgoing message (local)
if (type == MSG_OUT) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_msg_out, viewGroup, false);
return new MessageHolder(v, type);
} else if (type == INTRODUCTION_IN) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_introduction_in, viewGroup, false);
return new IntroductionHolder(v, type);
} else if (type == INTRODUCTION_OUT) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_msg_notice_out, viewGroup, false);
return new IntroductionHolder(v, type);
} else if (type == NOTICE_IN) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_notice_in, viewGroup, false);
return new NoticeHolder(v, type);
} else if (type == NOTICE_OUT) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_notice_out, viewGroup, false);
return new NoticeHolder(v, type);
} else if (type == FORUM_INVITATION_IN || type == BLOG_INVITATION_IN) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_shareable_invitation_in, viewGroup,
false);
return new InvitationHolder(v, type);
} else if (type == FORUM_INVITATION_OUT ||
type == BLOG_INVITATION_OUT) {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_msg_notice_out, viewGroup, false);
return new InvitationHolder(v, type);
}
// incoming message (non-local)
else {
v = LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.list_item_msg_in, viewGroup, false);
return new MessageHolder(v, type);
public ConversationItemViewHolder onCreateViewHolder(ViewGroup viewGroup,
@LayoutRes int type) {
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
type, viewGroup, false);
switch (type) {
case R.layout.list_item_conversation_msg_in:
return new ConversationItemViewHolder(v);
case R.layout.list_item_conversation_msg_out:
return new ConversationMessageOutViewHolder(v);
case R.layout.list_item_conversation_notice_in:
return new ConversationNoticeInViewHolder(v);
case R.layout.list_item_conversation_notice_out:
return new ConversationNoticeOutViewHolder(v);
case R.layout.list_item_conversation_request:
return new ConversationRequestViewHolder(v);
default:
throw new IllegalArgumentException("Unknown ConversationItem");
}
}
@Override
public void onBindViewHolder(ViewHolder ui, int position) {
ConversationItem item = getItemAt(position);
if (item instanceof ConversationMessageItem) {
bindMessage((MessageHolder) ui, (ConversationMessageItem) item);
} else if (item instanceof ConversationIntroductionOutItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionOutItem) item, position);
} else if (item instanceof ConversationIntroductionInItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionInItem) item, position);
} else if (item instanceof ConversationNoticeOutItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item);
} else if (item instanceof ConversationNoticeInItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item);
} else if (item instanceof ConversationShareableInvitationOutItem) {
bindInvitation((InvitationHolder) ui,
(ConversationShareableInvitationOutItem) item);
} else if (item instanceof ConversationShareableInvitationInItem) {
bindInvitation((InvitationHolder) ui,
(ConversationShareableInvitationInItem) item);
public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
ConversationItem item = items.get(position);
if (item instanceof ConversationRequestItem) {
((ConversationRequestViewHolder) ui).bind(item, listener);
} else {
throw new IllegalArgumentException("Unhandled Conversation Item");
ui.bind(item);
}
}
private void bindMessage(MessageHolder ui, ConversationMessageItem item) {
PrivateMessageHeader header = item.getHeader();
if (item instanceof ConversationItem.OutgoingItem) {
if (((OutgoingItem) item).isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered_white);
} else if (((OutgoingItem) item).isSent()) {
ui.status.setImageResource(R.drawable.message_sent_white);
} else {
ui.status.setImageResource(R.drawable.message_stored_white);
}
} else {
if (item.getType() == MSG_IN_UNREAD) {
// TODO implement new unread message highlight according to #232
/* int left = ui.layout.getPaddingLeft();
int top = ui.layout.getPaddingTop();
int right = ui.layout.getPaddingRight();
int bottom = ui.layout.getPaddingBottom();
// show unread messages in different color to not miss them
ui.layout.setBackgroundResource(R.drawable.msg_in_unread);
// re-apply the previous padding due to bug in some Android versions
// see: https://code.google.com/p/android/issues/detail?id=17885
ui.layout.setPadding(left, top, right, bottom);
*/
}
}
if (item.getBody() == null) {
ui.body.setText("\u2026");
} else if (header.getContentType().equals("text/plain")) {
ui.body.setText(
StringUtils.trim(StringUtils.fromUtf8(item.getBody())));
} else {
// TODO support other content types
}
long timestamp = header.getTimestamp();
ui.date.setText(AndroidUtils.formatDate(ctx, timestamp));
}
private void bindIntroduction(IntroductionHolder ui,
final ConversationIntroductionItem item, final int position) {
final IntroductionRequest ir = item.getIntroductionRequest();
int backgroundRes;
String message = ir.getMessage();
if (StringUtils.isNullOrEmpty(message)) {
ui.message.setVisibility(GONE);
if (item instanceof ConversationIntroductionOutItem) {
backgroundRes = R.drawable.notice_out;
} else {
backgroundRes = R.drawable.notice_in;
}
} else {
ui.message.setText(StringUtils.trim(message));
ui.message.setVisibility(VISIBLE);
if (item instanceof ConversationIntroductionOutItem) {
backgroundRes = R.drawable.notice_out_bottom;
} else {
backgroundRes = R.drawable.notice_in_bottom;
}
}
// Outgoing Introduction Request
if (item instanceof ConversationIntroductionOutItem) {
ui.text.setText(ctx.getString(R.string.introduction_request_sent,
contactName, ir.getName()));
ConversationIntroductionOutItem i =
(ConversationIntroductionOutItem) item;
if (i.isSeen()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_delivered);
} else if (i.isSent()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_sent);
} else {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_stored);
}
}
// Incoming Introduction Request (Answered)
else if (item.wasAnswered()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_answered_received,
contactName, ir.getName()));
ui.acceptButton.setVisibility(GONE);
ui.declineButton.setVisibility(GONE);
}
// Incoming Introduction Request (Not Answered)
else {
if (item.getIntroductionRequest().contactExists()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_exists_received,
contactName, ir.getName()));
} else {
ui.text.setText(
ctx.getString(R.string.introduction_request_received,
contactName, ir.getName()));
}
if (item.getIntroductionRequest().doesIntroduceOtherIdentity()) {
// don't allow accept when one of our identities is introduced
ui.acceptButton.setVisibility(GONE);
ui.text.setText(ctx.getString(
R.string.introduction_request_for_our_identity_received,
contactName, ir.getName()));
} else {
ui.acceptButton.setVisibility(VISIBLE);
ui.acceptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), true);
item.setAnswered(true);
notifyItemChanged(position);
}
});
}
ui.declineButton.setVisibility(VISIBLE);
ui.declineButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), false);
item.setAnswered(true);
notifyItemChanged(position);
}
});
}
ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
ui.notice.setBackgroundResource(backgroundRes);
}
private void bindNotice(NoticeHolder ui, ConversationNoticeItem item) {
ui.text.setText(item.getText());
ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
if (item instanceof ConversationNoticeOutItem) {
ConversationNoticeOutItem n = (ConversationNoticeOutItem) item;
if (n.isSeen()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_delivered);
} else if (n.isSent()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_sent);
} else {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_stored);
}
}
}
private void bindInvitation(InvitationHolder ui,
final ConversationShareableInvitationItem item) {
final InvitationRequest ir = item.getInvitationRequest();
String name = "";
int receivedRes = 0, sentRes = 0, buttonRes = 0, backgroundRes;
if (ir instanceof ForumInvitationRequest) {
name = ((ForumInvitationRequest) ir).getForumName();
receivedRes = R.string.forum_invitation_received;
sentRes = R.string.forum_invitation_sent;
buttonRes = R.string.forum_show_invitations;
} else if (ir instanceof BlogInvitationRequest) {
name = ((BlogInvitationRequest) ir).getBlogAuthorName();
receivedRes = R.string.blogs_sharing_invitation_received;
sentRes = R.string.blogs_sharing_invitation_sent;
buttonRes = R.string.blogs_sharing_show_invitations;
}
String message = ir.getMessage();
if (StringUtils.isNullOrEmpty(message)) {
ui.message.setVisibility(GONE);
if (item instanceof ConversationShareableInvitationOutItem) {
backgroundRes = R.drawable.notice_out;
} else {
backgroundRes = R.drawable.notice_in;
}
} else {
ui.message.setVisibility(VISIBLE);
ui.message.setText(StringUtils.trim(message));
if (item instanceof ConversationShareableInvitationOutItem) {
backgroundRes = R.drawable.notice_out_bottom;
} else {
backgroundRes = R.drawable.notice_in_bottom;
}
}
// Outgoing Invitation
if (item instanceof ConversationShareableInvitationOutItem) {
ui.text.setText(ctx.getString(sentRes, name, contactName));
ConversationShareableInvitationOutItem i =
(ConversationShareableInvitationOutItem) item;
if (i.isSeen()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_delivered);
} else if (i.isSent()) {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_sent);
} else {
//noinspection ConstantConditions
ui.status.setImageResource(R.drawable.message_stored);
}
}
// Incoming Invitation
else {
ui.text.setText(ctx.getString(receivedRes, contactName, name));
if (ir.isAvailable()) {
final Class c = ir instanceof ForumInvitationRequest ?
InvitationsForumActivity.class :
InvitationsBlogActivity.class;
ui.showInvitationsButton.setText(ctx.getString(buttonRes));
ui.showInvitationsButton.setVisibility(VISIBLE);
ui.showInvitationsButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(ctx, c);
ctx.startActivity(i);
}
});
} else {
ui.showInvitationsButton.setVisibility(GONE);
}
}
ui.date.setText(AndroidUtils.formatDate(ctx, item.getTime()));
ui.notice.setBackgroundResource(backgroundRes);
}
@Override
public int compare(ConversationItem c1,
ConversationItem c2) {
@@ -393,138 +92,48 @@ class ConversationAdapter extends BriarAdapter<ConversationItem, ViewHolder> {
}
}
SparseArray<IncomingItem> getIncomingMessages() {
SparseArray<IncomingItem> messages = new SparseArray<>();
SparseArray<ConversationItem> getIncomingMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof IncomingItem) {
messages.put(i, (IncomingItem) item);
if (item.isIncoming()) {
messages.put(i, item);
}
}
return messages;
}
SparseArray<OutgoingItem> getOutgoingMessages() {
SparseArray<OutgoingItem> messages = new SparseArray<>();
SparseArray<ConversationOutItem> getOutgoingMessages() {
SparseArray<ConversationOutItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof OutgoingItem) {
messages.put(i, (OutgoingItem) item);
if (item instanceof ConversationOutItem) {
messages.put(i, (ConversationOutItem) item);
}
}
return messages;
}
SparseArray<ConversationMessageItem> getPrivateMessages() {
SparseArray<ConversationMessageItem> messages = new SparseArray<>();
SparseArray<ConversationItem> getPrivateMessages() {
SparseArray<ConversationItem> messages = new SparseArray<>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof ConversationMessageItem) {
messages.put(i, (ConversationMessageItem) item);
if (item instanceof ConversationMessageInItem) {
messages.put(i, item);
} else if (item instanceof ConversationMessageOutItem) {
messages.put(i, item);
}
}
return messages;
}
private static class MessageHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public TextView body;
private TextView date;
public ImageView status;
private MessageHolder(View v, int type) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.msgLayout);
body = (TextView) v.findViewById(R.id.msgBody);
date = (TextView) v.findViewById(R.id.msgTime);
// outgoing message (local)
if (type == MSG_OUT) {
status = (ImageView) v.findViewById(R.id.msgStatus);
}
}
@UiThread
@NotNullByDefault
interface RequestListener {
void respondToRequest(ConversationRequestItem item, boolean accept);
}
private static class IntroductionHolder extends RecyclerView.ViewHolder {
private final TextView message;
private final ViewGroup notice;
private final TextView text;
private final Button acceptButton;
private final Button declineButton;
private final TextView date;
private final ImageView status;
private IntroductionHolder(View v, int type) {
super(v);
message = (TextView) v.findViewById(R.id.msgBody);
notice = (ViewGroup) v.findViewById(R.id.noticeLayout);
text = (TextView) v.findViewById(R.id.introductionText);
acceptButton = (Button) v.findViewById(R.id.acceptButton);
declineButton = (Button) v.findViewById(R.id.declineButton);
date = (TextView) v.findViewById(R.id.introductionTime);
if (type == INTRODUCTION_OUT) {
status = (ImageView) v.findViewById(R.id.introductionStatus);
} else {
status = null;
}
}
}
private static class NoticeHolder extends RecyclerView.ViewHolder {
private final TextView text;
private final TextView date;
private final ImageView status;
private NoticeHolder(View v, int type) {
super(v);
text = (TextView) v.findViewById(R.id.noticeText);
date = (TextView) v.findViewById(R.id.noticeTime);
if (type == NOTICE_OUT) {
status = (ImageView) v.findViewById(R.id.noticeStatus);
} else {
status = null;
}
}
}
private static class InvitationHolder extends RecyclerView.ViewHolder {
private final TextView message;
private final View notice;
private final TextView text;
private final Button showInvitationsButton;
private final TextView date;
private final ImageView status;
private InvitationHolder(View v, int type) {
super(v);
message = (TextView) v.findViewById(R.id.msgBody);
text = (TextView) v.findViewById(R.id.introductionText);
notice = v.findViewById(R.id.noticeLayout);
showInvitationsButton = (Button) v.findViewById(R.id.showInvitationsButton);
date = (TextView) v.findViewById(R.id.introductionTime);
if (type == FORUM_INVITATION_OUT || type == BLOG_INVITATION_OUT) {
status = (ImageView) v.findViewById(R.id.introductionStatus);
} else {
status = null;
}
}
}
interface IntroductionHandler {
void respondToIntroduction(SessionId sessionId, boolean accept);
}
}

View File

@@ -1,33 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.android.contact.ConversationItem.IncomingItem;
import org.briarproject.api.introduction.IntroductionRequest;
import org.jetbrains.annotations.NotNull;
// This class is not thread-safe
class ConversationIntroductionInItem extends ConversationIntroductionItem
implements IncomingItem {
private boolean read;
ConversationIntroductionInItem(@NotNull IntroductionRequest ir) {
super(ir);
this.read = ir.isRead();
}
@Override
int getType() {
return INTRODUCTION_IN;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -1,31 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
import org.jetbrains.annotations.NotNull;
// This class is not thread-safe
abstract class ConversationIntroductionItem extends ConversationItem {
private final IntroductionRequest ir;
private boolean answered;
ConversationIntroductionItem(@NotNull IntroductionRequest ir) {
super(ir.getMessageId(), ir.getGroupId(), ir.getTimestamp());
this.ir = ir;
this.answered = ir.wasAnswered();
}
@NotNull
IntroductionRequest getIntroductionRequest() {
return ir;
}
boolean wasAnswered() {
return answered;
}
void setAnswered(boolean answered) {
this.answered = answered;
}
}

View File

@@ -1,47 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
/**
* This class is needed and can not be replaced by an ConversationNoticeOutItem,
* because it carries the optional introduction message
* to be displayed as a regular private message.
*
* This class is not thread-safe
*/
class ConversationIntroductionOutItem extends ConversationIntroductionItem
implements ConversationItem.OutgoingItem {
private boolean sent, seen;
ConversationIntroductionOutItem(IntroductionRequest ir) {
super(ir);
this.sent = ir.isSent();
this.seen = ir.isSeen();
}
@Override
int getType() {
return INTRODUCTION_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -1,65 +1,85 @@
package org.briarproject.android.contact;
import android.content.Context;
import android.support.annotation.LayoutRes;
import android.support.annotation.StringRes;
import org.briarproject.R;
import org.briarproject.android.contact.ConversationRequestItem.RequestType;
import org.briarproject.api.blogs.BlogInvitationRequest;
import org.briarproject.api.blogs.BlogInvitationResponse;
import org.briarproject.api.clients.BaseMessageHeader;
import org.briarproject.api.forum.ForumInvitationRequest;
import org.briarproject.api.forum.ForumInvitationResponse;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.api.privategroup.invitation.GroupInvitationResponse;
import org.briarproject.api.sharing.InvitationRequest;
import org.briarproject.api.sharing.InvitationResponse;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// This class is not thread-safe
public abstract class ConversationItem {
import javax.annotation.concurrent.NotThreadSafe;
// this is needed for RecyclerView adapter which requires an int type
final static int MSG_IN = 0;
final static int MSG_IN_UNREAD = 1;
final static int MSG_OUT = 2;
final static int INTRODUCTION_IN = 3;
final static int INTRODUCTION_OUT = 4;
final static int NOTICE_IN = 5;
final static int NOTICE_OUT = 6;
final static int FORUM_INVITATION_IN = 7;
final static int FORUM_INVITATION_OUT = 8;
final static int BLOG_INVITATION_IN = 9;
final static int BLOG_INVITATION_OUT = 10;
import static org.briarproject.android.contact.ConversationRequestItem.RequestType.BLOG;
import static org.briarproject.android.contact.ConversationRequestItem.RequestType.FORUM;
import static org.briarproject.android.contact.ConversationRequestItem.RequestType.GROUP;
import static org.briarproject.android.contact.ConversationRequestItem.RequestType.INTRODUCTION;
@NotThreadSafe
@NotNullByDefault
abstract class ConversationItem {
protected @Nullable String body;
final private MessageId id;
final private GroupId groupId;
final private long time;
private boolean read;
public ConversationItem(@NotNull MessageId id, @NotNull GroupId groupId,
long time) {
ConversationItem(MessageId id, GroupId groupId, @Nullable String body,
long time, boolean read) {
this.id = id;
this.groupId = groupId;
this.body = body;
this.time = time;
this.read = read;
}
abstract int getType();
@NotNull
public MessageId getId() {
MessageId getId() {
return id;
}
@NotNull
public GroupId getGroupId() {
GroupId getGroupId() {
return groupId;
}
void setBody(String body) {
this.body = body;
}
@Nullable
public String getBody() {
return body;
}
long getTime() {
return time;
}
public static ConversationMessageItem from(PrivateMessageHeader h) {
public boolean isRead() {
return read;
}
abstract public boolean isIncoming();
@LayoutRes
abstract public int getLayout();
static ConversationItem from(PrivateMessageHeader h) {
if (h.isLocal()) {
return new ConversationMessageOutItem(h);
} else {
@@ -67,17 +87,40 @@ public abstract class ConversationItem {
}
}
public static ConversationIntroductionItem from(IntroductionRequest ir) {
static ConversationItem from(Context ctx, String contactName,
IntroductionRequest ir) {
if (ir.isLocal()) {
return new ConversationIntroductionOutItem(ir);
String text = ctx.getString(R.string.introduction_request_sent,
contactName, ir.getName());
return new ConversationNoticeOutItem(ir.getMessageId(),
ir.getGroupId(), text, ir.getMessage(), ir.getTimestamp(),
ir.isSent(), ir.isSeen());
} else {
return new ConversationIntroductionInItem(ir);
String text;
if (ir.wasAnswered()) {
text = ctx.getString(
R.string.introduction_request_answered_received,
contactName, ir.getName());
return new ConversationNoticeInItem(ir.getMessageId(),
ir.getGroupId(), text, ir.getMessage(), ir.getTimestamp(),
ir.isRead());
} else if (ir.contactExists()){
text = ctx.getString(
R.string.introduction_request_exists_received,
contactName, ir.getName());
} else {
text = ctx.getString(R.string.introduction_request_received,
contactName, ir.getName());
}
return new ConversationRequestItem(ir.getMessageId(),
ir.getGroupId(), INTRODUCTION, ir.getSessionId(), text,
ir.getMessage(), ir.getTimestamp(), ir.isRead(),
ir.wasAnswered());
}
}
public static ConversationNoticeItem from(Context ctx, String contactName,
static ConversationItem from(Context ctx, String contactName,
IntroductionResponse ir) {
if (ir.isLocal()) {
String text;
if (ir.wasAccepted()) {
@@ -90,7 +133,7 @@ public abstract class ConversationItem {
ir.getName());
}
return new ConversationNoticeOutItem(ir.getMessageId(),
ir.getGroupId(), text, ir.getTimestamp(), ir.isSent(),
ir.getGroupId(), text, null, ir.getTimestamp(), ir.isSent(),
ir.isSeen());
} else {
String text;
@@ -110,143 +153,146 @@ public abstract class ConversationItem {
}
}
return new ConversationNoticeInItem(ir.getMessageId(),
ir.getGroupId(), text, ir.getTimestamp(), ir.isRead());
ir.getGroupId(), text, null, ir.getTimestamp(),
ir.isRead());
}
}
public static ConversationShareableInvitationItem from(
InvitationRequest fim) {
if (fim.isLocal()) {
return new ConversationShareableInvitationOutItem(fim);
static ConversationItem from(Context ctx, String contactName,
InvitationRequest ir) {
if (ir.isLocal()) {
String text;
if (ir instanceof ForumInvitationRequest) {
text = ctx.getString(R.string.forum_invitation_sent,
((ForumInvitationRequest) ir).getForumName(),
contactName);
} else if (ir instanceof BlogInvitationRequest) {
text = ctx.getString(R.string.blogs_sharing_invitation_sent,
((BlogInvitationRequest) ir).getBlogAuthorName(),
contactName);
} else if (ir instanceof GroupInvitationRequest) {
text = ctx.getString(
R.string.groups_invitations_invitation_sent,
contactName,
((GroupInvitationRequest) ir).getGroupName());
} else {
throw new IllegalArgumentException("Unknown InvitationRequest");
}
return new ConversationNoticeOutItem(ir.getId(), ir.getGroupId(),
text, ir.getMessage(), ir.getTimestamp(), ir.isSent(),
ir.isSeen());
} else {
return new ConversationShareableInvitationInItem(fim);
String text;
RequestType type;
if (ir instanceof ForumInvitationRequest) {
text = ctx.getString(R.string.forum_invitation_received,
contactName,
((ForumInvitationRequest) ir).getForumName());
type = FORUM;
} else if (ir instanceof BlogInvitationRequest) {
text = ctx.getString(R.string.blogs_sharing_invitation_received,
contactName,
((BlogInvitationRequest) ir).getBlogAuthorName());
type = BLOG;
} else if (ir instanceof GroupInvitationRequest) {
text = ctx.getString(
R.string.groups_invitations_invitation_received,
contactName,
((GroupInvitationRequest) ir).getGroupName());
type = GROUP;
} else {
throw new IllegalArgumentException("Unknown InvitationRequest");
}
if (!ir.isAvailable()) {
return new ConversationNoticeInItem(ir.getId(), ir.getGroupId(),
text, ir.getMessage(), ir.getTimestamp(), ir.isRead());
}
return new ConversationRequestItem(ir.getId(),
ir.getGroupId(), type, ir.getSessionId(), text,
ir.getMessage(), ir.getTimestamp(), ir.isRead(),
!ir.isAvailable());
}
}
public static ConversationNoticeItem from(Context ctx, String contactName,
static ConversationItem from(Context ctx, String contactName,
InvitationResponse ir) {
if (ir instanceof ForumInvitationResponse) {
return from(ctx, contactName, (ForumInvitationResponse) ir);
} else if (ir instanceof BlogInvitationResponse) {
return from(ctx, contactName, (BlogInvitationResponse) ir);
@StringRes int res;
if (ir.isLocal()) {
if (ir.wasAccepted()) {
if (ir instanceof ForumInvitationResponse) {
res = R.string.forum_invitation_response_accepted_sent;
} else if (ir instanceof BlogInvitationResponse) {
res = R.string.blogs_sharing_response_accepted_sent;
} else if (ir instanceof GroupInvitationResponse) {
res = R.string.groups_invitations_response_accepted_sent;
} else {
throw new IllegalArgumentException(
"Unknown InvitationResponse");
}
} else {
if (ir instanceof ForumInvitationResponse) {
res = R.string.forum_invitation_response_declined_sent;
} else if (ir instanceof BlogInvitationResponse) {
res = R.string.blogs_sharing_response_declined_sent;
} else if (ir instanceof GroupInvitationResponse) {
res = R.string.groups_invitations_response_declined_sent;
} else {
throw new IllegalArgumentException(
"Unknown InvitationResponse");
}
}
String text = ctx.getString(res, contactName);
return new ConversationNoticeOutItem(ir.getId(), ir.getGroupId(),
text, null, ir.getTimestamp(), ir.isSent(), ir.isSeen());
} else {
throw new IllegalArgumentException("Unknown Invitation Response.");
}
}
private static ConversationNoticeItem from(Context ctx, String contactName,
ForumInvitationResponse fir) {
if (fir.isLocal()) {
String text;
if (fir.wasAccepted()) {
text = ctx.getString(
R.string.forum_invitation_response_accepted_sent,
contactName);
if (ir.wasAccepted()) {
if (ir instanceof ForumInvitationResponse) {
res = R.string.forum_invitation_response_accepted_received;
} else if (ir instanceof BlogInvitationResponse) {
res = R.string.blogs_sharing_response_accepted_received;
} else if (ir instanceof GroupInvitationResponse) {
res = R.string.groups_invitations_response_accepted_received;
} else {
throw new IllegalArgumentException(
"Unknown InvitationResponse");
}
} else {
text = ctx.getString(
R.string.forum_invitation_response_declined_sent,
contactName);
if (ir instanceof ForumInvitationResponse) {
res = R.string.forum_invitation_response_declined_received;
} else if (ir instanceof BlogInvitationResponse) {
res = R.string.blogs_sharing_response_declined_received;
} else if (ir instanceof GroupInvitationResponse) {
res = R.string.groups_invitations_response_declined_received;
} else {
throw new IllegalArgumentException(
"Unknown InvitationResponse");
}
}
return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
} else {
String text;
if (fir.wasAccepted()) {
text = ctx.getString(
R.string.forum_invitation_response_accepted_received,
contactName);
} else {
text = ctx.getString(
R.string.forum_invitation_response_declined_received,
contactName);
}
return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
text, fir.getTimestamp(), fir.isRead());
}
}
private static ConversationNoticeItem from(Context ctx, String contactName,
BlogInvitationResponse fir) {
if (fir.isLocal()) {
String text;
if (fir.wasAccepted()) {
text = ctx.getString(
R.string.blogs_sharing_response_accepted_sent,
contactName);
} else {
text = ctx.getString(
R.string.blogs_sharing_response_declined_sent,
contactName);
}
return new ConversationNoticeOutItem(fir.getId(), fir.getGroupId(),
text, fir.getTimestamp(), fir.isSent(), fir.isSeen());
} else {
String text;
if (fir.wasAccepted()) {
text = ctx.getString(
R.string.blogs_sharing_response_accepted_received,
contactName);
} else {
text = ctx.getString(
R.string.blogs_sharing_response_declined_received,
contactName);
}
return new ConversationNoticeInItem(fir.getId(), fir.getGroupId(),
text, fir.getTimestamp(), fir.isRead());
String text = ctx.getString(res, contactName);
return new ConversationNoticeInItem(ir.getId(), ir.getGroupId(),
text, null, ir.getTimestamp(), ir.isRead());
}
}
/**
* This method should not be used to get user-facing objects,
* Its purpose is only to provide data for the contact list.
*/
public static ConversationItem from(IntroductionMessage im) {
if (im.isLocal())
return new ConversationNoticeOutItem(im.getMessageId(),
im.getGroupId(), "", im.getTimestamp(), false, false);
return new ConversationNoticeInItem(im.getMessageId(), im.getGroupId(),
"", im.getTimestamp(), im.isRead());
* This method should not be used to display the resulting ConversationItem
* in the UI, but only to update list information based on the
* BaseMessageHeader.
**/
static ConversationItem from(Context ctx, BaseMessageHeader h) {
if (h instanceof PrivateMessageHeader) {
return from((PrivateMessageHeader) h);
} else if(h instanceof IntroductionRequest) {
return from(ctx, "", (IntroductionRequest) h);
} else if(h instanceof IntroductionResponse) {
return from(ctx, "", (IntroductionResponse) h);
} else if(h instanceof InvitationRequest) {
return from(ctx, "", (InvitationRequest) h);
} else if(h instanceof InvitationResponse) {
return from(ctx, "", (InvitationResponse) h);
} else {
throw new IllegalArgumentException("Unknown message header");
}
}
/**
* This method should not be used to get user-facing objects,
* Its purpose is only to provide data for the contact list.
*/
public static ConversationItem from(InvitationMessage im) {
if (im.isLocal())
return new ConversationNoticeOutItem(im.getId(), im.getGroupId(),
"", im.getTimestamp(), false, false);
return new ConversationNoticeInItem(im.getId(), im.getGroupId(), "",
im.getTimestamp(), im.isRead());
}
interface OutgoingItem {
@NotNull
MessageId getId();
boolean isSent();
void setSent(boolean sent);
boolean isSeen();
void setSeen(boolean seen);
}
interface IncomingItem {
@NotNull
MessageId getId();
@NotNull
GroupId getGroupId();
boolean isRead();
void setRead(boolean read);
}
}

View File

@@ -0,0 +1,42 @@
package org.briarproject.android.contact;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.AndroidUtils;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.util.StringUtils;
@UiThread
@NotNullByDefault
class ConversationItemViewHolder extends ViewHolder {
protected final ViewGroup layout;
private final TextView text;
private final TextView time;
ConversationItemViewHolder(View v) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.layout);
text = (TextView) v.findViewById(R.id.text);
time = (TextView) v.findViewById(R.id.time);
}
@CallSuper
void bind(ConversationItem item) {
if (item.getBody() == null) {
text.setText("\u2026");
} else {
text.setText(StringUtils.trim(item.getBody()));
}
long timestamp = item.getTime();
time.setText(AndroidUtils.formatDate(time.getContext(), timestamp));
}
}

View File

@@ -1,31 +1,30 @@
package org.briarproject.android.contact;
import android.support.annotation.LayoutRes;
import org.briarproject.R;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.nullsafety.NotNullByDefault;
// This class is not thread-safe
class ConversationMessageInItem extends ConversationMessageItem
implements ConversationItem.IncomingItem {
import javax.annotation.concurrent.NotThreadSafe;
private boolean read;
@NotThreadSafe
@NotNullByDefault
class ConversationMessageInItem extends ConversationItem {
ConversationMessageInItem(PrivateMessageHeader header) {
super(header);
read = header.isRead();
ConversationMessageInItem(PrivateMessageHeader h) {
super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isRead());
}
@Override
int getType() {
return MSG_IN;
public boolean isIncoming() {
return true;
}
@LayoutRes
@Override
public boolean isRead() {
return read;
public int getLayout() {
return R.layout.list_item_conversation_msg_in;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -1,29 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
// This class is not thread-safe
abstract class ConversationMessageItem extends ConversationItem {
private final PrivateMessageHeader header;
private byte[] body;
ConversationMessageItem(PrivateMessageHeader header) {
super(header.getId(), header.getGroupId(), header.getTimestamp());
this.header = header;
body = null;
}
PrivateMessageHeader getHeader() {
return header;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
}

View File

@@ -1,42 +1,26 @@
package org.briarproject.android.contact;
import android.support.annotation.LayoutRes;
import org.briarproject.R;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.nullsafety.NotNullByDefault;
// This class is not thread-safe
class ConversationMessageOutItem extends ConversationMessageItem
implements ConversationItem.OutgoingItem {
import javax.annotation.concurrent.NotThreadSafe;
private boolean sent, seen;
@NotThreadSafe
@NotNullByDefault
class ConversationMessageOutItem extends ConversationOutItem {
ConversationMessageOutItem(PrivateMessageHeader header) {
super(header);
sent = header.isSent();
seen = header.isSeen();
ConversationMessageOutItem(PrivateMessageHeader h) {
super(h.getId(), h.getGroupId(), null, h.getTimestamp(), h.isSent(),
h.isSeen());
}
@LayoutRes
@Override
int getType() {
return MSG_OUT;
public int getLayout() {
return R.layout.list_item_conversation_msg_out;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.android.contact;
import android.view.View;
class ConversationMessageOutViewHolder extends ConversationOutItemViewHolder {
ConversationMessageOutViewHolder(View v) {
super(v);
}
@Override
protected boolean hasDarkBackground() {
return true;
}
}

View File

@@ -1,33 +1,43 @@
package org.briarproject.android.contact;
import android.support.annotation.LayoutRes;
import org.briarproject.R;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
// This class is not thread-safe
class ConversationNoticeInItem extends ConversationNoticeItem
implements ConversationItem.IncomingItem {
import javax.annotation.concurrent.NotThreadSafe;
private boolean read;
@NotThreadSafe
@NotNullByDefault
class ConversationNoticeInItem extends ConversationItem {
ConversationNoticeInItem(MessageId id, GroupId groupId, String text,
long time, boolean read) {
super(id, groupId, text, time);
@Nullable
private final String msgText;
this.read = read;
ConversationNoticeInItem(MessageId id, GroupId groupId,
String text, @Nullable String msgText, long time,
boolean read) {
super(id, groupId, text, time, read);
this.msgText = msgText;
}
@Nullable
public String getMsgText() {
return msgText;
}
@Override
int getType() {
return NOTICE_IN;
public boolean isIncoming() {
return true;
}
@LayoutRes
@Override
public boolean isRead() {
return read;
public int getLayout() {
return R.layout.list_item_conversation_notice_in;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.android.contact;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.util.StringUtils;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@UiThread
@NotNullByDefault
class ConversationNoticeInViewHolder extends ConversationItemViewHolder {
private final TextView msgText;
ConversationNoticeInViewHolder(View v) {
super(v);
msgText = (TextView) v.findViewById(R.id.msgText);
}
@Override
void bind(ConversationItem conversationItem) {
super.bind(conversationItem);
ConversationNoticeInItem item =
(ConversationNoticeInItem) conversationItem;
String message = item.getMsgText();
if (StringUtils.isNullOrEmpty(message)) {
msgText.setVisibility(GONE);
layout.setBackgroundResource(R.drawable.notice_in);
} else {
msgText.setVisibility(VISIBLE);
msgText.setText(StringUtils.trim(message));
layout.setBackgroundResource(R.drawable.notice_in_bottom);
}
}
}

View File

@@ -1,20 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
abstract class ConversationNoticeItem extends ConversationItem {
private final String text;
ConversationNoticeItem(MessageId id, GroupId groupId, String text,
long time) {
super(id, groupId, time);
this.text = text;
}
public String getText() {
return text;
}
}

View File

@@ -1,44 +1,38 @@
package org.briarproject.android.contact;
import android.support.annotation.LayoutRes;
import org.briarproject.R;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
// This class is not thread-safe
class ConversationNoticeOutItem extends ConversationNoticeItem
implements ConversationItem.OutgoingItem {
import javax.annotation.concurrent.NotThreadSafe;
private boolean sent, seen;
@NotThreadSafe
@NotNullByDefault
class ConversationNoticeOutItem extends ConversationOutItem {
ConversationNoticeOutItem(MessageId id, GroupId groupId, String text,
long time, boolean sent, boolean seen) {
super(id, groupId, text, time);
@Nullable
private final String msgText;
this.sent = sent;
this.seen = seen;
ConversationNoticeOutItem(MessageId id, GroupId groupId,
String text, @Nullable String msgText, long time,
boolean sent, boolean seen) {
super(id, groupId, text, time, sent, seen);
this.msgText = msgText;
}
@Nullable
public String getMsgText() {
return msgText;
}
@LayoutRes
@Override
int getType() {
return NOTICE_OUT;
public int getLayout() {
return R.layout.list_item_conversation_notice_out;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -0,0 +1,48 @@
package org.briarproject.android.contact;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.util.StringUtils;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@UiThread
@NotNullByDefault
class ConversationNoticeOutViewHolder extends ConversationOutItemViewHolder {
private final TextView msgText;
ConversationNoticeOutViewHolder(View v) {
super(v);
msgText = (TextView) v.findViewById(R.id.msgText);
}
@Override
void bind(ConversationItem conversationItem) {
super.bind(conversationItem);
ConversationNoticeOutItem item =
(ConversationNoticeOutItem) conversationItem;
String message = item.getMsgText();
if (StringUtils.isNullOrEmpty(message)) {
msgText.setVisibility(GONE);
layout.setBackgroundResource(R.drawable.notice_out);
} else {
msgText.setVisibility(VISIBLE);
msgText.setText(StringUtils.trim(message));
layout.setBackgroundResource(R.drawable.notice_out_bottom);
}
}
@Override
protected boolean hasDarkBackground() {
return false;
}
}

View File

@@ -0,0 +1,45 @@
package org.briarproject.android.contact;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
abstract class ConversationOutItem extends ConversationItem {
private boolean sent, seen;
ConversationOutItem(MessageId id, GroupId groupId, @Nullable String text,
long time, boolean sent, boolean seen) {
super(id, groupId, text, time, true);
this.sent = sent;
this.seen = seen;
}
public boolean isSent() {
return sent;
}
public void setSent(boolean sent) {
this.sent = sent;
}
public boolean isSeen() {
return seen;
}
public void setSeen(boolean seen) {
this.seen = seen;
}
@Override
public boolean isIncoming() {
return false;
}
}

View File

@@ -0,0 +1,44 @@
package org.briarproject.android.contact;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.ImageView;
import org.briarproject.R;
import org.briarproject.api.nullsafety.NotNullByDefault;
@UiThread
@NotNullByDefault
abstract class ConversationOutItemViewHolder
extends ConversationItemViewHolder {
private final ImageView status;
ConversationOutItemViewHolder(View v) {
super(v);
status = (ImageView) v.findViewById(R.id.status);
}
@Override
void bind(ConversationItem conversationItem) {
super.bind(conversationItem);
ConversationOutItem item = (ConversationOutItem) conversationItem;
int res;
if (item.isSeen()) {
if (hasDarkBackground()) res = R.drawable.message_delivered_white;
else res = R.drawable.message_delivered;
} else if (item.isSent()) {
if (hasDarkBackground()) res = R.drawable.message_sent_white;
else res = R.drawable.message_sent;
} else {
if (hasDarkBackground()) res = R.drawable.message_stored_white;
else res = R.drawable.message_stored;
}
status.setImageResource(res);
}
protected abstract boolean hasDarkBackground();
}

View File

@@ -0,0 +1,55 @@
package org.briarproject.android.contact;
import android.support.annotation.LayoutRes;
import org.briarproject.R;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@NotNullByDefault
class ConversationRequestItem extends ConversationNoticeInItem {
enum RequestType { INTRODUCTION, FORUM, BLOG, GROUP };
private final RequestType requestType;
private final SessionId sessionId;
private boolean answered;
ConversationRequestItem(MessageId id, GroupId groupId,
RequestType requestType, SessionId sessionId, String text,
@Nullable String msgText, long time, boolean read,
boolean answered) {
super(id, groupId, text, msgText, time, read);
this.requestType = requestType;
this.sessionId = sessionId;
this.answered = answered;
}
public RequestType getRequestType() {
return requestType;
}
public SessionId getSessionId() {
return sessionId;
}
boolean wasAnswered() {
return answered;
}
void setAnswered(boolean answered) {
this.answered = answered;
}
@LayoutRes
@Override
public int getLayout() {
return R.layout.list_item_conversation_request;
}
}

View File

@@ -0,0 +1,58 @@
package org.briarproject.android.contact;
import android.support.annotation.UiThread;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import org.briarproject.R;
import org.briarproject.android.contact.ConversationAdapter.RequestListener;
import org.briarproject.api.nullsafety.NotNullByDefault;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@UiThread
@NotNullByDefault
class ConversationRequestViewHolder extends ConversationNoticeInViewHolder {
private final Button acceptButton;
private final Button declineButton;
ConversationRequestViewHolder(View v) {
super(v);
acceptButton = (Button) v.findViewById(R.id.acceptButton);
declineButton = (Button) v.findViewById(R.id.declineButton);
}
void bind(ConversationItem conversationItem,
final RequestListener listener) {
super.bind(conversationItem);
final ConversationRequestItem item =
(ConversationRequestItem) conversationItem;
if (item.wasAnswered()) {
acceptButton.setVisibility(GONE);
declineButton.setVisibility(GONE);
} else {
acceptButton.setVisibility(VISIBLE);
acceptButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
item.setAnswered(true);
listener.respondToRequest(item, true);
}
});
declineButton.setVisibility(VISIBLE);
declineButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
item.setAnswered(true);
listener.respondToRequest(item, false);
}
});
}
}
}

View File

@@ -1,43 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.blogs.BlogInvitationRequest;
import org.briarproject.api.forum.ForumInvitationRequest;
import org.briarproject.api.sharing.InvitationRequest;
// This class is not thread-safe
class ConversationShareableInvitationInItem
extends ConversationShareableInvitationItem
implements ConversationItem.IncomingItem {
private final int type;
private boolean read;
ConversationShareableInvitationInItem(InvitationRequest ir) {
super(ir);
if (ir instanceof ForumInvitationRequest) {
this.type = FORUM_INVITATION_IN;
} else if (ir instanceof BlogInvitationRequest) {
this.type = BLOG_INVITATION_IN;
} else {
throw new IllegalArgumentException("Unknown Invitation Type.");
}
this.read = ir.isRead();
}
@Override
int getType() {
return type;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -1,18 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.sharing.InvitationRequest;
abstract class ConversationShareableInvitationItem extends ConversationItem {
private final InvitationRequest fim;
ConversationShareableInvitationItem(InvitationRequest fim) {
super(fim.getId(), fim.getGroupId(), fim.getTimestamp());
this.fim = fim;
}
InvitationRequest getInvitationRequest() {
return fim;
}
}

View File

@@ -1,60 +0,0 @@
package org.briarproject.android.contact;
import org.briarproject.api.blogs.BlogInvitationRequest;
import org.briarproject.api.forum.ForumInvitationRequest;
import org.briarproject.api.sharing.InvitationRequest;
/**
* This class is needed and can not be replaced by an ConversationNoticeOutItem,
* because it carries the optional invitation message
* to be displayed as a regular private message.
* <p/>
* This class is not thread-safe
*/
class ConversationShareableInvitationOutItem
extends ConversationShareableInvitationItem
implements ConversationItem.OutgoingItem {
private final int type;
private boolean sent, seen;
ConversationShareableInvitationOutItem(InvitationRequest ir) {
super(ir);
if (ir instanceof ForumInvitationRequest) {
this.type = FORUM_INVITATION_OUT;
} else if (ir instanceof BlogInvitationRequest) {
this.type = BLOG_INVITATION_OUT;
} else {
throw new IllegalArgumentException("Unknown Invitation Type.");
}
this.sent = ir.isSent();
this.seen = ir.isSeen();
}
@Override
int getType() {
return type;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -19,7 +19,7 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.sharing.SharingStatusForumActivity;
import org.briarproject.android.sharing.ForumSharingStatusActivity;
import org.briarproject.android.threaded.ThreadListActivity;
import org.briarproject.android.threaded.ThreadListController;
import org.briarproject.api.db.DbException;
@@ -114,7 +114,7 @@ public class ForumActivity extends
REQUEST_FORUM_SHARED, options.toBundle());
return true;
case R.id.action_forum_sharing_status:
Intent i3 = new Intent(this, SharingStatusForumActivity.class);
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
ActivityCompat.startActivity(this, i3, options.toBundle());

View File

@@ -18,7 +18,7 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.sharing.InvitationsForumActivity;
import org.briarproject.android.sharing.ForumInvitationActivity;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.db.DbException;
@@ -220,10 +220,10 @@ public class ForumListFragment extends BaseEventFragment implements
if (availableCount == 0) {
snackbar.dismiss();
} else {
snackbar.show();
snackbar.setText(getResources().getQuantityString(
R.plurals.forums_shared, availableCount,
availableCount));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
}
});
@@ -286,7 +286,7 @@ public class ForumListFragment extends BaseEventFragment implements
@Override
public void onClick(View view) {
// snackbar click
Intent i = new Intent(getContext(), InvitationsForumActivity.class);
Intent i = new Intent(getContext(), ForumInvitationActivity.class);
startActivity(i);
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.android.privategroup.invitation;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.sharing.InvitationActivity;
import org.briarproject.android.sharing.InvitationAdapter;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
import javax.inject.Inject;
import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
public class GroupInvitationActivity
extends InvitationActivity<GroupInvitationItem> {
@Inject
protected GroupInvitationController controller;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
protected GroupInvitationController getController() {
return controller;
}
@Override
protected InvitationAdapter<GroupInvitationItem, ?> getAdapter(Context ctx,
InvitationClickListener<GroupInvitationItem> listener) {
return new GroupInvitationAdapter(ctx, listener);
}
@Override
protected int getAcceptRes() {
return R.string.groups_invitations_joined;
}
@Override
protected int getDeclineRes() {
return R.string.groups_invitations_declined;
}
}

View File

@@ -0,0 +1,28 @@
package org.briarproject.android.privategroup.invitation;
import android.content.Context;
import android.view.ViewGroup;
import org.briarproject.android.sharing.InvitationAdapter;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
class GroupInvitationAdapter extends
InvitationAdapter<GroupInvitationItem, GroupInvitationViewHolder> {
GroupInvitationAdapter(Context ctx,
InvitationClickListener<GroupInvitationItem> listener) {
super(ctx, GroupInvitationItem.class, listener);
}
@Override
public GroupInvitationViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
return new GroupInvitationViewHolder(getView(parent));
}
@Override
public boolean areContentsTheSame(GroupInvitationItem item1,
GroupInvitationItem item2) {
return item1.isSubscribed() == item2.isSubscribed();
}
}

View File

@@ -0,0 +1,8 @@
package org.briarproject.android.privategroup.invitation;
import org.briarproject.android.sharing.InvitationController;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
public interface GroupInvitationController
extends InvitationController<GroupInvitationItem> {
}

View File

@@ -0,0 +1,83 @@
package org.briarproject.android.privategroup.invitation;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.android.sharing.InvitationControllerImpl;
import org.briarproject.api.contact.Contact;
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.GroupInvitationReceivedEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sync.ClientId;
import java.util.Collection;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
public class GroupInvitationControllerImpl
extends InvitationControllerImpl<GroupInvitationItem>
implements GroupInvitationController {
private final PrivateGroupManager privateGroupManager;
private final GroupInvitationManager groupInvitationManager;
@Inject
GroupInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
PrivateGroupManager privateGroupManager,
GroupInvitationManager groupInvitationManager) {
super(dbExecutor, lifecycleManager, eventBus);
this.privateGroupManager = privateGroupManager;
this.groupInvitationManager = groupInvitationManager;
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof GroupInvitationReceivedEvent) {
LOG.info("Group invitation received, reloading");
listener.loadInvitations(false);
}
}
@Override
protected ClientId getShareableClientId() {
return privateGroupManager.getClientId();
}
@Override
protected Collection<GroupInvitationItem> getInvitations()
throws DbException {
return groupInvitationManager.getInvitations();
}
@Override
public void respondToInvitation(final GroupInvitationItem item,
final boolean accept,
final ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
PrivateGroup g = item.getShareable();
Contact c = item.getCreator();
groupInvitationManager.respondToInvitation(g, c, accept);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
}

View File

@@ -0,0 +1,28 @@
package org.briarproject.android.privategroup.invitation;
import android.support.annotation.Nullable;
import android.view.View;
import org.briarproject.R;
import org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
import org.briarproject.android.sharing.InvitationViewHolder;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
public class GroupInvitationViewHolder extends InvitationViewHolder<GroupInvitationItem> {
public GroupInvitationViewHolder(View v) {
super(v);
}
@Override
public void onBind(@Nullable final GroupInvitationItem item,
final InvitationClickListener<GroupInvitationItem> listener) {
super.onBind(item, listener);
if (item == null) return;
sharedBy.setText(
sharedBy.getContext().getString(R.string.groups_created_by,
item.getCreator().getAuthor().getName()));
}
}

View File

@@ -30,6 +30,9 @@ public interface GroupListController extends DbController {
void removeGroup(GroupId g,
ResultExceptionHandler<Void, DbException> result);
void loadAvailableGroups(
ResultExceptionHandler<Integer, DbException> result);
interface GroupListListener extends DestroyableContext {
@UiThread

View File

@@ -20,6 +20,7 @@ import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.privategroup.GroupMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
@@ -41,6 +42,7 @@ public class GroupListControllerImpl extends DbControllerImpl
Logger.getLogger(GroupListControllerImpl.class.getName());
private final PrivateGroupManager groupManager;
private final GroupInvitationManager groupInvitationManager;
private final EventBus eventBus;
private final AndroidNotificationManager notificationManager;
private final IdentityManager identityManager;
@@ -50,10 +52,12 @@ public class GroupListControllerImpl extends DbControllerImpl
@Inject
GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
EventBus eventBus, AndroidNotificationManager notificationManager,
GroupInvitationManager groupInvitationManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
IdentityManager identityManager) {
super(dbExecutor, lifecycleManager);
this.groupManager = groupManager;
this.groupInvitationManager = groupInvitationManager;
this.eventBus = eventBus;
this.notificationManager = notificationManager;
this.identityManager = identityManager;
@@ -187,4 +191,22 @@ public class GroupListControllerImpl extends DbControllerImpl
});
}
@Override
public void loadAvailableGroups(
final ResultExceptionHandler<Integer, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
handler.onResult(
groupInvitationManager.getInvitations().size());
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
}

View File

@@ -4,13 +4,16 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import org.briarproject.R;
@@ -18,6 +21,7 @@ import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.android.privategroup.list.GroupListController.GroupListListener;
import org.briarproject.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.android.view.BriarRecyclerView;
@@ -30,10 +34,11 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
public class GroupListFragment extends BaseFragment implements
GroupListListener, OnGroupRemoveClickListener {
GroupListListener, OnGroupRemoveClickListener, OnClickListener {
public final static String TAG = GroupListFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@@ -47,6 +52,7 @@ public class GroupListFragment extends BaseFragment implements
private BriarRecyclerView list;
private GroupListAdapter adapter;
private Snackbar snackbar;
@Nullable
@Override
@@ -61,6 +67,12 @@ public class GroupListFragment extends BaseFragment implements
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);
snackbar = Snackbar.make(list, "", LENGTH_INDEFINITE);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.setAction(R.string.show, this);
snackbar.setActionTextColor(ContextCompat
.getColor(getContext(), R.color.briar_button_positive));
return v;
}
@@ -76,6 +88,7 @@ public class GroupListFragment extends BaseFragment implements
controller.onStart();
list.startPeriodicUpdate();
loadGroups();
loadAvailableGroups();
}
@Override
@@ -180,4 +193,40 @@ public class GroupListFragment extends BaseFragment implements
});
}
private void loadAvailableGroups() {
controller.loadAvailableGroups(
new UiResultExceptionHandler<Integer, DbException>(this) {
@Override
public void onResultUi(Integer num) {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.groups_invitations_open, num,
num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
}
@Override
public void onExceptionUi(DbException exception) {
// TODO handle this error
finish();
}
});
}
/**
* This method is handling the available groups snackbar action
*/
@Override
public void onClick(View v) {
Intent i = new Intent(getContext(), GroupInvitationActivity.class);
ActivityOptionsCompat options =
makeCustomAnimation(getActivity(),
android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
startActivity(i, options.toBundle());
}
}

View File

@@ -0,0 +1,46 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.api.sharing.SharingInvitationItem;
import javax.inject.Inject;
import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
public class BlogInvitationActivity
extends InvitationActivity<SharingInvitationItem> {
@Inject
BlogInvitationController controller;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
protected InvitationController<SharingInvitationItem> getController() {
return controller;
}
@Override
protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
Context ctx,
InvitationClickListener<SharingInvitationItem> listener) {
return new SharingInvitationAdapter(ctx, listener);
}
@Override
protected int getAcceptRes() {
return R.string.blogs_sharing_joined_toast;
}
@Override
protected int getDeclineRes() {
return R.string.blogs_sharing_declined_toast;
}
}

View File

@@ -1,40 +0,0 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.sharing.InvitationItem;
class BlogInvitationAdapter extends InvitationAdapter {
BlogInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
super(ctx, listener);
}
@Override
public void onBindViewHolder(InvitationsViewHolder ui, int position) {
super.onBindViewHolder(ui, position);
InvitationItem item = getItemAt(position);
if (item == null) return;
Blog blog = (Blog) item.getShareable();
ui.avatar.setAuthorAvatar(blog.getAuthor());
ui.name.setText(ctx.getString(R.string.blogs_personal_blog,
blog.getAuthor().getName()));
if (item.isSubscribed()) {
ui.subscribed.setText(ctx.getString(R.string.blogs_sharing_exists));
}
}
@Override
public int compare(InvitationItem o1, InvitationItem o2) {
return String.CASE_INSENSITIVE_ORDER
.compare(((Blog) o1.getShareable()).getAuthor().getName(),
((Blog) o2.getShareable()).getAuthor().getName());
}
}

View File

@@ -0,0 +1,7 @@
package org.briarproject.android.sharing;
import org.briarproject.api.sharing.SharingInvitationItem;
public interface BlogInvitationController
extends InvitationController<SharingInvitationItem> {
}

View File

@@ -0,0 +1,82 @@
package org.briarproject.android.sharing;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogSharingManager;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.BlogInvitationReceivedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.SharingInvitationItem;
import org.briarproject.api.sync.ClientId;
import java.util.Collection;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
public class BlogInvitationControllerImpl
extends InvitationControllerImpl<SharingInvitationItem>
implements BlogInvitationController {
private final BlogManager blogManager;
private final BlogSharingManager blogSharingManager;
@Inject
BlogInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
BlogManager blogManager, BlogSharingManager blogSharingManager) {
super(dbExecutor, lifecycleManager, eventBus);
this.blogManager = blogManager;
this.blogSharingManager = blogSharingManager;
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof BlogInvitationReceivedEvent) {
LOG.info("Blog invitation received, reloading");
listener.loadInvitations(false);
}
}
@Override
protected ClientId getShareableClientId() {
return blogManager.getClientId();
}
@Override
protected Collection<SharingInvitationItem> getInvitations() throws DbException {
return blogSharingManager.getInvitations();
}
@Override
public void respondToInvitation(final SharingInvitationItem item,
final boolean accept,
final ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Blog f = (Blog) item.getShareable();
for (Contact c : item.getNewSharers()) {
// TODO: What happens if a contact has been removed?
blogSharingManager.respondToInvitation(f, c, accept);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
}

View File

@@ -9,7 +9,7 @@ import java.util.Collection;
import javax.inject.Inject;
public class SharingStatusBlogActivity extends SharingStatusActivity {
public class BlogSharingStatusActivity extends SharingStatusActivity {
// Fields that are accessed from background threads must be volatile
@Inject

View File

@@ -0,0 +1,46 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.api.sharing.SharingInvitationItem;
import javax.inject.Inject;
import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
public class ForumInvitationActivity
extends InvitationActivity<SharingInvitationItem> {
@Inject
ForumInvitationController controller;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
protected InvitationController<SharingInvitationItem> getController() {
return controller;
}
@Override
protected InvitationAdapter<SharingInvitationItem, ?> getAdapter(
Context ctx,
InvitationClickListener<SharingInvitationItem> listener) {
return new SharingInvitationAdapter(ctx, listener);
}
@Override
protected int getAcceptRes() {
return R.string.forum_joined_toast;
}
@Override
protected int getDeclineRes() {
return R.string.forum_declined_toast;
}
}

View File

@@ -1,35 +0,0 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.sharing.InvitationItem;
class ForumInvitationAdapter extends InvitationAdapter {
ForumInvitationAdapter(Context ctx, AvailableForumClickListener listener) {
super(ctx, listener);
}
@Override
public void onBindViewHolder(InvitationsViewHolder ui, int position) {
super.onBindViewHolder(ui, position);
InvitationItem item = getItemAt(position);
if (item == null) return;
Forum forum = (Forum) item.getShareable();
ui.avatar.setText(forum.getName().substring(0, 1));
ui.avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
ui.name.setText(forum.getName());
}
@Override
public int compare(InvitationItem o1, InvitationItem o2) {
return String.CASE_INSENSITIVE_ORDER
.compare(((Forum) o1.getShareable()).getName(),
((Forum) o2.getShareable()).getName());
}
}

View File

@@ -0,0 +1,7 @@
package org.briarproject.android.sharing;
import org.briarproject.api.sharing.SharingInvitationItem;
public interface ForumInvitationController
extends InvitationController<SharingInvitationItem> {
}

View File

@@ -0,0 +1,83 @@
package org.briarproject.android.sharing;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.contact.Contact;
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.ForumInvitationReceivedEvent;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.SharingInvitationItem;
import org.briarproject.api.sync.ClientId;
import java.util.Collection;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
public class ForumInvitationControllerImpl
extends InvitationControllerImpl<SharingInvitationItem>
implements ForumInvitationController {
private final ForumManager forumManager;
private final ForumSharingManager forumSharingManager;
@Inject
ForumInvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
ForumManager forumManager,
ForumSharingManager forumSharingManager) {
super(dbExecutor, lifecycleManager, eventBus);
this.forumManager = forumManager;
this.forumSharingManager = forumSharingManager;
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof ForumInvitationReceivedEvent) {
LOG.info("Forum invitation received, reloading");
listener.loadInvitations(false);
}
}
@Override
protected ClientId getShareableClientId() {
return forumManager.getClientId();
}
@Override
protected Collection<SharingInvitationItem> getInvitations() throws DbException {
return forumSharingManager.getInvitations();
}
@Override
public void respondToInvitation(final SharingInvitationItem item,
final boolean accept,
final ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Forum f = (Forum) item.getShareable();
for (Contact c : item.getNewSharers()) {
// TODO: What happens if a contact has been removed?
forumSharingManager.respondToInvitation(f, c, accept);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
}

View File

@@ -9,7 +9,7 @@ import java.util.Collection;
import javax.inject.Inject;
public class SharingStatusForumActivity extends SharingStatusActivity {
public class ForumSharingStatusActivity extends SharingStatusActivity {
// Fields that are accessed from background threads must be volatile
@Inject

View File

@@ -2,39 +2,34 @@ package org.briarproject.android.sharing;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.StringRes;
import android.support.v7.widget.LinearLayoutManager;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.sharing.InvitationController.InvitationListener;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sharing.InvitationItem;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
import static org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
abstract class InvitationsActivity extends BriarActivity
implements EventListener, AvailableForumClickListener {
public abstract class InvitationActivity<I extends InvitationItem>
extends BriarActivity
implements InvitationListener, InvitationClickListener<I> {
protected static final Logger LOG =
Logger.getLogger(InvitationsActivity.class.getName());
Logger.getLogger(InvitationActivity.class.getName());
protected InvitationAdapter adapter;
private InvitationAdapter<I, ?> adapter;
private BriarRecyclerView list;
@Inject
EventBus eventBus;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
@@ -42,7 +37,6 @@ abstract class InvitationsActivity extends BriarActivity
setContentView(R.layout.list);
adapter = getAdapter(this, this);
list = (BriarRecyclerView) findViewById(R.id.list);
if (list != null) {
list.setLayoutManager(new LinearLayoutManager(this));
@@ -50,32 +44,24 @@ abstract class InvitationsActivity extends BriarActivity
}
}
abstract protected InvitationAdapter<I, ?> getAdapter(Context ctx,
InvitationClickListener<I> listener);
@Override
public void onStart() {
super.onStart();
eventBus.addListener(this);
loadInvitations(false);
}
@Override
public void onStop() {
super.onStop();
eventBus.removeListener(this);
adapter.clear();
list.showProgressBar();
}
@Override
@CallSuper
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed, reloading...");
loadInvitations(true);
}
}
@Override
public void onItemClick(InvitationItem item, boolean accept) {
public void onItemClick(I item, boolean accept) {
respondToInvitation(item, accept);
// show toast
@@ -91,26 +77,58 @@ abstract class InvitationsActivity extends BriarActivity
}
}
abstract protected InvitationAdapter getAdapter(Context ctx,
AvailableForumClickListener listener);
@Override
public void loadInvitations(final boolean clear) {
final int revision = adapter.getRevision();
getController().loadInvitations(clear,
new UiResultExceptionHandler<Collection<I>, DbException>(
this) {
@Override
public void onResultUi(Collection<I> items) {
displayInvitations(revision, items, clear);
}
abstract protected void loadInvitations(boolean clear);
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
}
});
}
abstract protected void respondToInvitation(final InvitationItem item,
final boolean accept);
abstract protected InvitationController<I> getController();
protected void respondToInvitation(final I item,
final boolean accept) {
getController().respondToInvitation(item, accept,
new UiResultExceptionHandler<Void, DbException>(this) {
@Override
public void onResultUi(Void result) {
}
@Override
public void onExceptionUi(DbException exception) {
// TODO proper error handling
finish();
}
});
}
@StringRes
abstract protected int getAcceptRes();
@StringRes
abstract protected int getDeclineRes();
protected void displayInvitations(final int revision,
final Collection<InvitationItem> invitations, final boolean clear) {
final Collection<I> invitations, final boolean clear) {
runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
if (invitations.isEmpty()) {
LOG.info("No more invitations available, finishing");
finish();
supportFinishAfterTransition();
} else if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (clear) adapter.setItems(invitations);

View File

@@ -1,111 +1,51 @@
package org.briarproject.android.sharing;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.BriarAdapter;
import org.briarproject.android.view.TextAvatarView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
public abstract class InvitationAdapter<I extends InvitationItem, VH extends InvitationViewHolder<I>>
extends BriarAdapter<I, VH> {
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
private final InvitationClickListener<I> listener;
abstract class InvitationAdapter extends
BriarAdapter<InvitationItem, InvitationAdapter.InvitationsViewHolder> {
private final AvailableForumClickListener listener;
InvitationAdapter(Context ctx, AvailableForumClickListener listener) {
super(ctx, InvitationItem.class);
public InvitationAdapter(Context ctx, Class<I> c,
InvitationClickListener<I> listener) {
super(ctx, c);
this.listener = listener;
}
@Override
public InvitationsViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(ctx)
.inflate(R.layout.list_item_invitations, parent, false);
return new InvitationsViewHolder(v);
protected View getView(ViewGroup parent) {
return LayoutInflater.from(ctx)
.inflate(R.layout.list_item_invitations, parent, false);
}
@Override
public void onBindViewHolder(InvitationsViewHolder ui, int position) {
final InvitationItem item = getItemAt(position);
public void onBindViewHolder(VH ui, int position) {
final I item = getItemAt(position);
if (item == null) return;
Collection<String> names = new ArrayList<>();
for (Contact c : item.getNewSharers())
names.add(c.getAuthor().getName());
ui.sharedBy.setText(ctx.getString(R.string.shared_by_format,
StringUtils.join(names, ", ")));
if (item.isSubscribed()) {
ui.subscribed.setVisibility(VISIBLE);
} else {
ui.subscribed.setVisibility(GONE);
}
ui.accept.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(item, true);
}
});
ui.decline.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(item, false);
}
});
ui.onBind(item, listener);
}
@Override
public boolean areContentsTheSame(InvitationItem oldItem,
InvitationItem newItem) {
return oldItem.isSubscribed() == newItem.isSubscribed() &&
oldItem.getNewSharers().equals(newItem.getNewSharers());
}
@Override
public boolean areItemsTheSame(InvitationItem oldItem,
InvitationItem newItem) {
public boolean areItemsTheSame(I oldItem, I newItem) {
return oldItem.getShareable().equals(newItem.getShareable());
}
static class InvitationsViewHolder extends RecyclerView.ViewHolder {
final TextAvatarView avatar;
final TextView name;
private final TextView sharedBy;
final TextView subscribed;
private final Button accept;
private final Button decline;
private InvitationsViewHolder(View v) {
super(v);
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.forumNameView);
sharedBy = (TextView) v.findViewById(R.id.sharedByView);
subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
accept = (Button) v.findViewById(R.id.acceptButton);
decline = (Button) v.findViewById(R.id.declineButton);
}
@Override
public int compare(I o1, I o2) {
return String.CASE_INSENSITIVE_ORDER
.compare((o1.getShareable()).getName(),
(o2.getShareable()).getName());
}
interface AvailableForumClickListener {
void onItemClick(InvitationItem item, boolean accept);
public interface InvitationClickListener<I> {
void onItemClick(I item, boolean accept);
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.android.sharing;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sharing.InvitationItem;
import java.util.Collection;
public interface InvitationController<I extends InvitationItem>
extends ActivityLifecycleController {
void loadInvitations(boolean clear,
ResultExceptionHandler<Collection<I>, DbException> handler);
void respondToInvitation(I item, boolean accept,
ResultExceptionHandler<Void, DbException> handler);
interface InvitationListener {
void loadInvitations(boolean clear);
}
}

View File

@@ -0,0 +1,116 @@
package org.briarproject.android.sharing;
import android.app.Activity;
import android.support.annotation.CallSuper;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.GroupAddedEvent;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sync.ClientId;
import java.util.ArrayList;
import java.util.Collection;
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 InvitationControllerImpl<I extends InvitationItem>
extends DbControllerImpl
implements InvitationController<I>, EventListener {
protected static final Logger LOG =
Logger.getLogger(InvitationControllerImpl.class.getName());
private final EventBus eventBus;
protected InvitationListener listener;
public InvitationControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus) {
super(dbExecutor, lifecycleManager);
this.eventBus = eventBus;
}
@Override
public void onActivityCreate(Activity activity) {
listener = (InvitationListener) activity;
}
@Override
public void onActivityStart() {
eventBus.addListener(this);
}
@Override
public void onActivityStop() {
eventBus.removeListener(this);
}
@Override
public void onActivityDestroy() {
}
@CallSuper
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactRemovedEvent) {
LOG.info("Contact removed, reloading...");
listener.loadInvitations(true);
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(getShareableClientId())) {
LOG.info("Group added, reloading");
listener.loadInvitations(false);
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(getShareableClientId())) {
LOG.info("Group removed, reloading");
listener.loadInvitations(false);
}
}
}
protected abstract ClientId getShareableClientId();
@Override
public void loadInvitations(final boolean clear,
final ResultExceptionHandler<Collection<I>, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
Collection<I> invitations = new ArrayList<>();
try {
long now = System.currentTimeMillis();
invitations.addAll(getInvitations());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info(
"Loading invitations took " + duration + " ms");
handler.onResult(invitations);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@DatabaseExecutor
protected abstract Collection<I> getInvitations() throws DbException;
}

View File

@@ -0,0 +1,69 @@
package org.briarproject.android.sharing;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.sharing.InvitationAdapter.InvitationClickListener;
import org.briarproject.android.view.TextAvatarView;
import org.briarproject.api.sharing.InvitationItem;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class InvitationViewHolder<I extends InvitationItem>
extends RecyclerView.ViewHolder {
private final TextAvatarView avatar;
private final TextView name;
protected final TextView sharedBy;
private final TextView subscribed;
private final Button accept;
private final Button decline;
public InvitationViewHolder(View v) {
super(v);
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.forumNameView);
sharedBy = (TextView) v.findViewById(R.id.sharedByView);
subscribed = (TextView) v.findViewById(R.id.forumSubscribedView);
accept = (Button) v.findViewById(R.id.acceptButton);
decline = (Button) v.findViewById(R.id.declineButton);
}
@CallSuper
public void onBind(@Nullable final I item,
final InvitationClickListener<I> listener) {
if (item == null) return;
avatar.setText(item.getShareable().getName().substring(0, 1));
avatar.setBackgroundBytes(item.getShareable().getId().getBytes());
name.setText(item.getShareable().getName());
if (item.isSubscribed()) {
subscribed.setVisibility(VISIBLE);
} else {
subscribed.setVisibility(GONE);
}
accept.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(item, true);
}
});
decline.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(item, false);
}
});
}
}

View File

@@ -1,122 +0,0 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogSharingManager;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.BlogInvitationReceivedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.GroupAddedEvent;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sync.ClientId;
import java.util.ArrayList;
import java.util.Collection;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
public class InvitationsBlogActivity extends InvitationsActivity {
// Fields that are accessed from background threads must be volatile
@Inject
volatile BlogManager blogManager;
@Inject
volatile BlogSharingManager blogSharingManager;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(blogManager.getClientId())) {
LOG.info("Blog added, reloading");
loadInvitations(false);
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(blogManager.getClientId())) {
LOG.info("Blog removed, reloading");
loadInvitations(false);
}
} else if (e instanceof BlogInvitationReceivedEvent) {
LOG.info("Blog invitation received, reloading");
loadInvitations(false);
}
}
@Override
protected InvitationAdapter getAdapter(Context ctx,
AvailableForumClickListener listener) {
return new BlogInvitationAdapter(ctx, listener);
}
@Override
protected void loadInvitations(final boolean clear) {
final int revision = adapter.getRevision();
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Collection<InvitationItem> invitations = new ArrayList<>();
long now = System.currentTimeMillis();
invitations.addAll(blogSharingManager.getInvitations());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayInvitations(revision, invitations, clear);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
protected void respondToInvitation(final InvitationItem item,
final boolean accept) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Blog b = (Blog) item.getShareable();
for (Contact c : item.getNewSharers()) {
// TODO: What happens if a contact has been removed?
blogSharingManager.respondToInvitation(b, c, accept);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
protected int getAcceptRes() {
return R.string.blogs_sharing_joined_toast;
}
@Override
protected int getDeclineRes() {
return R.string.blogs_sharing_declined_toast;
}
}

View File

@@ -1,122 +0,0 @@
package org.briarproject.android.sharing;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.ForumInvitationReceivedEvent;
import org.briarproject.api.event.GroupAddedEvent;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sync.ClientId;
import java.util.ArrayList;
import java.util.Collection;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.sharing.InvitationAdapter.AvailableForumClickListener;
public class InvitationsForumActivity extends InvitationsActivity {
// Fields that are accessed from background threads must be volatile
@Inject
volatile ForumManager forumManager;
@Inject
volatile ForumSharingManager forumSharingManager;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(forumManager.getClientId())) {
LOG.info("Forum added, reloading");
loadInvitations(false);
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
ClientId cId = g.getGroup().getClientId();
if (cId.equals(forumManager.getClientId())) {
LOG.info("Forum removed, reloading");
loadInvitations(false);
}
} else if (e instanceof ForumInvitationReceivedEvent) {
LOG.info("Forum invitation received, reloading");
loadInvitations(false);
}
}
@Override
protected InvitationAdapter getAdapter(Context ctx,
AvailableForumClickListener listener) {
return new ForumInvitationAdapter(ctx, listener);
}
@Override
protected void loadInvitations(final boolean clear) {
final int revision = adapter.getRevision();
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Collection<InvitationItem> invitations = new ArrayList<>();
long now = System.currentTimeMillis();
invitations.addAll(forumSharingManager.getInvitations());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayInvitations(revision, invitations, clear);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
protected void respondToInvitation(final InvitationItem item,
final boolean accept) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Forum f = (Forum) item.getShareable();
for (Contact c : item.getNewSharers()) {
// TODO: What happens if a contact has been removed?
forumSharingManager.respondToInvitation(f, c, accept);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
@Override
protected int getAcceptRes() {
return R.string.forum_joined_toast;
}
@Override
protected int getDeclineRes() {
return R.string.forum_declined_toast;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.android.sharing;
import android.content.Context;
import android.view.ViewGroup;
import org.briarproject.api.sharing.SharingInvitationItem;
class SharingInvitationAdapter extends
InvitationAdapter<SharingInvitationItem, SharingInvitationViewHolder> {
SharingInvitationAdapter(Context ctx, InvitationClickListener listener) {
super(ctx, SharingInvitationItem.class, listener);
}
@Override
public SharingInvitationViewHolder onCreateViewHolder(
ViewGroup parent,
int viewType) {
return new SharingInvitationViewHolder(getView(parent));
}
@Override
public boolean areContentsTheSame(SharingInvitationItem oldItem,
SharingInvitationItem newItem) {
return oldItem.isSubscribed() == newItem.isSubscribed() &&
oldItem.getNewSharers().equals(newItem.getNewSharers());
}
}

View File

@@ -0,0 +1,35 @@
package org.briarproject.android.sharing;
import android.support.annotation.Nullable;
import android.view.View;
import org.briarproject.R;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.sharing.SharingInvitationItem;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
public class SharingInvitationViewHolder
extends InvitationViewHolder<SharingInvitationItem> {
public SharingInvitationViewHolder(View v) {
super(v);
}
@Override
public void onBind(@Nullable final SharingInvitationItem item,
final InvitationAdapter.InvitationClickListener<SharingInvitationItem> listener) {
super.onBind(item, listener);
if (item == null) return;
Collection<String> names = new ArrayList<>();
for (Contact c : item.getNewSharers())
names.add(c.getAuthor().getName());
sharedBy.setText(
sharedBy.getContext().getString(R.string.shared_by_format,
StringUtils.join(names, ", ")));
}
}

View File

@@ -1,21 +1,26 @@
package org.briarproject.api.clients;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public abstract class BaseMessage {
private final Message message;
@Nullable
private final MessageId parent;
public BaseMessage(@NotNull Message message, @Nullable MessageId parent) {
public BaseMessage(Message message, @Nullable MessageId parent) {
this.message = message;
this.parent = parent;
}
@NotNull
public Message getMessage() {
return message;
}

View File

@@ -5,17 +5,11 @@ import org.briarproject.api.contact.ContactId;
import org.briarproject.api.sharing.InvitationRequest;
public class BlogInvitationReceivedEvent extends
InvitationRequestReceivedEvent {
private final Blog blog;
InvitationRequestReceivedEvent<Blog> {
public BlogInvitationReceivedEvent(Blog blog, ContactId contactId,
InvitationRequest request) {
super(contactId, request);
this.blog = blog;
super(blog, contactId, request);
}
public Blog getBlog() {
return blog;
}
}

View File

@@ -5,18 +5,11 @@ import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumInvitationRequest;
public class ForumInvitationReceivedEvent extends
InvitationRequestReceivedEvent {
private final Forum forum;
InvitationRequestReceivedEvent<Forum> {
public ForumInvitationReceivedEvent(Forum forum, ContactId contactId,
ForumInvitationRequest request) {
super(contactId, request);
this.forum = forum;
}
public Forum getForum() {
return forum;
super(forum, contactId, request);
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.api.event;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.forum.ForumInvitationRequest;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
public class GroupInvitationReceivedEvent extends
InvitationRequestReceivedEvent<PrivateGroup> {
public GroupInvitationReceivedEvent(PrivateGroup group, ContactId contactId,
GroupInvitationRequest request) {
super(group, contactId, request);
}
}

View File

@@ -2,14 +2,18 @@ package org.briarproject.api.event;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.sharing.InvitationRequest;
import org.briarproject.api.sharing.Shareable;
public abstract class InvitationRequestReceivedEvent extends Event {
public abstract class InvitationRequestReceivedEvent<S extends Shareable>
extends Event {
private final S shareable;
private final ContactId contactId;
private final InvitationRequest request;
InvitationRequestReceivedEvent(ContactId contactId,
InvitationRequestReceivedEvent(S shareable, ContactId contactId,
InvitationRequest request) {
this.shareable = shareable;
this.contactId = contactId;
this.request = request;
}
@@ -21,4 +25,8 @@ public abstract class InvitationRequestReceivedEvent extends Event {
public InvitationRequest getRequest() {
return request;
}
public S getShareable() {
return shareable;
}
}

View File

@@ -5,16 +5,15 @@ import org.briarproject.api.contact.ContactId;
import org.briarproject.api.sharing.InvitationRequest;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
public class ForumInvitationRequest extends InvitationRequest {
private final String forumName;
public ForumInvitationRequest(MessageId id, SessionId sessionId,
GroupId groupId, ContactId contactId, String forumName, String message,
boolean available, long time, boolean local, boolean sent,
boolean seen, boolean read) {
GroupId groupId, ContactId contactId, String forumName,
String message, boolean available, long time, boolean local,
boolean sent, boolean seen, boolean read) {
super(id, sessionId, groupId, contactId, message, available, time,
local, sent, seen, read);

View File

@@ -2,17 +2,22 @@ package org.briarproject.api.privategroup;
import org.briarproject.api.clients.BaseMessage;
import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class GroupMessage extends BaseMessage {
private final Author author;
public GroupMessage(@NotNull Message message, @Nullable MessageId parent,
@NotNull Author author) {
public GroupMessage(Message message, @Nullable MessageId parent,
Author author) {
super(message, parent);
this.author = author;
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.api.privategroup;
import org.briarproject.api.clients.NamedGroup;
import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sharing.Shareable;
import org.briarproject.api.sync.Group;
import org.jetbrains.annotations.NotNull;
@@ -10,7 +11,7 @@ import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class PrivateGroup extends NamedGroup {
public class PrivateGroup extends NamedGroup implements Shareable {
private final Author author;

View File

@@ -0,0 +1,8 @@
package org.briarproject.api.privategroup.invitation;
public interface GroupInvitationConstants {
// Group Metadata Keys
String CONTACT_ID = "contactId";
}

View File

@@ -0,0 +1,27 @@
package org.briarproject.api.privategroup.invitation;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.sharing.InvitationItem;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class GroupInvitationItem extends InvitationItem<PrivateGroup> {
private final Contact creator;
public GroupInvitationItem(PrivateGroup shareable, boolean subscribed,
Contact creator) {
super(shareable, subscribed);
this.creator = creator;
}
public Contact getCreator() {
return creator;
}
}

View File

@@ -0,0 +1,48 @@
package org.briarproject.api.privategroup.invitation;
import org.briarproject.api.clients.MessageTracker;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DbException;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
public interface GroupInvitationManager extends MessageTracker {
/** Returns the unique ID of the private group invitation client. */
ClientId getClientId();
/**
* Sends an invitation to share the given forum with the given contact
* and sends an optional message along with it.
*/
void sendInvitation(GroupId groupId, ContactId contactId,
String message) throws DbException;
/**
* Responds to a pending private group invitation
*/
void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
throws DbException;
/**
* Responds to a pending private group invitation
*/
void respondToInvitation(SessionId id, boolean accept) throws DbException;
/**
* Returns all private group invitation messages related to the contact
* identified by contactId.
*/
Collection<InvitationMessage> getInvitationMessages(
ContactId contactId) throws DbException;
/** Returns all private groups to which the user has been invited. */
Collection<GroupInvitationItem> getInvitations() throws DbException;
}

View File

@@ -0,0 +1,39 @@
package org.briarproject.api.privategroup.invitation;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sharing.InvitationRequest;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
@Immutable
@NotNullByDefault
public class GroupInvitationRequest extends InvitationRequest {
private final String groupName;
private final Author creator;
public GroupInvitationRequest(MessageId id, SessionId sessionId,
GroupId groupId, Author creator, ContactId contactId,
String groupName, String message, boolean available, long time,
boolean local, boolean sent, boolean seen, boolean read) {
super(id, sessionId, groupId, contactId, message, available, time,
local, sent, seen, read);
this.groupName = groupName;
this.creator = creator;
}
public String getGroupName() {
return groupName;
}
public Author getCreator() {
return creator;
}
}

View File

@@ -0,0 +1,40 @@
package org.briarproject.api.privategroup.invitation;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.Author;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sharing.InvitationResponse;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.NotNull;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
@Immutable
@NotNullByDefault
public class GroupInvitationResponse extends InvitationResponse {
private final String groupName;
private final Author creator;
public GroupInvitationResponse(MessageId id, SessionId sessionId,
GroupId groupId, String groupName, Author creator,
ContactId contactId, boolean accept, long time, boolean local,
boolean sent, boolean seen, boolean read) {
super(id, sessionId, groupId, contactId, accept, time, local, sent,
seen, read);
this.groupName = groupName;
this.creator = creator;
}
public String getGroupName() {
return groupName;
}
public Author getCreator() {
return creator;
}
}

View File

@@ -1,32 +1,36 @@
package org.briarproject.api.sharing;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.sync.GroupId;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
public class InvitationItem {
@Immutable
@NotNullByDefault
public abstract class InvitationItem<S extends Shareable> {
private final Shareable shareable;
private final S shareable;
private final boolean subscribed;
private final Collection<Contact> newSharers;
public InvitationItem(Shareable shareable, boolean subscribed,
Collection<Contact> newSharers) {
public InvitationItem(S shareable, boolean subscribed) {
this.shareable = shareable;
this.subscribed = subscribed;
this.newSharers = newSharers;
}
public Shareable getShareable() {
public S getShareable() {
return shareable;
}
public GroupId getId() {
return shareable.getId();
}
public String getName() {
return shareable.getName();
}
public boolean isSubscribed() {
return subscribed;
}
public Collection<Contact> getNewSharers() {
return newSharers;
}
}

View File

@@ -8,4 +8,7 @@ public interface Shareable {
GroupId getId();
Group getGroup();
String getName();
}

View File

@@ -0,0 +1,27 @@
package org.briarproject.api.sharing;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class SharingInvitationItem extends InvitationItem<Shareable> {
private final Collection<Contact> newSharers;
public SharingInvitationItem(Shareable shareable, boolean subscribed,
Collection<Contact> newSharers) {
super(shareable, subscribed);
this.newSharers = newSharers;
}
public Collection<Contact> getNewSharers() {
return newSharers;
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.api.sharing;
import org.briarproject.api.clients.MessageTracker;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DbException;
@@ -30,7 +31,14 @@ public interface SharingManager<S extends Shareable> extends MessageTracker {
throws DbException;
/**
* Returns all group sharing messages sent by the given contact.
* Responds to a pending group invitation
*/
void respondToInvitation(SessionId id, boolean accept)
throws DbException;
/**
* Returns all group sharing messages sent by the Contact
* identified by contactId.
*/
Collection<InvitationMessage> getInvitationMessages(
ContactId contactId) throws DbException;
@@ -38,7 +46,7 @@ public interface SharingManager<S extends Shareable> extends MessageTracker {
/**
* Returns all invitations to groups.
*/
Collection<InvitationItem> getInvitations() throws DbException;
Collection<SharingInvitationItem> getInvitations() throws DbException;
/**
* Returns all contacts who are sharing the given group with us.

View File

@@ -1,17 +1,19 @@
package org.briarproject.privategroup;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.ConversationManager;
import org.briarproject.api.privategroup.GroupMessageFactory;
import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import java.security.SecureRandom;
import org.briarproject.privategroup.invitation.GroupInvitationManagerImpl;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -25,6 +27,8 @@ public class PrivateGroupModule {
public static class EagerSingletons {
@Inject
GroupMessageValidator groupMessageValidator;
@Inject
GroupInvitationManager groupInvitationManager;
}
@Provides
@@ -65,4 +69,22 @@ public class PrivateGroupModule {
return validator;
}
@Provides
@Singleton
GroupInvitationManager provideGroupInvitationManager(
LifecycleManager lifecycleManager, ContactManager contactManager,
GroupInvitationManagerImpl groupInvitationManager,
ConversationManager conversationManager,
ValidationManager validationManager) {
validationManager.registerIncomingMessageHook(
groupInvitationManager.getClientId(), groupInvitationManager);
lifecycleManager.registerClient(groupInvitationManager);
contactManager.registerAddContactHook(groupInvitationManager);
contactManager.registerRemoveContactHook(groupInvitationManager);
conversationManager.registerConversationClient(groupInvitationManager);
return groupInvitationManager;
}
}

View File

@@ -0,0 +1,141 @@
package org.briarproject.privategroup.invitation;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.Client;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.ContactGroupFactory;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
import org.briarproject.api.messaging.ConversationManager;
import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.clients.ConversationClientImpl;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import javax.inject.Inject;
import static org.briarproject.api.privategroup.invitation.GroupInvitationConstants.CONTACT_ID;
public class GroupInvitationManagerImpl extends ConversationClientImpl
implements GroupInvitationManager, Client,
ContactManager.AddContactHook, ContactManager.RemoveContactHook,
ConversationManager.ConversationClient {
private static final ClientId CLIENT_ID =
new ClientId(StringUtils.fromHexString(
"B55231ABFC4A10666CD93D649B1D7F4F"
+ "016E65B87BB4C04F4E35613713DBCD13"));
private final ContactGroupFactory contactGroupFactory;
private final Group localGroup;
@Inject
protected GroupInvitationManagerImpl(DatabaseComponent db,
ClientHelper clientHelper, MetadataParser metadataParser,
ContactGroupFactory contactGroupFactory) {
super(db, clientHelper, metadataParser);
this.contactGroupFactory = contactGroupFactory;
localGroup = contactGroupFactory.createLocalGroup(getClientId());
}
@Override
public ClientId getClientId() {
return CLIENT_ID;
}
@Override
public void createLocalState(Transaction txn) throws DbException {
db.addGroup(txn, localGroup);
// Ensure we've set things up for any pre-existing contacts
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
}
@Override
public void addingContact(Transaction txn, Contact c) throws DbException {
try {
// Create a group to share with the contact
Group g = getContactGroup(c);
// Return if we've already set things up for this contact
if (db.containsGroup(txn, g.getId())) return;
// Store the group and share it with the contact
db.addGroup(txn, g);
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
// Attach the contact ID to the group
BdfDictionary meta = new BdfDictionary();
meta.put(CONTACT_ID, c.getId().getInt());
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public void removingContact(Transaction txn, Contact c) throws DbException {
// remove the contact group (all messages will be removed with it)
db.removeGroup(txn, getContactGroup(c));
}
@Override
protected Group getContactGroup(Contact c) {
return contactGroupFactory.createContactGroup(getClientId(), c);
}
@Override
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
BdfDictionary meta) throws DbException, FormatException {
return false;
}
@Override
public void sendInvitation(GroupId groupId, ContactId contactId,
String message) throws DbException {
}
@Override
public void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
throws DbException {
}
@Override
public void respondToInvitation(SessionId id, boolean accept)
throws DbException {
}
@Override
public Collection<InvitationMessage> getInvitationMessages(
ContactId contactId) throws DbException {
Collection<InvitationMessage> invitations =
new ArrayList<InvitationMessage>();
return invitations;
}
@Override
public Collection<GroupInvitationItem> getInvitations() throws DbException {
Collection<GroupInvitationItem> invitations =
new ArrayList<GroupInvitationItem>();
return invitations;
}
}

View File

@@ -25,7 +25,7 @@ import org.briarproject.api.event.Event;
import org.briarproject.api.event.InvitationRequestReceivedEvent;
import org.briarproject.api.event.InvitationResponseReceivedEvent;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sharing.SharingInvitationItem;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sharing.Shareable;
import org.briarproject.api.sharing.SharingManager;
@@ -316,27 +316,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
try {
// find session state based on shareable
IS localState = getSessionStateForResponse(txn, f, c);
// define action
InviteeSessionState.Action localAction;
if (accept) {
localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
} else {
localAction = InviteeSessionState.Action.LOCAL_DECLINE;
}
// start engine and process its state update
InviteeEngine<IS, IR> engine =
new InviteeEngine<IS, IR>(getIRFactory(), clock);
StateUpdate<IS, BaseMessage> update =
engine.onLocalAction(localState, localAction);
processInviteeStateUpdate(txn, null, update);
// track message
// TODO handle this properly without engine hacks (#376)
long time = update.toSend.get(0).getTime();
trackMessage(txn, localState.getGroupId(), time, true);
respondToInvitation(txn, localState, accept);
txn.setComplete();
} catch (FormatException e) {
throw new DbException(e);
@@ -345,6 +325,45 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
}
}
@Override
public void respondToInvitation(SessionId id, boolean accept)
throws DbException {
Transaction txn = db.startTransaction(false);
try {
IS localState = (IS) getSessionState(txn, id, true);
respondToInvitation(txn, localState, accept);
txn.setComplete();
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
private void respondToInvitation(Transaction txn, IS localState,
boolean accept) throws DbException, FormatException {
// define action
InviteeSessionState.Action localAction;
if (accept) {
localAction = InviteeSessionState.Action.LOCAL_ACCEPT;
} else {
localAction = InviteeSessionState.Action.LOCAL_DECLINE;
}
// start engine and process its state update
InviteeEngine<IS, IR> engine =
new InviteeEngine<IS, IR>(getIRFactory(), clock);
StateUpdate<IS, BaseMessage> update =
engine.onLocalAction(localState, localAction);
processInviteeStateUpdate(txn, null, update);
// track message
// TODO handle this properly without engine hacks (#376)
long time = update.toSend.get(0).getTime();
trackMessage(txn, localState.getGroupId(), time, true);
}
@Override
public Collection<InvitationMessage> getInvitationMessages(ContactId contactId)
throws DbException {
@@ -418,8 +437,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
}
@Override
public Collection<InvitationItem> getInvitations() throws DbException {
List<InvitationItem> invitations = new ArrayList<InvitationItem>();
public Collection<SharingInvitationItem> getInvitations() throws DbException {
List<SharingInvitationItem> invitations = new ArrayList<SharingInvitationItem>();
Transaction txn = db.startTransaction(true);
try {
Set<S> shareables = new HashSet<S>();
@@ -445,8 +464,8 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
for (S s : shareables) {
Collection<Contact> newS = newSharers.get(s.getId());
boolean subscribed = db.containsGroup(txn, s.getId());
InvitationItem invitation =
new InvitationItem(s, subscribed, newS);
SharingInvitationItem invitation =
new SharingInvitationItem(s, subscribed, newS);
invitations.add(invitation);
}
txn.setComplete();