diff --git a/briar-android/res/drawable/ic_check_white.xml b/briar-android/res/drawable/ic_check_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..59f823220d2daf783d765923dad866a8f68653cc --- /dev/null +++ b/briar-android/res/drawable/ic_check_white.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/> +</vector> diff --git a/briar-android/res/layout/list_item_contact.xml b/briar-android/res/layout/list_item_contact.xml index 8f879c90a041b99df3882897e259ba47db12e6b0..4f1410693e48d7875498416ce9f88c4ff918791a 100644 --- a/briar-android/res/layout/list_item_contact.xml +++ b/briar-android/res/layout/list_item_contact.xml @@ -64,6 +64,7 @@ android:layout_height="wrap_content" android:textColor="@android:color/tertiary_text_light" android:textSize="@dimen/text_size_tiny" + android:visibility="gone" tools:text="My Identity"/> </LinearLayout> diff --git a/briar-android/res/layout/list_item_selectable_contact.xml b/briar-android/res/layout/list_item_selectable_contact.xml new file mode 100644 index 0000000000000000000000000000000000000000..920129df01265b3819dbef778e67180441fbf36e --- /dev/null +++ b/briar-android/res/layout/list_item_selectable_contact.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="@dimen/listitem_horizontal_margin" + android:paddingBottom="@dimen/listitem_horizontal_margin" + android:background="?attr/selectableItemBackground" + > + + <de.hdodenhof.circleimageview.CircleImageView + android:id="@+id/avatarView" + android:layout_width="@dimen/listitem_picture_size" + android:layout_height="@dimen/listitem_picture_size" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + android:layout_marginLeft="@dimen/listitem_horizontal_margin" + android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:transitionName="avatar" + app:civ_border_color="@color/briar_text_primary" + app:civ_border_width="@dimen/avatar_border_width" + tools:src="@drawable/ic_launcher"/> + + <TextView + android:id="@+id/nameView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_marginLeft="@dimen/listitem_horizontal_margin" + android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:layout_toEndOf="@+id/avatarView" + android:layout_toLeftOf="@+id/checkBox" + android:layout_toRightOf="@+id/avatarView" + android:maxLines="2" + android:textColor="@android:color/primary_text_light" + android:textSize="@dimen/text_size_large" + tools:text="This is a name of a contact"/> + + <CheckBox + android:id="@+id/checkBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:layout_marginRight="@dimen/listitem_horizontal_margin" + android:clickable="false"/> + + </RelativeLayout> + + <View style="@style/Divider.ContactList"/> + +</LinearLayout> \ No newline at end of file diff --git a/briar-android/res/menu/forum_share_actions.xml b/briar-android/res/menu/forum_share_actions.xml new file mode 100644 index 0000000000000000000000000000000000000000..f128fd17c56f6991fda5bf0766cf9582aae25fc5 --- /dev/null +++ b/briar-android/res/menu/forum_share_actions.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_share_forum" + android:icon="@drawable/ic_check_white" + android:title="@string/forum_share_with_some" + app:showAsAction="always"/> + +</menu> \ No newline at end of file diff --git a/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..590c7a2d747c8c5936786a717d83d1d64ae13b78 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/BaseContactListAdapter.java @@ -0,0 +1,189 @@ +package org.briarproject.android.contact; + +import android.content.Context; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.briarproject.R; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.identity.Author; + +import java.util.List; + +import im.delight.android.identicons.IdenticonDrawable; + +import static android.support.v7.util.SortedList.INVALID_POSITION; + +public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.BaseContactHolder> + extends RecyclerView.Adapter<VH> { + + protected SortedList<ContactListItem> contacts; + protected final OnItemClickListener listener; + protected Context ctx; + + public BaseContactListAdapter(Context context, OnItemClickListener listener) { + this.ctx = context; + this.listener = listener; + this.contacts = new SortedList<ContactListItem>(ContactListItem.class, + new SortedListCallBacks()); + } + + @Override + public void onBindViewHolder(final VH ui, final int position) { + final ContactListItem item = getItem(position); + + Author author = item.getContact().getAuthor(); + ui.avatar.setImageDrawable( + new IdenticonDrawable(author.getId().getBytes())); + String contactName = author.getName(); + ui.name.setText(contactName); + + ui.layout.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) listener.onItemClick(ui.avatar, item); + } + }); + } + + @Override + public int getItemCount() { + return contacts.size(); + } + + public ContactListItem getItem(int position) { + if (position == INVALID_POSITION || contacts.size() <= position) { + return null; // Not found + } + return contacts.get(position); + } + + public void updateItem(int position, ContactListItem item) { + contacts.updateItemAt(position, item); + } + + public int findItemPosition(ContactListItem c) { + return contacts.indexOf(c); + } + + public int findItemPosition(ContactId c) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + ContactListItem item = getItem(i); + if (item.getContact().getId().equals(c)) return i; + } + return INVALID_POSITION; // Not found + } + + public void addAll(List<ContactListItem> contacts) { + this.contacts.addAll(contacts); + } + + public void add(ContactListItem contact) { + contacts.add(contact); + } + + public void remove(ContactListItem contact) { + contacts.remove(contact); + } + + public void clear() { + contacts.clear(); + } + + public static class BaseContactHolder extends RecyclerView.ViewHolder { + public final ViewGroup layout; + public final ImageView avatar; + public final TextView name; + + public BaseContactHolder(View v) { + super(v); + + layout = (ViewGroup) v; + avatar = (ImageView) v.findViewById(R.id.avatarView); + name = (TextView) v.findViewById(R.id.nameView); + } + } + + public int compareContactListItems(ContactListItem c1, ContactListItem c2) { + return compareByName(c1, c2); + } + + protected int compareByName(ContactListItem c1, ContactListItem c2) { + int authorCompare = c1.getLocalAuthor().getName() + .compareTo(c2.getLocalAuthor().getName()); + if (authorCompare == 0) { + // if names are equal, compare by time instead + return compareByTime(c1, c2); + } else { + return authorCompare; + } + } + + protected int compareByTime(ContactListItem c1, ContactListItem c2) { + long time1 = c1.getTimestamp(); + long time2 = c2.getTimestamp(); + if (time1 < time2) return 1; + if (time1 > time2) return -1; + return 0; + } + + protected class SortedListCallBacks extends SortedList.Callback<ContactListItem> { + + @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(ContactListItem c1, ContactListItem c2) { + return compareContactListItems(c1, c2); + } + + @Override + public boolean areItemsTheSame(ContactListItem c1, ContactListItem c2) { + return c1.getContact().getId().equals(c2.getContact().getId()); + } + + @Override + public boolean areContentsTheSame(ContactListItem c1, + ContactListItem c2) { + // check for all properties that influence visual + // representation of contact + if (c1.isConnected() != c2.isConnected()) { + return false; + } + if (c1.getUnreadCount() != c2.getUnreadCount()) { + return false; + } + if (c1.getTimestamp() != c2.getTimestamp()) { + return false; + } + return true; + } + } + + public interface OnItemClickListener { + void onItemClick(View view, ContactListItem item); + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java index becdaa9220d13699c91b10f66f6f4f3fe9c3366a..dee3031f74130789fad146d55eab1266431d7cf1 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java @@ -1,14 +1,7 @@ package org.briarproject.android.contact; 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.support.v4.content.ContextCompat; -import android.support.v7.util.SortedList; -import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; @@ -17,98 +10,12 @@ import android.widget.ImageView; import android.widget.TextView; import org.briarproject.R; -import org.briarproject.api.contact.ContactId; -import org.briarproject.api.identity.Author; -import org.briarproject.api.identity.AuthorId; - -import java.util.List; - -import im.delight.android.identicons.IdenticonDrawable; - -import static android.support.v7.util.SortedList.INVALID_POSITION; public class ContactListAdapter - extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> { - - private final SortedList<ContactListItem> contacts = - new SortedList<ContactListItem>(ContactListItem.class, - new SortedList.Callback<ContactListItem>() { - @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(ContactListItem c1, - ContactListItem c2) { - int authorCompare = 0; - if (chooser) { - authorCompare = c1.getLocalAuthor().getName() - .compareTo( - c2.getLocalAuthor().getName()); - } - if (authorCompare == 0) { - // sort items by time - // and do not take unread messages into account - long time1 = c1.getTimestamp(); - long time2 = c2.getTimestamp(); - if (time1 < time2) return 1; - if (time1 > time2) return -1; - return 0; - } else { - return authorCompare; - } - } + extends BaseContactListAdapter<ContactListAdapter.ContactHolder> { - @Override - public boolean areItemsTheSame(ContactListItem c1, - ContactListItem c2) { - return c1.getContact().getId() - .equals(c2.getContact().getId()); - } - - @Override - public boolean areContentsTheSame(ContactListItem c1, - ContactListItem c2) { - // check for all properties that influence visual - // representation of contact - if (c1.isConnected() != c2.isConnected()) { - return false; - } - if (c1.getUnreadCount() != c2.getUnreadCount()) { - return false; - } - if (c1.getTimestamp() != c2.getTimestamp()) { - return false; - } - return true; - } - }); - private final OnItemClickListener listener; - private final boolean chooser; - private Context ctx; - private AuthorId localAuthorId; - - public ContactListAdapter(Context context, OnItemClickListener listener, - boolean chooser) { - ctx = context; - this.listener = listener; - this.chooser = chooser; + public ContactListAdapter(Context context, OnItemClickListener listener) { + super(context, listener); } @Override @@ -121,38 +28,25 @@ public class ContactListAdapter @Override public void onBindViewHolder(final ContactHolder ui, final int position) { + super.onBindViewHolder(ui, position); + final ContactListItem item = getItem(position); + // name and unread count + String contactName = item.getContact().getAuthor().getName(); int unread = item.getUnreadCount(); - if (!chooser && unread > 0) { - ui.layout.setBackgroundColor( - ContextCompat.getColor(ctx, R.color.unread_background)); - } - - if (item.isConnected()) { - ui.bulb.setImageResource(R.drawable.contact_connected); - } else { - ui.bulb.setImageResource(R.drawable.contact_disconnected); - } - - Author author = item.getContact().getAuthor(); - ui.avatar.setImageDrawable( - new IdenticonDrawable(author.getId().getBytes())); - String contactName = author.getName(); - - if (!chooser && unread > 0) { + if (unread > 0) { // TODO show these in a bubble on top of the avatar ui.name.setText(contactName + " (" + unread + ")"); - } else { - ui.name.setText(contactName); - } - if (chooser) { - ui.identity.setText(item.getLocalAuthor().getName()); + // different background for contacts with unread messages + ui.layout.setBackgroundColor( + ContextCompat.getColor(ctx, R.color.unread_background)); } else { - ui.identity.setVisibility(View.GONE); + ui.name.setText(contactName); } + // date of last message if (item.isEmpty()) { ui.date.setText(R.string.no_private_messages); } else { @@ -162,115 +56,33 @@ public class ContactListAdapter DateUtils.getRelativeTimeSpanString(ctx, timestamp)); } - if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) { - grayOutItem(ui); - } - - ui.layout.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onItemClick(ui.avatar, item); - } - }); - } - - @Override - public int getItemCount() { - return contacts.size(); - } - - /** - * Set the identity from whose perspective the contact shall be chosen. - * This is only used if chooser is true. - * @param authorId The ID of the local Author - */ - public void setLocalAuthor(AuthorId authorId) { - localAuthorId = authorId; - notifyDataSetChanged(); - } - - private void grayOutItem(final ContactHolder ui) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - float alpha = 0.25f; - ui.bulb.setAlpha(alpha); - ui.avatar.setAlpha(alpha); - ui.name.setAlpha(alpha); - ui.date.setAlpha(alpha); - ui.identity.setAlpha(alpha); + // online/offline + if (item.isConnected()) { + ui.bulb.setImageResource(R.drawable.contact_connected); } else { - ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY, - PorterDuff.Mode.MULTIPLY); - ui.bulb.setColorFilter(colorFilter); - ui.avatar.setColorFilter(colorFilter); - ui.name.setEnabled(false); - ui.date.setEnabled(false); - } - } - - public ContactListItem getItem(int position) { - if (position == INVALID_POSITION || contacts.size() <= position) { - return null; // Not found - } - return contacts.get(position); - } - - public void updateItem(int position, ContactListItem item) { - contacts.updateItemAt(position, item); - } - - public int findItemPosition(ContactId c) { - int count = getItemCount(); - for (int i = 0; i < count; i++) { - ContactListItem item = getItem(i); - if (item.getContact().getId().equals(c)) return i; + ui.bulb.setImageResource(R.drawable.contact_disconnected); } - return INVALID_POSITION; // Not found - } - - public void addAll(List<ContactListItem> contacts) { - this.contacts.addAll(contacts); } - public void add(ContactListItem contact) { - contacts.add(contact); - } - - public void remove(ContactListItem contact) { - contacts.remove(contact); - } + protected static class ContactHolder + extends BaseContactListAdapter.BaseContactHolder { - public void clear() { - contacts.beginBatchedUpdates(); - - while(contacts.size() != 0) { - contacts.removeItemAt(0); - } - - contacts.endBatchedUpdates(); - } - - public static class ContactHolder extends RecyclerView.ViewHolder { - public ViewGroup layout; - public ImageView bulb; - public ImageView avatar; - public TextView name; - public TextView identity; - public TextView date; + public final ImageView bulb; + public final TextView date; + public final TextView identity; public ContactHolder(View v) { super(v); - layout = (ViewGroup) v; bulb = (ImageView) v.findViewById(R.id.bulbView); - avatar = (ImageView) v.findViewById(R.id.avatarView); - name = (TextView) v.findViewById(R.id.nameView); - identity = (TextView) v.findViewById(R.id.identityView); date = (TextView) v.findViewById(R.id.dateView); + identity = (TextView) v.findViewById(R.id.identityView); } } - public interface OnItemClickListener { - void onItemClick(View view, ContactListItem item); + @Override + public int compareContactListItems(ContactListItem c1, ContactListItem c2) { + return compareByTime(c1, c2); } } diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index f3737bd1d32953278d550d6b9a26b393917590f8..6e063bb36e9febd56df028754540fbe87277d0fa 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -101,7 +101,7 @@ public class ContactListFragment extends BaseEventFragment { inflater.inflate(R.layout.activity_contact_list, container, false); - ContactListAdapter.OnItemClickListener onItemClickListener = + BaseContactListAdapter.OnItemClickListener onItemClickListener = new ContactListAdapter.OnItemClickListener() { @Override public void onItemClick(View view, ContactListItem item) { @@ -124,8 +124,7 @@ public class ContactListFragment extends BaseEventFragment { } }; - adapter = new ContactListAdapter(getContext(), onItemClickListener, - false); + adapter = new ContactListAdapter(getContext(), onItemClickListener); list = (BriarRecyclerView) contentView.findViewById(R.id.contactList); list.setLayoutManager(new LinearLayoutManager(getContext())); list.setAdapter(adapter); diff --git a/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java b/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java deleted file mode 100644 index 53b6588d7e69583e9752e2b354a05d5835d9bece..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/contact/SelectContactsDialog.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.briarproject.android.contact; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnMultiChoiceClickListener; -import android.support.v7.app.AlertDialog; - -import org.briarproject.R; -import org.briarproject.api.contact.Contact; -import org.briarproject.api.contact.ContactId; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class SelectContactsDialog implements OnMultiChoiceClickListener { - - private Listener listener = null; - private List<Contact> contacts = null; - private Set<ContactId> selected = null; - - public void setListener(Listener listener) { - this.listener = listener; - } - - public void setContacts(Collection<Contact> contacts) { - this.contacts = new ArrayList<Contact>(contacts); - } - - public void setSelected(Collection<ContactId> selected) { - this.selected = new HashSet<ContactId>(selected); - } - - public Dialog build(Context ctx) { - if (listener == null || contacts == null || selected == null) - throw new IllegalStateException(); - AlertDialog.Builder builder = new AlertDialog.Builder(ctx, - R.style.BriarDialogTheme); - int size = contacts.size(); - String[] names = new String[size]; - boolean[] checked = new boolean[size]; - for (int i = 0; i < size; i++) { - Contact c = contacts.get(i); - names[i] = c.getAuthor().getName(); - checked[i] = selected.contains(c.getId()); - } - builder.setMultiChoiceItems(names, checked, this); - builder.setPositiveButton(R.string.done_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - listener.contactsSelected(selected); - } - }); - builder.setNegativeButton(R.string.cancel_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - listener.contactSelectionCancelled(); - } - }); - return builder.create(); - } - - public void onClick(DialogInterface dialog, int which, boolean isChecked) { - if (isChecked) selected.add(contacts.get(which).getId()); - else selected.remove(contacts.get(which).getId()); - } - - public interface Listener { - - void contactsSelected(Collection<ContactId> selected); - - void contactSelectionCancelled(); - } -} diff --git a/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..0e3e81181f37ec6a16b713e8772e09b8386b4077 --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ContactSelectorAdapter.java @@ -0,0 +1,81 @@ +package org.briarproject.android.forum; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; + +import org.briarproject.R; +import org.briarproject.android.contact.BaseContactListAdapter; +import org.briarproject.android.contact.ContactListItem; +import org.briarproject.api.contact.ContactId; + +import java.util.ArrayList; +import java.util.Collection; + +public class ContactSelectorAdapter + extends + BaseContactListAdapter<ContactSelectorAdapter.SelectableContactHolder> { + + public ContactSelectorAdapter(Context context, + OnItemClickListener listener) { + + super(context, listener); + } + + @Override + public SelectableContactHolder onCreateViewHolder(ViewGroup viewGroup, + int i) { + View v = LayoutInflater.from(ctx) + .inflate(R.layout.list_item_selectable_contact, viewGroup, + false); + + return new SelectableContactHolder(v); + } + + @Override + public void onBindViewHolder(final SelectableContactHolder ui, + final int position) { + super.onBindViewHolder(ui, position); + + final SelectableContactListItem item = + (SelectableContactListItem) getItem(position); + + if (item.isSelected()) { + ui.checkBox.setChecked(true); + } else { + ui.checkBox.setChecked(false); + } + } + + public Collection<ContactId> getSelectedContactIds() { + Collection<ContactId> selected = new ArrayList<ContactId>(); + + for (int i = 0; i < contacts.size(); i++) { + SelectableContactListItem item = + (SelectableContactListItem) contacts.get(i); + if (item.isSelected()) selected.add(item.getContact().getId()); + } + + return selected; + } + + protected static class SelectableContactHolder + extends BaseContactListAdapter.BaseContactHolder { + + private final CheckBox checkBox; + + public SelectableContactHolder(View v) { + super(v); + + checkBox = (CheckBox) v.findViewById(R.id.checkBox); + } + } + + @Override + public int compareContactListItems(ContactListItem c1, ContactListItem c2) { + return compareByName(c1, c2); + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index 8f2f58d345e80376716e2e36174f729d0e310c4d..cd44592df40e9cea57de3b5d6313fd7f1d449967 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -2,6 +2,8 @@ package org.briarproject.android.forum; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.ActivityOptionsCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -42,6 +44,8 @@ import java.util.logging.Logger; import javax.inject.Inject; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.View.GONE; @@ -152,9 +156,13 @@ public class ForumActivity extends BriarActivity implements EventListener, return true; case R.id.action_forum_share: Intent i2 = new Intent(this, ShareForumActivity.class); + i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); i2.putExtra(GROUP_ID, groupId.getBytes()); i2.putExtra(FORUM_NAME, forum.getName()); - startActivity(i2); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeCustomAnimation(this, android.R.anim.slide_in_left, + android.R.anim.slide_out_right); + ActivityCompat.startActivity(this, i2, options.toBundle()); return true; default: return super.onOptionsItemSelected(item); diff --git a/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java b/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java deleted file mode 100644 index 583e25d82c03e030edf91321046a6342ab8ddd6c..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/forum/NoContactsDialog.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.briarproject.android.forum; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.support.v7.app.AlertDialog; - -import org.briarproject.R; - -public class NoContactsDialog { - - private Listener listener = null; - - public void setListener(Listener listener) { - this.listener = listener; - } - - public Dialog build(Context ctx) { - if (listener == null) throw new IllegalStateException(); - AlertDialog.Builder builder = new AlertDialog.Builder(ctx, - R.style.BriarDialogTheme); - builder.setMessage(R.string.no_contacts_prompt); - builder.setPositiveButton(R.string.add_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - listener.contactCreationSelected(); - } - }); - builder.setNegativeButton(R.string.cancel_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - listener.contactCreationCancelled(); - } - }); - return builder.create(); - } - - public interface Listener { - - void contactCreationSelected(); - - void contactCreationCancelled(); - } -} diff --git a/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java new file mode 100644 index 0000000000000000000000000000000000000000..93d266c6c03e699d4f436f54588a92519d6da07a --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/SelectableContactListItem.java @@ -0,0 +1,36 @@ +package org.briarproject.android.forum; + +import org.briarproject.android.contact.ContactListItem; +import org.briarproject.android.contact.ConversationItem; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.GroupId; + +import java.util.Collections; + +// This class is not thread-safe +public class SelectableContactListItem extends ContactListItem { + + private boolean selected; + + public SelectableContactListItem(Contact contact, LocalAuthor localAuthor, + GroupId groupId, boolean selected) { + + super(contact, localAuthor, false, groupId, Collections.<ConversationItem>emptyList()); + + this.selected = selected; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public boolean isSelected() { + return selected; + } + + public void toggleSelected() { + selected = !selected; + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java index fd8e805f1852a6c6dc2549e98e134ed43ba1d42c..e2d4926b3b41c15816664fb9581fd810be386014 100644 --- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java @@ -2,67 +2,59 @@ 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 android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RadioButton; -import android.widget.RadioGroup; import org.briarproject.R; import org.briarproject.android.AndroidComponent; import org.briarproject.android.BriarActivity; -import org.briarproject.android.contact.SelectContactsDialog; -import org.briarproject.android.invitation.AddContactActivity; -import org.briarproject.android.util.LayoutUtils; +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.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.HashSet; +import java.util.List; import java.util.logging.Logger; import javax.inject.Inject; -import static android.view.Gravity.CENTER_HORIZONTAL; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static android.widget.LinearLayout.VERTICAL; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.forum.ForumActivity.FORUM_NAME; -import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; -import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP; -public class ShareForumActivity extends BriarActivity -implements OnClickListener, NoContactsDialog.Listener, -SelectContactsDialog.Listener { +public class ShareForumActivity extends BriarActivity implements + BaseContactListAdapter.OnItemClickListener { private static final Logger LOG = Logger.getLogger(ShareForumActivity.class.getName()); - private RadioGroup radioGroup = null; - private RadioButton shareWithAll = null, shareWithSome = null; - private Button shareButton = null; - private ProgressBar progress = null; - private boolean changed = false; + 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 = null; - private volatile Collection<Contact> contacts = null; - private volatile Collection<ContactId> selected = null; + private volatile GroupId groupId; @Override public void onCreate(Bundle state) { super.onCreate(state); + setContentView(R.layout.introduction_contact_chooser); + Intent i = getIntent(); byte[] b = i.getByteArrayExtra(GROUP_ID); if (b == null) throw new IllegalStateException(); @@ -71,86 +63,73 @@ SelectContactsDialog.Listener { if (forumName == null) throw new IllegalStateException(); setTitle(forumName); - LinearLayout layout = new LinearLayout(this); - layout.setLayoutParams(MATCH_MATCH); - layout.setOrientation(VERTICAL); - layout.setGravity(CENTER_HORIZONTAL); - int pad = LayoutUtils.getPadding(this); - layout.setPadding(pad, pad, pad, pad); - - radioGroup = new RadioGroup(this); - radioGroup.setOrientation(VERTICAL); - radioGroup.setPadding(0, 0, 0, pad); - - shareWithAll = new RadioButton(this); - shareWithAll.setId(2); - shareWithAll.setText(R.string.forum_share_with_all); - shareWithAll.setOnClickListener(this); - radioGroup.addView(shareWithAll); - - shareWithSome = new RadioButton(this); - shareWithSome.setId(3); - shareWithSome.setText(R.string.forum_share_with_some); - shareWithSome.setOnClickListener(this); - radioGroup.addView(shareWithSome); - - layout.addView(radioGroup); - - shareButton = new Button(this); - shareButton.setLayoutParams(WRAP_WRAP); - shareButton.setText(R.string.forum_share_button); - shareButton.setOnClickListener(this); - layout.addView(shareButton); - - progress = new ProgressBar(this); - progress.setLayoutParams(WRAP_WRAP); - progress.setIndeterminate(true); - progress.setVisibility(GONE); - layout.addView(progress); - - setContentView(layout); + 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)); } @Override - public void injectActivity(AndroidComponent component) { - component.inject(this); + public void onResume() { + super.onResume(); + + loadContactsAndVisibility(); } - public void onClick(View view) { - if (view == shareWithAll) { - changed = true; - } else if (view == shareWithSome) { - changed = true; - if (contacts == null) loadVisibility(); - else displayVisibility(); - } else if (view == shareButton) { - if (changed) { - share(); - } else { - finish(); - } + @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); + } + + @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: + storeVisibility(); + return true; + default: + return super.onOptionsItemSelected(item); } } - private void share() { - // Replace the button with a progress bar - shareButton.setVisibility(GONE); - progress.setVisibility(VISIBLE); - // Update the group in a background thread - storeVisibility(shareWithAll.isChecked()); + @Override + public void injectActivity(AndroidComponent component) { + component.inject(this); } - private void loadVisibility() { + private void loadContactsAndVisibility() { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); - contacts = contactManager.getActiveContacts(); - selected = forumSharingManager.getSharedWith(groupId); + List<ContactListItem> contacts = + new ArrayList<ContactListItem>(); + Collection<ContactId> selectedContacts = + new HashSet<ContactId>( + 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"); - displayVisibility(); + displayContacts(contacts); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -159,32 +138,22 @@ SelectContactsDialog.Listener { }); } - private void displayVisibility() { + private void displayContacts(final List<ContactListItem> contact) { runOnUiThread(new Runnable() { public void run() { - if (contacts.isEmpty()) { - NoContactsDialog builder = new NoContactsDialog(); - builder.setListener(ShareForumActivity.this); - builder.build(ShareForumActivity.this).show(); - } else { - SelectContactsDialog builder = new SelectContactsDialog(); - builder.setListener(ShareForumActivity.this); - builder.setContacts(contacts); - builder.setSelected(selected); - builder.build(ShareForumActivity.this).show(); - } + adapter.addAll(contact); } }); } - private void storeVisibility(final boolean all) { + private void storeVisibility() { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); - if (all) forumSharingManager.setSharedWithAll(groupId); - else forumSharingManager.setSharedWith(groupId, - selected); + Collection<ContactId> selected = + adapter.getSelectedContactIds(); + forumSharingManager.setSharedWith(groupId, selected); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Update took " + duration + " ms"); @@ -197,20 +166,10 @@ SelectContactsDialog.Listener { }); } - public void contactCreationSelected() { - startActivity(new Intent(this, AddContactActivity.class)); - } - - public void contactCreationCancelled() { - radioGroup.clearCheck(); - } - - public void contactsSelected(Collection<ContactId> selected) { - this.selected = Collections.unmodifiableCollection(selected); - share(); + @Override + public void onItemClick(View view, ContactListItem item) { + ((SelectableContactListItem) item).toggleSelected(); + adapter.notifyItemChanged(adapter.findItemPosition(item), item); } - public void contactSelectionCancelled() { - radioGroup.clearCheck(); - } } diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..8c96163be0b83801984df47842b8513a60336cf2 --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserAdapter.java @@ -0,0 +1,75 @@ +package org.briarproject.android.introduction; + +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.View; + +import org.briarproject.android.contact.ContactListAdapter; +import org.briarproject.android.contact.ContactListItem; +import org.briarproject.api.identity.AuthorId; + +public class ContactChooserAdapter extends ContactListAdapter { + + private AuthorId localAuthorId; + + public ContactChooserAdapter(Context context, + OnItemClickListener listener) { + + super(context, listener); + } + + @Override + public void onBindViewHolder(final ContactHolder ui, final int position) { + super.onBindViewHolder(ui, position); + + final ContactListItem item = getItem(position); + + ui.name.setText(item.getContact().getAuthor().getName()); + + ui.identity.setText(item.getLocalAuthor().getName()); + ui.identity.setVisibility(View.VISIBLE); + + if (!item.getLocalAuthor().getId().equals(localAuthorId)) { + grayOutItem(ui); + } + } + + @Override + public int compareContactListItems(ContactListItem c1, ContactListItem c2) { + return compareByName(c1, c2); + } + + /** + * Set the identity from whose perspective the contact shall be chosen. + * Contacts that belong to a different author will be shown grayed out, + * but are still clickable. + * @param authorId The ID of the local Author + */ + public void setLocalAuthor(AuthorId authorId) { + localAuthorId = authorId; + notifyDataSetChanged(); + } + + private void grayOutItem(final ContactHolder ui) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + float alpha = 0.25f; + ui.bulb.setAlpha(alpha); + ui.avatar.setAlpha(alpha); + ui.name.setAlpha(alpha); + ui.date.setAlpha(alpha); + ui.identity.setAlpha(alpha); + } else { + ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY, + PorterDuff.Mode.MULTIPLY); + ui.bulb.setColorFilter(colorFilter); + ui.avatar.setColorFilter(colorFilter); + ui.name.setEnabled(false); + ui.date.setEnabled(false); + } + } + +} diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java index c381291f2a80bd93fc7e06bf74ac938df507ed79..11e30d6356ab9bfe2b27dffae8a251dc28a88d6d 100644 --- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java +++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java @@ -47,7 +47,7 @@ public class ContactChooserFragment extends BaseFragment { public final static String TAG = "ContactChooserFragment"; private IntroductionActivity introductionActivity; private BriarRecyclerView list; - private ContactListAdapter adapter; + private ContactChooserAdapter adapter; private int contactId; private static final Logger LOG = @@ -111,8 +111,7 @@ public class ContactChooserFragment extends BaseFragment { } } }; - adapter = - new ContactListAdapter(getActivity(), onItemClickListener, true); + adapter = new ContactChooserAdapter(getActivity(), onItemClickListener); list = (BriarRecyclerView) contentView.findViewById(R.id.contactList); list.setLayoutManager(new LinearLayoutManager(getActivity()));