Use a RecyclerView for the Contact List

This commit is contained in:
Torsten Grote
2015-12-22 18:22:51 -02:00
parent d46ad6cc14
commit 4bcd204687
24 changed files with 505 additions and 230 deletions

View File

@@ -1,28 +1,20 @@
package org.briarproject.android.contact;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
@@ -36,43 +28,34 @@ import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Menu.NONE;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ContactListActivity extends BriarActivity
implements OnClickListener, OnItemClickListener, OnCreateContextMenuListener,
EventListener {
implements OnCreateContextMenuListener, EventListener {
private static final int MENU_ITEM_DELETE = 1;
private static final Logger LOG =
Logger.getLogger(ContactListActivity.class.getName());
@Inject private ConnectionRegistry connectionRegistry;
private TextView empty = null;
private ContactListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private RecyclerView list = null;
private ProgressBar loading = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile ContactManager contactManager;
@@ -82,47 +65,28 @@ EventListener {
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
empty = new TextView(this);
empty.setLayoutParams(MATCH_WRAP_1);
empty.setGravity(CENTER);
empty.setTextSize(18);
empty.setText(R.string.no_contacts);
empty.setVisibility(GONE);
layout.addView(empty);
setContentView(R.layout.activity_contact_list);
adapter = new ContactListAdapter(this);
list = new ListView(this);
list.setLayoutParams(MATCH_WRAP_1);
list = (RecyclerView) findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
list.setOnItemClickListener(this);
list.setOnCreateContextMenuListener(this);
list.setVisibility(GONE);
layout.addView(list);
// Show a notice when there are no contacts
empty = (TextView) findViewById(R.id.emptyView);
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
loading = (ProgressBar) findViewById(R.id.progressBar);
loading.setVisibility(VISIBLE);
}
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setGravity(CENTER);
Resources res = getResources();
footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
ImageButton addContactButton = new ImageButton(this);
addContactButton.setBackgroundResource(0);
addContactButton.setImageResource(R.drawable.social_add_person);
addContactButton.setOnClickListener(this);
footer.addView(addContactButton);
layout.addView(footer);
setContentView(layout);
@Override
public void onPause() {
super.onPause();
eventBus.removeListener(this);
}
@Override
@@ -132,12 +96,47 @@ EventListener {
loadContacts();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_list_actions, menu);
// adapt icon color to dark action bar
menu.findItem(R.id.action_social_add_person).getIcon().setColorFilter(
getResources().getColor(R.color.action_bar_text),
PorterDuff.Mode.SRC_IN);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case R.id.action_social_add_person:
startActivity(new Intent(this, AddContactActivity.class));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void loadContacts() {
clearContacts();
runOnUiThread(new Runnable() {
public void run() {
empty.setVisibility(GONE);
list.setVisibility(GONE);
loading.setVisibility(VISIBLE);
}
});
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
List<ContactListItem> contacts =
new ArrayList<ContactListItem>();
for (Contact c : contactManager.getContacts()) {
try {
ContactId id = c.getId();
@@ -145,15 +144,20 @@ EventListener {
messagingManager.getConversationId(id);
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
displayContact(c, conversation, headers);
boolean connected =
connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected,
conversation,
headers));
} catch (NoSuchContactException e) {
// Continue
}
}
displayContacts(contacts);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms");
hideProgressBar();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
@@ -162,110 +166,19 @@ EventListener {
});
}
private void clearContacts() {
private void displayContacts(final List<ContactListItem> contacts) {
runOnUiThread(new Runnable() {
public void run() {
empty.setVisibility(GONE);
list.setVisibility(GONE);
loading.setVisibility(VISIBLE);
adapter.clear();
adapter.notifyDataSetChanged();
}
});
}
private void displayContact(final Contact c, final GroupId conversation,
final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
boolean connected = connectionRegistry.isConnected(c.getId());
// Remove the old item, if any
ContactListItem item = findItem(c.getId());
if (item != null) adapter.remove(item);
// Add a new item
adapter.add(new ContactListItem(c, connected, conversation,
headers));
adapter.sort(ContactListItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
}
});
}
private void hideProgressBar() {
runOnUiThread(new Runnable() {
public void run() {
if (adapter.isEmpty()) empty.setVisibility(VISIBLE);
else list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
}
});
}
private ContactListItem findItem(ContactId c) {
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
ContactListItem item = adapter.getItem(i);
if (item.getContact().getId().equals(c)) return item;
}
return null; // Not found
}
@Override
public void onPause() {
super.onPause();
eventBus.removeListener(this);
}
public void onClick(View view) {
startActivity(new Intent(this, AddContactActivity.class));
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ContactListItem item = adapter.getItem(position);
ContactId contactId = item.getContact().getId();
String contactName = item.getContact().getAuthor().getName();
GroupId groupId = item.getConversationId();
AuthorId localAuthorId = item.getContact().getLocalAuthorId();
Intent i = new Intent(this, ConversationActivity.class);
i.putExtra("briar.CONTACT_ID", contactId.getInt());
i.putExtra("briar.CONTACT_NAME", contactName);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
startActivity(i);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view,
ContextMenu.ContextMenuInfo info) {
String delete = getString(R.string.delete_contact);
menu.add(NONE, MENU_ITEM_DELETE, NONE, delete);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem) {
if (menuItem.getItemId() == MENU_ITEM_DELETE) {
ContextMenuInfo info = menuItem.getMenuInfo();
int position = ((AdapterContextMenuInfo) info).position;
ContactListItem item = adapter.getItem(position);
removeContact(item.getContact().getId());
String deleted = getString(R.string.contact_deleted_toast);
Toast.makeText(this, deleted, LENGTH_SHORT).show();
}
return true;
}
private void removeContact(final ContactId c) {
runOnDbThread(new Runnable() {
public void run() {
try {
contactManager.removeContact(c);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
if(contacts.size() > 0) {
list.setVisibility(VISIBLE);
empty.setVisibility(GONE);
} else {
list.setVisibility(GONE);
empty.setVisibility(VISIBLE);
}
loading.setVisibility(GONE);
adapter.addAll(contacts);
}
});
}
@@ -314,7 +227,7 @@ EventListener {
final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
ContactListItem item = adapter.findItem(c);
if (item != null) {
item.setHeaders(headers);
adapter.notifyDataSetChanged();
@@ -326,10 +239,10 @@ EventListener {
private void removeItem(final ContactId c) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
ContactListItem item = adapter.findItem(c);
if (item != null) {
adapter.remove(item);
adapter.notifyDataSetChanged();
if (adapter.isEmpty()) {
empty.setVisibility(VISIBLE);
list.setVisibility(GONE);
@@ -342,7 +255,7 @@ EventListener {
private void setConnected(final ContactId c, final boolean connected) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
ContactListItem item = adapter.findItem(c);
if (item != null) {
item.setConnected(connected);
adapter.notifyDataSetChanged();

View File

@@ -1,80 +1,194 @@
package org.briarproject.android.contact;
import static android.text.TextUtils.TruncateAt.END;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
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;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ContactListAdapter extends ArrayAdapter<ContactListItem> {
import org.briarproject.R;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.sync.GroupId;
private final int pad;
import java.util.List;
ContactListAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ContactListItem>());
pad = LayoutUtils.getPadding(ctx);
public class ContactListAdapter
extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> {
private 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) {
return (int) (c1.getTimestamp() -
c2.getTimestamp());
}
@Override
public boolean areItemsTheSame(ContactListItem c1,
ContactListItem c2) {
return c1.getContact().getId().equals(c2.getContact().getId());
}
@Override
public boolean areContentsTheSame(ContactListItem c1,
ContactListItem c2) {
return c1.equals(c2);
}
});
private Context ctx;
public ContactListAdapter(Context context) {
ctx = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ContactListItem item = getItem(position);
Context ctx = getContext();
public ContactHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_contact, viewGroup, false);
return new ContactHolder(v);
}
@Override
public void onBindViewHolder(final ContactHolder ui, final int position) {
final ContactListItem item = getItem(position);
Resources res = ctx.getResources();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
layout.setGravity(CENTER_VERTICAL);
int unread = item.getUnreadCount();
if (unread > 0)
layout.setBackgroundColor(res.getColor(R.color.unread_background));
ImageView bulb = new ImageView(ctx);
bulb.setPadding(pad, pad, pad, pad);
if (item.isConnected())
bulb.setImageResource(R.drawable.contact_connected);
else bulb.setImageResource(R.drawable.contact_disconnected);
layout.addView(bulb);
TextView name = new TextView(ctx);
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setSingleLine();
name.setEllipsize(END);
name.setPadding(0, pad, pad, pad);
String contactName = item.getContact().getAuthor().getName();
if (unread > 0) name.setText(contactName + " (" + unread + ")");
else name.setText(contactName);
layout.addView(name);
if (item.isEmpty()) {
TextView noMessages = new TextView(ctx);
noMessages.setPadding(pad, pad, pad, pad);
noMessages.setTextColor(res.getColor(R.color.no_private_messages));
noMessages.setText(R.string.no_private_messages);
layout.addView(noMessages);
} else {
TextView date = new TextView(ctx);
date.setPadding(pad, pad, pad, pad);
long timestamp = item.getTimestamp();
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
layout.addView(date);
if (unread > 0) {
ui.layout.setBackgroundColor(
res.getColor(R.color.unread_background));
}
return layout;
if (item.isConnected()) {
ui.bulb.setImageResource(R.drawable.contact_connected);
} else {
ui.bulb.setImageResource(R.drawable.contact_disconnected);
}
String contactName = item.getContact().getAuthor().getName();
if (unread > 0) {
ui.name.setText(contactName + " (" + unread + ")");
} else {
ui.name.setText(contactName);
}
if (item.isEmpty()) {
ui.date.setText(R.string.no_private_messages);
} else {
long timestamp = item.getTimestamp();
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
}
ui.layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContactId contactId = item.getContact().getId();
String contactName = item.getContact().getAuthor().getName();
GroupId groupId = item.getConversationId();
AuthorId localAuthorId = item.getContact().getLocalAuthorId();
Intent i = new Intent(ctx, ConversationActivity.class);
i.putExtra("briar.CONTACT_ID", contactId.getInt());
i.putExtra("briar.CONTACT_NAME", contactName);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
ctx.startActivity(i);
}
});
}
@Override
public int getItemCount() {
return contacts == null ? 0 : contacts.size();
}
public boolean isEmpty() {
return contacts == null || contacts.size() == 0;
}
public ContactListItem getItem(int position) {
if (position == -1 || contacts.size() <= position) {
return null; // Not found
}
return contacts.get(position);
}
public ContactListItem findItem(ContactId c) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
ContactListItem item = getItem(i);
if (item.getContact().getId().equals(c)) return item;
}
return null; // Not found
}
public void addAll(final List<ContactListItem> contacts) {
this.contacts.addAll(contacts);
}
public void add(final ContactListItem contact) {
this.contacts.add(contact);
}
public void remove(final ContactListItem contact) {
this.contacts.remove(contact);
}
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 TextView name;
public TextView date;
public ContactHolder(View v) {
super(v);
layout = (ViewGroup) v;
bulb = (ImageView) v.findViewById(R.id.bulbView);
name = (TextView) v.findViewById(R.id.nameView);
date = (TextView) v.findViewById(R.id.dateView);
}
}
}

View File

@@ -1,10 +1,16 @@
package org.briarproject.android.contact;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@@ -15,6 +21,7 @@ import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
@@ -66,6 +73,7 @@ import javax.inject.Inject;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.contact.ReadPrivateMessageActivity.RESULT_PREV_NEXT;
@@ -161,6 +169,33 @@ implements EventListener, OnClickListener, OnItemClickListener {
loadHeaders();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_actions, menu);
// adapt icon color to dark action bar
menu.findItem(R.id.action_social_remove_person).getIcon().setColorFilter(
getResources().getColor(R.color.action_bar_text),
PorterDuff.Mode.SRC_IN);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case R.id.action_social_remove_person:
askToRemoveContact();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void loadContactAndGroup() {
runOnDbThread(new Runnable() {
public void run() {
@@ -478,4 +513,59 @@ implements EventListener, OnClickListener, OnItemClickListener {
i.putExtra("briar.POSITION", position);
startActivityForResult(i, REQUEST_READ);
}
private void askToRemoveContact() {
runOnUiThread(new Runnable() {
@Override
public void run() {
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
removeContact();
}
};
AlertDialog.Builder builder =
new AlertDialog.Builder(ConversationActivity.this);
builder.setTitle(
getString(R.string.dialog_title_delete_contact));
builder.setMessage(
getString(R.string.dialog_message_delete_contact));
builder.setPositiveButton(android.R.string.ok, okListener);
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
});
}
private void removeContact() {
runOnDbThread(new Runnable() {
public void run() {
try {
contactManager.removeContact(contactId);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} finally {
finishAfterContactRemoved();
}
}
});
}
private void finishAfterContactRemoved() {
runOnUiThread(new Runnable() {
@Override
public void run() {
String deleted = getString(R.string.contact_deleted_toast);
Toast.makeText(ConversationActivity.this, deleted, LENGTH_SHORT)
.show();
finish();
}
});
}
}