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 0000000000000000000000000000000000000000..b91e96f00b0299104a5145334361b4b9d310df55 --- /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 0000000000000000000000000000000000000000..c6881437b392b42a675d5d84c52b9b5f8e0c03e8 --- /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 0000000000000000000000000000000000000000..88070ea669ea57ab6402247f6c18cebae68eecb9 --- /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 0000000000000000000000000000000000000000..64a027914890b9d91afd500921f8e36b3cbc67fb --- /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 e0daa614ee482e2e339368664b921378b7bd76c0..447d3dc6a3373dfc330fac4167d2f3c4601780cc 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 7c5777e5252ae226ce59b5a33603be3553d64661..8b2ac951ab65f8b7ed6e5565f850a054d06719bf 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 0a7b1ea2bb569a685f65ab15402b3db7c711cbc9..623391cc3b356c755f20b8dd3c855a980abf619a 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 9838eb06a830659af878b5ba9e4adad6232f9acf..82137d92ad531ebbcb49cca43c635d7d39abb961 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 213e412b520aeea2edb35c6c4608bcd72d2ef148..5a62bef1f3c378449c2ce8e8ff1178d9aba76fa9 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 91a006b9f53327f6534bfb30affd395f8599f4ef..cf1a4b738ea6610e31637d67378e50ea40da6af8 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 0000000000000000000000000000000000000000..06bf4a1adb5529fdfee54af829d84f2c9b41a69c --- /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 0000000000000000000000000000000000000000..0bf7d72abe85136c49fb2ce2e89df8e4d6621ba9 --- /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 0000000000000000000000000000000000000000..2d4699abf3e3df877463227d5c028e0bb9c0124c --- /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 fb891d0a1b3b5bb84bac329c397587bfa5cc44ea..9ccf21efdc57355a009f12b141d731b8a00955fd 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 a2aba398f93dee2f6305b346b2d60ad206dff389..7584c87380b1aa0581675df02ffaca9b403aaf8b 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 2fc96b6ab7e54857c3bb53f212b45c50c18fda67..93e95e24f3a0490364db04867365b1054956a18b 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 610b703c17ada2defc6cd39233376929092d2658..3affb3d4836e73ed130a152a1b41505230a7e8e1 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 b398897013f82366dbb252d43e7b679032b5a811..ca4503cf671e23595c7cbcd6d459e6f3c2f92689 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 0e3e81181f37ec6a16b713e8772e09b8386b4077..2f1ce156b65c3ca75406156bf207d27339f27b32 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 0000000000000000000000000000000000000000..73de55ec1817a8f7dff2dde0a87576a1a5b0afca --- /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 c092791a57c5876fd1f632c46240d8d06cf38fd7..9a2ec711d9233f87d2a60ad3d643433511de4419 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 93d266c6c03e699d4f436f54588a92519d6da07a..6b9116ca41a8b2feb935fe0b7e7c123a075376c3 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 ccb0b365d907f65b419929850c9b1d7ea369279e..820e25666fdf0a26d0644a2ffa25cb0031aeb0dc 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 0000000000000000000000000000000000000000..7ed79d67cde563f0669adb6afde95095fbed7b51 --- /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); + } + } +}