From 3a9d66a85f1dbbdc99bdcc8564e2df387b3e62d3 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Tue, 26 Apr 2016 20:27:03 -0300 Subject: [PATCH] Forum Sharing Client UI This changes `ShareForumActivity` to use two fragments to facilitate forum sharing with the new Forum Sharing Client backend. The `ContactSelectorFragment` allows the user to select a number of contacts. If there is an ongoing sharing session or the forum is already shared with the contact, it is disabled in the list. If there is at least one contact selected, a button appears in the toolbar that brings the user to the `ShareForumMessageFragment` where the user can write an optional message to be send along with the invitation. After sending an invitation, the user is brought back to the forum that she shared and there is a snackbar showing up briefly to indicate the successful invitation. The invitation is shown along with the message within the private conversation of each contact. The person who shares the forum also sees the invitation and the message as outgoing messages that also display the current status of the messages. A notification is shown like for other private messages as well. Please note that this commit does not include a way for users to respond to invitations. --- .../res/layout/activity_share_forum.xml | 6 + .../layout/list_item_forum_invitation_in.xml | 57 ++++ .../layout/list_item_forum_invitation_out.xml | 56 ++++ .../res/layout/share_forum_message.xml | 43 +++ briar-android/res/values/strings.xml | 5 + .../android/AndroidComponent.java | 6 + .../AndroidNotificationManagerImpl.java | 10 +- .../android/contact/ContactListFragment.java | 17 +- .../android/contact/ConversationActivity.java | 25 +- .../android/contact/ConversationAdapter.java | 237 ++++++++++++----- .../ConversationForumInvitationInItem.java | 33 +++ .../ConversationForumInvitationItem.java | 19 ++ .../ConversationForumInvitationOutItem.java | 49 ++++ .../ConversationIntroductionInItem.java | 1 + .../ConversationIntroductionOutItem.java | 2 + .../android/contact/ConversationItem.java | 11 + .../contact/ConversationNoticeInItem.java | 1 + .../contact/ConversationNoticeOutItem.java | 1 + .../android/forum/ContactSelectorAdapter.java | 28 +- .../forum/ContactSelectorFragment.java | 247 ++++++++++++++++++ .../android/forum/ForumActivity.java | 13 +- .../forum/SelectableContactListItem.java | 9 +- .../android/forum/ShareForumActivity.java | 163 +++++------- .../forum/ShareForumMessageFragment.java | 177 +++++++++++++ 24 files changed, 1037 insertions(+), 179 deletions(-) create mode 100644 briar-android/res/layout/activity_share_forum.xml create mode 100644 briar-android/res/layout/list_item_forum_invitation_in.xml create mode 100644 briar-android/res/layout/list_item_forum_invitation_out.xml create mode 100644 briar-android/res/layout/share_forum_message.xml create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationForumInvitationInItem.java create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationForumInvitationItem.java create mode 100644 briar-android/src/org/briarproject/android/contact/ConversationForumInvitationOutItem.java create mode 100644 briar-android/src/org/briarproject/android/forum/ContactSelectorFragment.java create mode 100644 briar-android/src/org/briarproject/android/forum/ShareForumMessageFragment.java diff --git a/briar-android/res/layout/activity_share_forum.xml b/briar-android/res/layout/activity_share_forum.xml new file mode 100644 index 0000000000..b91e96f00b --- /dev/null +++ b/briar-android/res/layout/activity_share_forum.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + android:id="@+id/shareForumContainer" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"/> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_forum_invitation_in.xml b/briar-android/res/layout/list_item_forum_invitation_in.xml new file mode 100644 index 0000000000..c6881437b3 --- /dev/null +++ b/briar-android/res/layout/list_item_forum_invitation_in.xml @@ -0,0 +1,57 @@ +<?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"> + + <include + android:id="@+id/messageLayout" + layout="@layout/list_item_msg_in"/> + + <RelativeLayout + android:id="@+id/introductionLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="left|start" + android:background="@drawable/notice_in" + android:layout_marginLeft="@dimen/message_bubble_margin_tail" + android:layout_marginRight="@dimen/message_bubble_margin_non_tail"> + + <TextView + android:id="@+id/introductionText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="80dp" + android:textIsSelectable="true" + android:textSize="@dimen/text_size_medium" + android:textStyle="italic" + tools:text="@string/forum_invitation_received"/> + + <TextView + android:id="@+id/introductionTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/message_bubble_timestamp_margin" + android:layout_alignEnd="@+id/introductionText" + android:layout_alignRight="@+id/introductionText" + android:layout_below="@+id/showForumsButton" + android:textColor="@color/private_message_date" + android:textSize="@dimen/text_size_tiny" + tools:text="Dec 24, 13:37"/> + + <Button + android:id="@+id/showForumsButton" + style="@style/BriarButtonFlat.Positive" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="-15dp" + android:layout_alignEnd="@+id/introductionText" + android:layout_alignRight="@+id/introductionText" + android:layout_below="@+id/introductionText" + android:text="@string/forum_show_available"/> + + </RelativeLayout> + +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_forum_invitation_out.xml b/briar-android/res/layout/list_item_forum_invitation_out.xml new file mode 100644 index 0000000000..88070ea669 --- /dev/null +++ b/briar-android/res/layout/list_item_forum_invitation_out.xml @@ -0,0 +1,56 @@ +<?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"> + + <include + android:id="@+id/messageLayout" + layout="@layout/list_item_msg_out"/> + + <RelativeLayout + android:id="@+id/introductionLayout" + 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"> + + <TextView + android:id="@+id/introductionText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textIsSelectable="true" + android:textSize="@dimen/text_size_medium" + android:textStyle="italic" + tools:text="@string/introduction_request_received"/> + + <TextView + android:id="@+id/introductionTime" + 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/introductionText" + android:textColor="@color/private_message_date" + android:textSize="@dimen/text_size_tiny" + tools:text="Dec 24, 13:37"/> + + <ImageView + android:id="@+id/introductionStatus" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@+id/introductionTime" + android:layout_toRightOf="@+id/introductionTime" + android:layout_alignBottom="@+id/introductionTime" + android:layout_marginLeft="@dimen/margin_medium" + tools:ignore="ContentDescription" + tools:src="@drawable/message_delivered"/> + + </RelativeLayout> + +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/share_forum_message.xml b/briar-android/res/layout/share_forum_message.xml new file mode 100644 index 0000000000..64a0279148 --- /dev/null +++ b/briar-android/res/layout/share_forum_message.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.v4.widget.NestedScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="@dimen/margin_activity_horizontal" + android:orientation="vertical"> + + <TextView + android:id="@+id/introductionText" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="@dimen/margin_medium" + android:layout_weight="1" + android:gravity="top" + android:textSize="@dimen/text_size_medium" + android:text="@string/forum_share_message"/> + + <EditText + android:id="@+id/invitationMessageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_medium" + android:gravity="bottom" + android:hint="@string/introduction_message_hint" + android:inputType="text|textMultiLine|textCapSentences"/> + + <Button + android:id="@+id/shareForumButton" + style="@style/BriarButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/forum_share_button" + /> + + </LinearLayout> + +</android.support.v4.widget.NestedScrollView> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index e5d21dec3e..8cbd86d956 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -92,6 +92,11 @@ <string name="forum_created_toast">Forum created</string> <string name="forum_share_action">Share this forum with chosen contacts</string> <string name="forum_share_button">Share Forum</string> + <string name="forum_shared_snackbar">Forum shared with chosen contacts</string> + <string name="forum_share_message">You may compose an optional invitation message that will be sent to the selected contacts.</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_available">Show Available Forums</string> <string name="forum_compose_post">New Forum Post</string> <string name="from">From:</string> <string name="anonymous">Anonymous</string> diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index 7c5777e525..8b2ac951ab 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -5,11 +5,13 @@ import org.briarproject.CoreModule; import org.briarproject.android.contact.ContactListFragment; import org.briarproject.android.contact.ConversationActivity; import org.briarproject.android.forum.AvailableForumsActivity; +import org.briarproject.android.forum.ContactSelectorFragment; import org.briarproject.android.forum.CreateForumActivity; import org.briarproject.android.forum.ForumActivity; import org.briarproject.android.forum.ForumListFragment; import org.briarproject.android.forum.ReadForumPostActivity; import org.briarproject.android.forum.ShareForumActivity; +import org.briarproject.android.forum.ShareForumMessageFragment; import org.briarproject.android.forum.WriteForumPostActivity; import org.briarproject.android.identity.CreateIdentityActivity; import org.briarproject.android.introduction.ContactChooserFragment; @@ -70,6 +72,10 @@ public interface AndroidComponent extends CoreEagerSingletons { void inject(ShareForumActivity activity); + void inject(ContactSelectorFragment fragment); + + void inject(ShareForumMessageFragment fragment); + void inject(ReadForumPostActivity activity); void inject(ForumActivity activity); diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index 0a7b1ea2bb..623391cc3b 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -20,6 +20,7 @@ import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.IntroductionSucceededEvent; @@ -164,13 +165,16 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } } else if (e instanceof IntroductionRequestReceivedEvent) { ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId(); - showIntroductionNotifications(c); + showNotificationForPrivateConversation(c); } else if (e instanceof IntroductionResponseReceivedEvent) { ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId(); - showIntroductionNotifications(c); + showNotificationForPrivateConversation(c); } else if (e instanceof IntroductionSucceededEvent) { Contact c = ((IntroductionSucceededEvent) e).getContact(); showIntroductionSucceededNotification(c); + } else if (e instanceof ForumInvitationReceivedEvent) { + ContactId c = ((ForumInvitationReceivedEvent) e).getContactId(); + showNotificationForPrivateConversation(c); } } @@ -359,7 +363,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, }); } - private void showIntroductionNotifications(final ContactId c) { + private void showNotificationForPrivateConversation(final ContactId c) { androidExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index 9838eb06a8..82137d92ad 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -29,6 +29,8 @@ import org.briarproject.api.event.ContactStatusChangedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.forum.ForumInvitationMessage; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.introduction.IntroductionManager; @@ -86,6 +88,8 @@ public class ContactListFragment extends BaseEventFragment { @Inject protected volatile IntroductionManager introductionManager; @Inject + protected volatile ForumSharingManager forumSharingManager; + @Inject protected volatile EventBus eventBus; @Override @@ -226,7 +230,8 @@ public class ContactListFragment extends BaseEventFragment { MessageValidatedEvent m = (MessageValidatedEvent) e; ClientId c = m.getClientId(); if (m.isValid() && (c.equals(messagingManager.getClientId()) || - c.equals(introductionManager.getClientId()))) { + c.equals(introductionManager.getClientId()) || + c.equals(forumSharingManager.getClientId()))) { LOG.info("Message added, reloading"); reloadConversation(m.getMessage().getGroupId()); } @@ -317,6 +322,16 @@ public class ContactListFragment extends BaseEventFragment { if (LOG.isLoggable(INFO)) LOG.info("Loading introduction messages took " + duration + " ms"); + now = System.currentTimeMillis(); + Collection<ForumInvitationMessage> invitations = + forumSharingManager.getForumInvitationMessages(id); + for (ForumInvitationMessage i : invitations) { + messages.add(ConversationItem.from(i)); + } + duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading forum invitations took " + duration + " ms"); + return messages; } } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 213e412b52..5a62bef1f3 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -42,11 +42,14 @@ 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.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; +import org.briarproject.api.forum.ForumInvitationMessage; +import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.introduction.IntroductionManager; import org.briarproject.api.introduction.IntroductionMessage; import org.briarproject.api.introduction.IntroductionRequest; @@ -109,6 +112,7 @@ public class ConversationActivity extends BriarActivity @Inject protected volatile EventBus eventBus; @Inject protected volatile PrivateMessageFactory privateMessageFactory; @Inject protected volatile IntroductionManager introductionManager; + @Inject protected volatile ForumSharingManager forumSharingManager; private volatile GroupId groupId = null; private volatile ContactId contactId = null; private volatile String contactName = null; @@ -278,10 +282,13 @@ public class ConversationActivity extends BriarActivity Collection<IntroductionMessage> introductions = introductionManager .getIntroductionMessages(contactId); + Collection<ForumInvitationMessage> invitations = + forumSharingManager + .getForumInvitationMessages(contactId); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Loading headers took " + duration + " ms"); - displayMessages(headers, introductions); + displayMessages(headers, introductions, invitations); } catch (NoSuchContactException e) { finishOnUiThread(); } catch (DbException e) { @@ -293,11 +300,13 @@ public class ConversationActivity extends BriarActivity } private void displayMessages(final Collection<PrivateMessageHeader> headers, - final Collection<IntroductionMessage> introductions) { + final Collection<IntroductionMessage> introductions, + final Collection<ForumInvitationMessage> invitations) { runOnUiThread(new Runnable() { public void run() { sendButton.setEnabled(true); - if (headers.isEmpty() && introductions.isEmpty()) { + if (headers.isEmpty() && introductions.isEmpty() && + invitations.isEmpty()) { // we have no messages, // so let the list know to hide progress bar list.showData(); @@ -326,6 +335,10 @@ public class ConversationActivity extends BriarActivity } items.add(item); } + for (ForumInvitationMessage i : invitations) { + ConversationItem item = ConversationItem.from(i); + items.add(item); + } adapter.addAll(items); // Scroll to the bottom list.scrollToPosition(adapter.getItemCount() - 1); @@ -476,6 +489,12 @@ public class ConversationActivity extends BriarActivity ConversationItem.from(this, contactName, ir); addIntroduction(item); } + } else if (e instanceof ForumInvitationReceivedEvent) { + ForumInvitationReceivedEvent event = + (ForumInvitationReceivedEvent) e; + if (event.getContactId().equals(contactId)) { + loadMessages(); + } } } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java index 91a006b9f5..cf1a4b738e 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java @@ -1,6 +1,7 @@ package org.briarproject.android.contact; import android.content.Context; +import android.content.Intent; import android.support.v7.util.SortedList; import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; @@ -13,8 +14,10 @@ import android.widget.ImageView; import android.widget.TextView; import org.briarproject.R; -import org.briarproject.api.introduction.IntroductionRequest; +import org.briarproject.android.forum.AvailableForumsActivity; import org.briarproject.api.clients.SessionId; +import org.briarproject.api.forum.ForumInvitationMessage; +import org.briarproject.api.introduction.IntroductionRequest; import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.util.StringUtils; @@ -22,6 +25,8 @@ import java.util.List; import static android.support.v7.util.SortedList.INVALID_POSITION; import static android.support.v7.widget.RecyclerView.ViewHolder; +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; @@ -35,50 +40,7 @@ import static org.briarproject.android.contact.ConversationItem.OutgoingItem; class ConversationAdapter extends RecyclerView.Adapter { private final SortedList<ConversationItem> items = - new SortedList<ConversationItem>(ConversationItem.class, - new SortedList.Callback<ConversationItem>() { - @Override - public void onInserted(int position, int count) { - notifyItemRangeInserted(position, count); - } - - @Override - public void onChanged(int position, int count) { - notifyItemRangeChanged(position, count); - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - notifyItemMoved(fromPosition, toPosition); - } - - @Override - public void onRemoved(int position, int count) { - notifyItemRangeRemoved(position, count); - } - - @Override - public int compare(ConversationItem c1, - ConversationItem c2) { - long time1 = c1.getTime(); - long time2 = c2.getTime(); - if (time1 < time2) return -1; - if (time1 > time2) return 1; - return 0; - } - - @Override - public boolean areItemsTheSame(ConversationItem c1, - ConversationItem c2) { - return c1.getId().equals(c2.getId()); - } - - @Override - public boolean areContentsTheSame(ConversationItem c1, - ConversationItem c2) { - return c1.equals(c2); - } - }); + new SortedList<>(ConversationItem.class, new ListCallbacks()); private Context ctx; private IntroductionHandler intro; private String contactName; @@ -129,6 +91,16 @@ class ConversationAdapter extends RecyclerView.Adapter { .inflate(R.layout.list_item_notice_out, viewGroup, false); return new NoticeHolder(v, type); } + else if (type == FORUM_INVITATION_IN) { + v = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.list_item_forum_invitation_in, viewGroup, false); + return new InvitationHolder(v, type); + } + else if (type == FORUM_INVITATION_OUT) { + v = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.list_item_forum_invitation_out, viewGroup, false); + return new InvitationHolder(v, type); + } // incoming message (non-local) else { v = LayoutInflater.from(viewGroup.getContext()) @@ -152,6 +124,12 @@ class ConversationAdapter extends RecyclerView.Adapter { bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item); } else if (item instanceof ConversationNoticeInItem) { bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item); + } else if (item instanceof ConversationForumInvitationOutItem) { + bindInvitation((InvitationHolder) ui, + (ConversationForumInvitationOutItem) item, position); + } else if (item instanceof ConversationForumInvitationInItem) { + bindInvitation((InvitationHolder) ui, + (ConversationForumInvitationInItem) item, position); } else { throw new IllegalArgumentException("Unhandled Conversation Item"); } @@ -204,7 +182,7 @@ class ConversationAdapter extends RecyclerView.Adapter { final IntroductionRequest ir = item.getIntroductionRequest(); - final String message = ir.getMessage(); + String message = ir.getMessage(); if (StringUtils.isNullOrEmpty(message)) { ui.messageLayout.setVisibility(View.GONE); } else { @@ -300,6 +278,63 @@ class ConversationAdapter extends RecyclerView.Adapter { } } + private void bindInvitation(InvitationHolder ui, + final ConversationForumInvitationItem item, final int position) { + + ForumInvitationMessage fim = item.getForumInvitationMessage(); + + String message = fim.getMessage(); + if (StringUtils.isNullOrEmpty(message)) { + ui.messageLayout.setVisibility(View.GONE); + } else { + ui.messageLayout.setVisibility(View.VISIBLE); + ui.message.body.setText(message); + ui.message.date.setText( + DateUtils.getRelativeTimeSpanString(ctx, item.getTime())); + } + + // Outgoing Invitation + if (item instanceof ConversationForumInvitationOutItem) { + ui.text.setText(ctx.getString(R.string.forum_invitation_sent, + fim.getForumName(), contactName)); + ConversationForumInvitationOutItem i = + (ConversationForumInvitationOutItem) item; + if (i.isSeen()) { + ui.status.setImageResource(R.drawable.message_delivered); + ui.message.status.setImageResource(R.drawable.message_delivered_white); + } else if (i.isSent()) { + ui.status.setImageResource(R.drawable.message_sent); + ui.message.status.setImageResource(R.drawable.message_sent_white); + } else { + ui.status.setImageResource(R.drawable.message_stored); + ui.message.status.setImageResource(R.drawable.message_stored_white); + } + } + // Incoming Invitation + else { + ui.text.setText(ctx.getString(R.string.forum_invitation_received, + contactName, fim.getForumName())); + + if (fim.isAvailable()) { + ui.showForumsButton.setVisibility(View.VISIBLE); + ui.showForumsButton + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = + new Intent(ctx, + AvailableForumsActivity.class); + ctx.startActivity(intent); + } + }); + } else { + ui.showForumsButton.setVisibility(View.GONE); + } + } + ui.date.setText( + DateUtils.getRelativeTimeSpanString(ctx, item.getTime())); + } + @Override public int getItemCount() { return items.size(); @@ -321,8 +356,7 @@ class ConversationAdapter extends RecyclerView.Adapter { } public SparseArray<IncomingItem> getIncomingMessages() { - SparseArray<IncomingItem> messages = - new SparseArray<IncomingItem>(); + SparseArray<IncomingItem> messages = new SparseArray<>(); for (int i = 0; i < items.size(); i++) { ConversationItem item = items.get(i); @@ -334,8 +368,7 @@ class ConversationAdapter extends RecyclerView.Adapter { } public SparseArray<OutgoingItem> getOutgoingMessages() { - SparseArray<OutgoingItem> messages = - new SparseArray<OutgoingItem>(); + SparseArray<OutgoingItem> messages = new SparseArray<>(); for (int i = 0; i < items.size(); i++) { ConversationItem item = items.get(i); @@ -347,8 +380,7 @@ class ConversationAdapter extends RecyclerView.Adapter { } public SparseArray<ConversationMessageItem> getPrivateMessages() { - SparseArray<ConversationMessageItem> messages = - new SparseArray<ConversationMessageItem>(); + SparseArray<ConversationMessageItem> messages = new SparseArray<>(); for (int i = 0; i < items.size(); i++) { ConversationItem item = items.get(i); @@ -394,14 +426,14 @@ class ConversationAdapter extends RecyclerView.Adapter { private static class IntroductionHolder extends RecyclerView.ViewHolder { - public ViewGroup layout; - public View messageLayout; - public MessageHolder message; - public TextView text; - public Button acceptButton; - public Button declineButton; - public TextView date; - public ImageView status; + final private ViewGroup layout; + final private View messageLayout; + final private MessageHolder message; + final private TextView text; + final private Button acceptButton; + final private Button declineButton; + final private TextView date; + final private ImageView status; public IntroductionHolder(View v, int type) { super(v); @@ -417,16 +449,18 @@ class ConversationAdapter extends RecyclerView.Adapter { if (type == INTRODUCTION_OUT) { status = (ImageView) v.findViewById(R.id.introductionStatus); + } else { + status = null; } } } private static class NoticeHolder extends RecyclerView.ViewHolder { - public ViewGroup layout; - public TextView text; - public TextView date; - public ImageView status; + final private ViewGroup layout; + final private TextView text; + final private TextView date; + final private ImageView status; public NoticeHolder(View v, int type) { super(v); @@ -437,10 +471,85 @@ class ConversationAdapter extends RecyclerView.Adapter { if (type == NOTICE_OUT) { status = (ImageView) v.findViewById(R.id.noticeStatus); + } else { + status = null; } } } + private static class InvitationHolder extends RecyclerView.ViewHolder { + + final private ViewGroup layout; + final private View messageLayout; + final private MessageHolder message; + final private TextView text; + final private Button showForumsButton; + final private TextView date; + final private ImageView status; + + public InvitationHolder(View v, int type) { + super(v); + + layout = (ViewGroup) v.findViewById(R.id.introductionLayout); + messageLayout = v.findViewById(R.id.messageLayout); + message = new MessageHolder(messageLayout, + type == FORUM_INVITATION_IN ? MSG_IN : MSG_OUT); + text = (TextView) v.findViewById(R.id.introductionText); + showForumsButton = (Button) v.findViewById(R.id.showForumsButton); + date = (TextView) v.findViewById(R.id.introductionTime); + + if (type == FORUM_INVITATION_OUT) { + status = (ImageView) v.findViewById(R.id.introductionStatus); + } else { + status = null; + } + } + } + + private class ListCallbacks extends SortedList.Callback<ConversationItem> { + @Override + public void onInserted(int position, int count) { + notifyItemRangeInserted(position, count); + } + + @Override + public void onChanged(int position, int count) { + notifyItemRangeChanged(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public void onRemoved(int position, int count) { + notifyItemRangeRemoved(position, count); + } + + @Override + public int compare(ConversationItem c1, + ConversationItem c2) { + long time1 = c1.getTime(); + long time2 = c2.getTime(); + if (time1 < time2) return -1; + if (time1 > time2) return 1; + return 0; + } + + @Override + public boolean areItemsTheSame(ConversationItem c1, + ConversationItem c2) { + return c1.getId().equals(c2.getId()); + } + + @Override + public boolean areContentsTheSame(ConversationItem c1, + ConversationItem c2) { + return c1.equals(c2); + } + } + public interface IntroductionHandler { void respondToIntroduction(final SessionId sessionId, final boolean accept); diff --git a/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationInItem.java new file mode 100644 index 0000000000..06bf4a1adb --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationInItem.java @@ -0,0 +1,33 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.forum.ForumInvitationMessage; + +// This class is not thread-safe +public class ConversationForumInvitationInItem + extends ConversationForumInvitationItem + implements ConversationItem.IncomingItem { + + private boolean read; + + public ConversationForumInvitationInItem(ForumInvitationMessage fim) { + super(fim); + + this.read = fim.isRead(); + } + + @Override + int getType() { + return FORUM_INVITATION_IN; + } + + @Override + public boolean isRead() { + return read; + } + + @Override + public void setRead(boolean read) { + this.read = read; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationItem.java new file mode 100644 index 0000000000..0bf7d72abe --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationItem.java @@ -0,0 +1,19 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.forum.ForumInvitationMessage; + +abstract class ConversationForumInvitationItem extends ConversationItem { + + private ForumInvitationMessage fim; + + public ConversationForumInvitationItem(ForumInvitationMessage fim) { + super(fim.getId(), fim.getTimestamp()); + + this.fim = fim; + } + + public ForumInvitationMessage getForumInvitationMessage() { + return fim; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationOutItem.java new file mode 100644 index 0000000000..2d4699abf3 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationForumInvitationOutItem.java @@ -0,0 +1,49 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.forum.ForumInvitationMessage; + +/** + * 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. + * + * This class is not thread-safe + */ +public class ConversationForumInvitationOutItem + extends ConversationForumInvitationItem + implements ConversationItem.OutgoingItem { + + private boolean sent, seen; + + public ConversationForumInvitationOutItem(ForumInvitationMessage fim) { + super(fim); + this.sent = fim.isSent(); + this.seen = fim.isSeen(); + } + + @Override + int getType() { + return FORUM_INVITATION_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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java index fb891d0a1b..9ccf21efdc 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java @@ -3,6 +3,7 @@ package org.briarproject.android.contact; import org.briarproject.api.introduction.IntroductionRequest; import org.briarproject.api.sync.MessageId; +// This class is not thread-safe public class ConversationIntroductionInItem extends ConversationIntroductionItem implements ConversationItem.IncomingItem { diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java index a2aba398f9..7584c87380 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java @@ -6,6 +6,8 @@ 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 */ public class ConversationIntroductionOutItem extends ConversationIntroductionItem diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 2fc96b6ab7..93e95e24f3 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -3,6 +3,7 @@ package org.briarproject.android.contact; import android.content.Context; import org.briarproject.R; +import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.introduction.IntroductionMessage; import org.briarproject.api.introduction.IntroductionRequest; import org.briarproject.api.introduction.IntroductionResponse; @@ -20,6 +21,8 @@ public abstract class ConversationItem { 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; private MessageId id; private long time; @@ -92,6 +95,14 @@ public abstract class ConversationItem { } } + public static ConversationItem from(ForumInvitationMessage fim) { + if (fim.isLocal()) { + return new ConversationForumInvitationOutItem(fim); + } else { + return new ConversationForumInvitationInItem(fim); + } + } + /** This method should not be used to get user-facing objects, * Its purpose is to provider data for the contact list. */ diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java index 610b703c17..3affb3d483 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java @@ -2,6 +2,7 @@ package org.briarproject.android.contact; import org.briarproject.api.sync.MessageId; +// This class is not thread-safe public class ConversationNoticeInItem extends ConversationNoticeItem implements ConversationItem.IncomingItem { diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java index b398897013..ca4503cf67 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java @@ -2,6 +2,7 @@ package org.briarproject.android.contact; import org.briarproject.api.sync.MessageId; +// This class is not thread-safe public class ConversationNoticeOutItem extends ConversationNoticeItem implements ConversationItem.OutgoingItem { diff --git a/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java index 0e3e81181f..2f1ce156b6 100644 --- a/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java +++ b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java @@ -1,6 +1,11 @@ package org.briarproject.android.forum; import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -47,10 +52,16 @@ public class ContactSelectorAdapter } else { ui.checkBox.setChecked(false); } + + if (item.isDisabled()) { + // we share this forum already with that contact + ui.layout.setEnabled(false); + grayOutItem(ui); + } } public Collection<ContactId> getSelectedContactIds() { - Collection<ContactId> selected = new ArrayList<ContactId>(); + Collection<ContactId> selected = new ArrayList<>(); for (int i = 0; i < contacts.size(); i++) { SelectableContactListItem item = @@ -78,4 +89,19 @@ public class ContactSelectorAdapter return compareByName(c1, c2); } + private void grayOutItem(final SelectableContactHolder ui) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + float alpha = 0.25f; + ui.avatar.setAlpha(alpha); + ui.name.setAlpha(alpha); + ui.checkBox.setAlpha(alpha); + } else { + ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY, + PorterDuff.Mode.MULTIPLY); + ui.avatar.setColorFilter(colorFilter); + ui.name.setEnabled(false); + ui.checkBox.setEnabled(false); + } + } + } diff --git a/briar-android/src/org/briarproject/android/forum/ContactSelectorFragment.java b/briar-android/src/org/briarproject/android/forum/ContactSelectorFragment.java new file mode 100644 index 0000000000..73de55ec18 --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ContactSelectorFragment.java @@ -0,0 +1,247 @@ +package org.briarproject.android.forum; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.transition.Fade; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import org.briarproject.R; +import org.briarproject.android.AndroidComponent; +import org.briarproject.android.contact.BaseContactListAdapter; +import org.briarproject.android.contact.ContactListItem; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.db.DbException; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.GroupId; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.android.forum.ShareForumActivity.CONTACTS; +import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; + +public class ContactSelectorFragment extends BaseFragment implements + BaseContactListAdapter.OnItemClickListener { + + public final static String TAG = "ContactSelectorFragment"; + private ShareForumActivity shareForumActivity; + private Menu menu; + private BriarRecyclerView list; + private ContactSelectorAdapter adapter; + private Collection<ContactId> selectedContacts; + + private static final Logger LOG = + Logger.getLogger(ContactSelectorFragment.class.getName()); + + // Fields that are accessed from background threads must be volatile + protected volatile GroupId groupId; + @Inject + protected volatile ContactManager contactManager; + @Inject + protected volatile IdentityManager identityManager; + @Inject + protected volatile ForumSharingManager forumSharingManager; + + public static ContactSelectorFragment newInstance(GroupId groupId) { + Bundle args = new Bundle(); + args.putByteArray(GROUP_ID, groupId.getBytes()); + + ContactSelectorFragment fragment = new ContactSelectorFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + shareForumActivity = (ShareForumActivity) context; + } catch (ClassCastException e) { + throw new InstantiationError( + "This fragment is only meant to be attached to the ShareForumActivity"); + } + } + + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setHasOptionsMenu(true); + groupId = new GroupId(getArguments().getByteArray(GROUP_ID)); + if (groupId == null) throw new IllegalStateException("No GroupId"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View contentView = + inflater.inflate(R.layout.introduction_contact_chooser, + container, false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setExitTransition(new Fade()); + } + + adapter = new ContactSelectorAdapter(getActivity(), this); + + list = (BriarRecyclerView) contentView.findViewById(R.id.contactList); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + list.setAdapter(adapter); + list.setEmptyText(getString(R.string.no_contacts)); + + // restore selected contacts if available + if (savedInstanceState != null) { + ArrayList<Integer> intContacts = + savedInstanceState.getIntegerArrayList(CONTACTS); + selectedContacts = ShareForumActivity.getContactsFromIntegers(intContacts); + } + + return contentView; + } + + @Override + public void onResume() { + super.onResume(); + + if (selectedContacts != null) { + loadContacts(Collections.unmodifiableCollection(selectedContacts)); + } else { + loadContacts(null); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (adapter != null) { + selectedContacts = adapter.getSelectedContactIds(); + outState.putIntegerArrayList(CONTACTS, + getContactsFromIds(selectedContacts)); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.forum_share_actions, menu); + super.onCreateOptionsMenu(menu, inflater); + this.menu = menu; + // hide sharing action initially, if no contact is selected + updateMenuItem(); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + // Handle presses on the action bar items + switch (item.getItemId()) { + case android.R.id.home: + shareForumActivity.onBackPressed(); + return true; + case R.id.action_share_forum: + selectedContacts = adapter.getSelectedContactIds(); + shareForumActivity.showMessageScreen(groupId, selectedContacts); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void onItemClick(View view, ContactListItem item) { + ((SelectableContactListItem) item).toggleSelected(); + adapter.notifyItemChanged(adapter.findItemPosition(item), item); + + updateMenuItem(); + } + + private void loadContacts(final Collection<ContactId> selection) { + shareForumActivity.runOnDbThread(new Runnable() { + public void run() { + try { + long now = System.currentTimeMillis(); + List<ContactListItem> contacts = + new ArrayList<>(); + + for (Contact c : contactManager.getActiveContacts()) { + LocalAuthor localAuthor = identityManager + .getLocalAuthor(c.getLocalAuthorId()); + // was this contact already selected? + boolean selected = selection != null && + selection.contains(c.getId()); + // do we have already some sharing with that contact? + boolean disabled = + !forumSharingManager.canBeShared(groupId, c); + contacts.add( + new SelectableContactListItem(c, localAuthor, + groupId, selected, disabled)); + } + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + displayContacts(Collections.unmodifiableList(contacts)); + } catch (DbException e) { + displayContacts(Collections.<ContactListItem>emptyList()); + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void displayContacts(final List<ContactListItem> contacts) { + shareForumActivity.runOnUiThread(new Runnable() { + public void run() { + if (!contacts.isEmpty()) { + adapter.addAll(contacts); + } else { + list.showData(); + } + updateMenuItem(); + } + }); + } + + private void updateMenuItem() { + if (menu == null) return; + MenuItem item = menu.findItem(R.id.action_share_forum); + if (item == null) return; + + selectedContacts = adapter.getSelectedContactIds(); + if (selectedContacts.size() > 0) { + item.setVisible(true); + } else { + item.setVisible(false); + } + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index c092791a57..9a2ec711d9 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -3,6 +3,7 @@ package org.briarproject.android.forum; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.app.AlertDialog; @@ -49,6 +50,7 @@ import javax.inject.Inject; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; +import static android.support.design.widget.Snackbar.LENGTH_LONG; import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.View.GONE; @@ -68,6 +70,7 @@ public class ForumActivity extends BriarActivity implements EventListener, public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP"; private static final int REQUEST_READ = 2; + private static final int REQUEST_FORUM_SHARED = 3; private static final Logger LOG = Logger.getLogger(ForumActivity.class.getName()); @@ -165,7 +168,9 @@ public class ForumActivity extends BriarActivity implements EventListener, ActivityOptionsCompat options = ActivityOptionsCompat .makeCustomAnimation(this, android.R.anim.slide_in_left, android.R.anim.slide_out_right); - ActivityCompat.startActivity(this, i2, options.toBundle()); + ActivityCompat + .startActivityForResult(this, i2, REQUEST_FORUM_SHARED, + options.toBundle()); return true; case R.id.action_forum_delete: showUnsubscribeDialog(); @@ -297,6 +302,12 @@ public class ForumActivity extends BriarActivity implements EventListener, if (position >= 0 && position < adapter.getCount()) displayPost(position); } + else if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) { + Snackbar s = Snackbar.make(list, R.string.forum_shared_snackbar, + LENGTH_LONG); + s.getView().setBackgroundResource(R.color.briar_primary); + s.show(); + } } @Override diff --git a/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java index 93d266c6c0..6b9116ca41 100644 --- a/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java +++ b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java @@ -11,14 +11,15 @@ import java.util.Collections; // This class is not thread-safe public class SelectableContactListItem extends ContactListItem { - private boolean selected; + private boolean selected, disabled; public SelectableContactListItem(Contact contact, LocalAuthor localAuthor, - GroupId groupId, boolean selected) { + GroupId groupId, boolean selected, boolean disabled) { super(contact, localAuthor, false, groupId, Collections.<ConversationItem>emptyList()); this.selected = selected; + this.disabled = disabled; } public void setSelected(boolean selected) { @@ -33,4 +34,8 @@ public class SelectableContactListItem extends ContactListItem { selected = !selected; } + public boolean isDisabled() { + return disabled; + } + } diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java index ccb0b365d9..820e25666f 100644 --- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java @@ -2,147 +2,102 @@ package org.briarproject.android.forum; import android.content.Intent; import android.os.Bundle; -import android.support.v7.widget.LinearLayoutManager; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import org.briarproject.R; import org.briarproject.android.AndroidComponent; import org.briarproject.android.BriarActivity; -import org.briarproject.android.contact.BaseContactListAdapter; -import org.briarproject.android.contact.ContactListItem; -import org.briarproject.android.util.BriarRecyclerView; -import org.briarproject.api.contact.Contact; +import org.briarproject.android.fragment.BaseFragment; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.contact.ContactManager; -import org.briarproject.api.db.DbException; -import org.briarproject.api.forum.ForumSharingManager; -import org.briarproject.api.identity.IdentityManager; -import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.GroupId; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; public class ShareForumActivity extends BriarActivity implements - BaseContactListAdapter.OnItemClickListener { - - private static final Logger LOG = - Logger.getLogger(ShareForumActivity.class.getName()); + BaseFragment.BaseFragmentListener { - private ContactSelectorAdapter adapter; - - // Fields that are accessed from background threads must be volatile - @Inject protected volatile IdentityManager identityManager; - @Inject protected volatile ContactManager contactManager; - @Inject protected volatile ForumSharingManager forumSharingManager; - private volatile GroupId groupId; + public final static String CONTACTS = "contacts"; @Override - public void onCreate(Bundle state) { - super.onCreate(state); + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - setContentView(R.layout.introduction_contact_chooser); + setContentView(R.layout.activity_share_forum); Intent i = getIntent(); byte[] b = i.getByteArrayExtra(GROUP_ID); - if (b == null) throw new IllegalStateException(); - groupId = new GroupId(b); - - adapter = new ContactSelectorAdapter(this, this); - BriarRecyclerView list = - (BriarRecyclerView) findViewById(R.id.contactList); - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - list.setEmptyText(getString(R.string.no_contacts)); + if (b == null) throw new IllegalStateException("No GroupId"); + GroupId groupId = new GroupId(b); + + if (savedInstanceState == null) { + ContactSelectorFragment contactSelectorFragment = + ContactSelectorFragment.newInstance(groupId); + getSupportFragmentManager().beginTransaction() + .add(R.id.shareForumContainer, contactSelectorFragment) + .commit(); + } } @Override - public void onResume() { - super.onResume(); - - loadContactsAndVisibility(); + public void injectActivity(AndroidComponent component) { + component.inject(this); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu items for use in the action bar - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.forum_share_actions, menu); - - return super.onCreateOptionsMenu(menu); + public void showMessageScreen(GroupId groupId, + Collection<ContactId> contacts) { + + ShareForumMessageFragment messageFragment = + ShareForumMessageFragment.newInstance(groupId, contacts); + + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, + android.R.anim.fade_out, + android.R.anim.slide_in_left, + android.R.anim.slide_out_right) + .replace(R.id.shareForumContainer, messageFragment, + ContactSelectorFragment.TAG) + .addToBackStack(null) + .commit(); } - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - // Handle presses on the action bar items - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - case R.id.action_share_forum: - return true; - default: - return super.onOptionsItemSelected(item); + public static ArrayList<Integer> getContactsFromIds( + Collection<ContactId> contacts) { + + // transform ContactIds to Integers so they can be added to a bundle + ArrayList<Integer> intContacts = new ArrayList<>(contacts.size()); + for (ContactId contactId : contacts) { + intContacts.add(contactId.getInt()); } + return intContacts; } - @Override - public void injectActivity(AndroidComponent component) { - component.inject(this); + public void sharingSuccessful(View v) { + setResult(RESULT_OK); + hideSoftKeyboard(v); + supportFinishAfterTransition(); } - private void loadContactsAndVisibility() { - runOnDbThread(new Runnable() { - public void run() { - try { - long now = System.currentTimeMillis(); - List<ContactListItem> contacts = new ArrayList<>(); - Collection<ContactId> selectedContacts = new HashSet<>( - forumSharingManager.getSharedWith(groupId)); - - for (Contact c : contactManager.getActiveContacts()) { - LocalAuthor localAuthor = identityManager - .getLocalAuthor(c.getLocalAuthorId()); - boolean selected = selectedContacts.contains(c.getId()); - contacts.add( - new SelectableContactListItem(c, localAuthor, - groupId, selected)); - } - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Load took " + duration + " ms"); - displayContacts(contacts); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - }); + protected static Collection<ContactId> getContactsFromIntegers( + ArrayList<Integer> intContacts) { + + // turn contact integers from a bundle back to ContactIds + List<ContactId> contacts = new ArrayList<>(intContacts.size()); + for(Integer c : intContacts) { + contacts.add(new ContactId(c)); + } + return contacts; } - private void displayContacts(final List<ContactListItem> contact) { - runOnUiThread(new Runnable() { - public void run() { - adapter.addAll(contact); - } - }); + @Override + public void showLoadingScreen(boolean isBlocking, int stringId) { + // this is handled by the recycler view in ContactSelectorFragment } @Override - public void onItemClick(View view, ContactListItem item) { - ((SelectableContactListItem) item).toggleSelected(); - adapter.notifyItemChanged(adapter.findItemPosition(item), item); + public void hideLoadingScreen() { + // this is handled by the recycler view in ContactSelectorFragment } } diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumMessageFragment.java b/briar-android/src/org/briarproject/android/forum/ShareForumMessageFragment.java new file mode 100644 index 0000000000..7ed79d67cd --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ShareForumMessageFragment.java @@ -0,0 +1,177 @@ +package org.briarproject.android.forum; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import org.briarproject.R; +import org.briarproject.android.AndroidComponent; +import org.briarproject.android.fragment.BaseFragment; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.db.DbException; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.sync.GroupId; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.android.forum.ShareForumActivity.CONTACTS; +import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds; +import static org.briarproject.api.forum.ForumConstants.GROUP_ID; + +public class ShareForumMessageFragment extends BaseFragment { + + private static final Logger LOG = + Logger.getLogger(ShareForumMessageFragment.class.getName()); + + public final static String TAG = "IntroductionMessageFragment"; + private ShareForumActivity shareForumActivity; + private ViewHolder ui; + + // Fields that are accessed from background threads must be volatile + @Inject protected volatile ForumSharingManager forumSharingManager; + private volatile GroupId groupId; + private volatile Collection<ContactId> contacts; + + public static ShareForumMessageFragment newInstance(GroupId groupId, + Collection<ContactId> contacts) { + + ShareForumMessageFragment f = new ShareForumMessageFragment(); + + Bundle args = new Bundle(); + args.putByteArray(GROUP_ID, groupId.getBytes()); + args.putIntegerArrayList(CONTACTS, getContactsFromIds(contacts)); + f.setArguments(args); + + return f; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + shareForumActivity = (ShareForumActivity) context; + } catch (ClassCastException e) { + throw new InstantiationError( + "This fragment is only meant to be attached to the ShareForumActivity"); + } + } + + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + // change toolbar text + ActionBar actionBar = shareForumActivity.getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.forum_share_button); + } + + // allow for home button to act as back button + setHasOptionsMenu(true); + + // inflate view + View v = + inflater.inflate(R.layout.share_forum_message, container, + false); + ui = new ViewHolder(v); + ui.button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onButtonClick(); + } + }); + + // get groupID and contactIDs from fragment arguments + groupId = new GroupId(getArguments().getByteArray(GROUP_ID)); + ArrayList<Integer> intContacts = + getArguments().getIntegerArrayList(CONTACTS); + if (intContacts == null) throw new IllegalArgumentException(); + contacts = ShareForumActivity.getContactsFromIntegers(intContacts); + + return v; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + shareForumActivity.onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public String getUniqueTag() { + return TAG; + } + + public void onButtonClick() { + // disable button to prevent accidental double invitations + ui.button.setEnabled(false); + + String msg = ui.message.getText().toString(); + shareForum(msg); + + // don't wait for the introduction to be made before finishing activity + shareForumActivity.sharingSuccessful(ui.message); + } + + private void shareForum(final String msg) { + shareForumActivity.runOnDbThread(new Runnable() { + public void run() { + try { + for (ContactId c : contacts) { + forumSharingManager + .sendForumInvitation(groupId, c, msg); + } + } catch (DbException e) { + sharingError(); + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void sharingError() { + shareForumActivity.runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(shareForumActivity, + R.string.introduction_error, Toast.LENGTH_SHORT) + .show(); + } + }); + } + + private static class ViewHolder { + final private TextView text; + final private EditText message; + final private Button button; + + ViewHolder(View v) { + text = (TextView) v.findViewById(R.id.introductionText); + message = (EditText) v.findViewById(R.id.invitationMessageView); + button = (Button) v.findViewById(R.id.shareForumButton); + } + } +} -- GitLab