mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-14 19:59:05 +01:00
Attached data to DB events to avoid DB lookups; refactored UI code.
Fields in Android UI objects that are accessed from background threads must be declared volatile. UI objects use data attached to DB events to avoid DB lookups, which complicates the UI code but should improve performance.
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
package net.sf.briar.android.contact;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
class ContactComparator implements Comparator<ContactListItem> {
|
||||
|
||||
static final ContactComparator INSTANCE = new ContactComparator();
|
||||
|
||||
public int compare(ContactListItem a, ContactListItem b) {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
|
||||
b.getContactName());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -46,12 +47,13 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
|
||||
private final BriarServiceConnection serviceConnection =
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private ConnectionRegistry connectionRegistry;
|
||||
|
||||
private ContactListAdapter adapter = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -87,8 +89,11 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
|
||||
serviceConnection, 0);
|
||||
|
||||
// Add some fake contacts to the database in a background thread
|
||||
// FIXME: Remove this
|
||||
final DatabaseComponent db = this.db;
|
||||
insertFakeContacts();
|
||||
}
|
||||
|
||||
// FIXME: Remove this
|
||||
private void insertFakeContacts() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -117,7 +122,44 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
reloadContactList();
|
||||
loadContacts();
|
||||
}
|
||||
|
||||
private void loadContacts() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the contacts from the database
|
||||
Collection<Contact> contacts = db.getContacts();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + contacts.size() + " contacts");
|
||||
// Display the contacts in the UI
|
||||
displayContacts(contacts);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for service");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayContacts(final Collection<Contact> contacts) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
adapter.clear();
|
||||
for(Contact c : contacts) {
|
||||
boolean conn = connectionRegistry.isConnected(c.getId());
|
||||
adapter.add(new ContactListItem(c, conn));
|
||||
}
|
||||
adapter.sort(ContactComparator.INSTANCE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,46 +175,9 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
|
||||
}
|
||||
|
||||
public void eventOccurred(DatabaseEvent e) {
|
||||
if(e instanceof ContactAddedEvent) reloadContactList();
|
||||
else if(e instanceof ContactRemovedEvent) reloadContactList();
|
||||
}
|
||||
|
||||
private void reloadContactList() {
|
||||
final DatabaseComponent db = this.db;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the contacts from the database
|
||||
Collection<Contact> contacts = db.getContacts();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + contacts.size() + " contacts");
|
||||
// Update the contact list
|
||||
updateContactList(contacts);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for service");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateContactList(final Collection<Contact> contacts) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
adapter.clear();
|
||||
for(Contact c : contacts) {
|
||||
boolean conn = connectionRegistry.isConnected(c.getId());
|
||||
adapter.add(new ContactListItem(c, conn));
|
||||
}
|
||||
adapter.sort(ContactComparator.INSTANCE);
|
||||
}
|
||||
});
|
||||
// These events should be rare, so just reload the list
|
||||
if(e instanceof ContactAddedEvent) loadContacts();
|
||||
else if(e instanceof ContactRemovedEvent) loadContacts();
|
||||
}
|
||||
|
||||
public void contactConnected(ContactId c) {
|
||||
@@ -197,4 +202,15 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ContactComparator
|
||||
implements Comparator<ContactListItem> {
|
||||
|
||||
static final ContactComparator INSTANCE = new ContactComparator();
|
||||
|
||||
public int compare(ContactListItem a, ContactListItem b) {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(),
|
||||
b.getContactName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static net.sf.briar.api.Rating.UNRATED;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -30,13 +27,13 @@ import net.sf.briar.api.db.GroupMessageHeader;
|
||||
import net.sf.briar.api.db.NoSuchSubscriptionException;
|
||||
import net.sf.briar.api.db.event.DatabaseEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseListener;
|
||||
import net.sf.briar.api.db.event.MessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.GroupMessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.MessageExpiredEvent;
|
||||
import net.sf.briar.api.db.event.RatingChangedEvent;
|
||||
import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
|
||||
import net.sf.briar.api.messaging.Author;
|
||||
import net.sf.briar.api.messaging.AuthorId;
|
||||
import net.sf.briar.api.messaging.GroupId;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageId;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@@ -58,14 +55,17 @@ OnClickListener, OnItemClickListener {
|
||||
private final BriarServiceConnection serviceConnection =
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
|
||||
private GroupId groupId = null;
|
||||
// The following fields must only be accessed from the UI thread
|
||||
private final Set<MessageId> messageIds = new HashSet<MessageId>();
|
||||
private String groupName = null;
|
||||
private GroupAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
private volatile GroupId groupId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -111,32 +111,22 @@ OnClickListener, OnItemClickListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
reloadMessageHeaders();
|
||||
loadHeaders();
|
||||
}
|
||||
|
||||
private void reloadMessageHeaders() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final GroupId groupId = this.groupId;
|
||||
private void loadHeaders() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the message headers from the database
|
||||
// Load the headers from the database
|
||||
Collection<GroupMessageHeader> headers =
|
||||
db.getMessageHeaders(groupId);
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + headers.size() + " headers");
|
||||
// Load the ratings for the authors
|
||||
Map<Author, Rating> ratings = new HashMap<Author, Rating>();
|
||||
for(GroupMessageHeader h : headers) {
|
||||
Author a = h.getAuthor();
|
||||
if(a != null && !ratings.containsKey(a))
|
||||
ratings.put(a, db.getRating(a.getId()));
|
||||
}
|
||||
ratings = Collections.unmodifiableMap(ratings);
|
||||
// Update the conversation
|
||||
updateConversation(headers, ratings);
|
||||
// Display the headers in the UI
|
||||
displayHeaders(headers);
|
||||
} catch(NoSuchSubscriptionException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
|
||||
finishOnUiThread();
|
||||
@@ -152,29 +142,46 @@ OnClickListener, OnItemClickListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateConversation(
|
||||
final Collection<GroupMessageHeader> headers,
|
||||
final Map<Author, Rating> ratings) {
|
||||
private void displayHeaders(final Collection<GroupMessageHeader> headers) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
List<GroupMessageHeader> sort =
|
||||
new ArrayList<GroupMessageHeader>(headers);
|
||||
Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
|
||||
int firstUnread = -1;
|
||||
messageIds.clear();
|
||||
adapter.clear();
|
||||
for(GroupMessageHeader h : sort) {
|
||||
if(firstUnread == -1 && !h.isRead())
|
||||
firstUnread = adapter.getCount();
|
||||
Author a = h.getAuthor();
|
||||
if(a == null) adapter.add(new GroupItem(h, UNRATED));
|
||||
else adapter.add(new GroupItem(h, ratings.get(a)));
|
||||
for(GroupMessageHeader h : headers) {
|
||||
messageIds.add(h.getId());
|
||||
adapter.add(h);
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
adapter.sort(AscendingHeaderComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void selectFirstUnread() {
|
||||
int firstUnread = -1, count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(!adapter.getItem(i).isRead()) {
|
||||
firstUnread = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(count - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int request, int result, Intent data) {
|
||||
if(result == ReadGroupMessageActivity.RESULT_PREV) {
|
||||
int position = request - 1;
|
||||
if(position >= 0 && position < adapter.getCount())
|
||||
showMessage(position);
|
||||
} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
|
||||
int position = request + 1;
|
||||
if(position >= 0 && position < adapter.getCount())
|
||||
showMessage(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -183,38 +190,59 @@ OnClickListener, OnItemClickListener {
|
||||
}
|
||||
|
||||
public void eventOccurred(DatabaseEvent e) {
|
||||
if(e instanceof MessageAddedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
|
||||
reloadMessageHeaders();
|
||||
if(e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
Message m = g.getMessage();
|
||||
if(m.getGroup().getId().equals(groupId))
|
||||
loadRatingOrAddToGroup(m, g.isIncoming());
|
||||
} else if(e instanceof MessageExpiredEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
|
||||
reloadMessageHeaders();
|
||||
} else if(e instanceof RatingChangedEvent) {
|
||||
RatingChangedEvent r = (RatingChangedEvent) e;
|
||||
updateRating(r.getAuthorId(), r.getRating());
|
||||
loadHeaders(); // FIXME: Don't reload unnecessarily
|
||||
} else if(e instanceof SubscriptionRemovedEvent) {
|
||||
SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
|
||||
if(s.getGroupId().equals(groupId)) {
|
||||
if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
|
||||
finishOnUiThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRating(final AuthorId a, final Rating r) {
|
||||
private void loadRatingOrAddToGroup(Message m, boolean incoming) {
|
||||
// FIXME: Cache ratings to avoid hitting the DB
|
||||
if(m.getAuthor() == null) addToGroup(m, UNRATED, incoming);
|
||||
else loadRating(m, incoming);
|
||||
}
|
||||
|
||||
private void addToGroup(final Message m, final Rating r,
|
||||
final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
boolean affected = false;
|
||||
int count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
GroupItem item = adapter.getItem(i);
|
||||
Author author = item.getAuthor();
|
||||
if(author != null && author.getId().equals(a)) {
|
||||
item.setRating(r);
|
||||
affected = true;
|
||||
}
|
||||
if(messageIds.add(m.getId())) {
|
||||
adapter.add(new GroupMessageHeader(m, !incoming, false, r));
|
||||
adapter.sort(AscendingHeaderComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadRating(final Message m, final boolean incoming) {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the rating from the database
|
||||
Rating r = db.getRating(m.getAuthor().getId());
|
||||
// Display the message
|
||||
addToGroup(m, r, incoming);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for service");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if(affected) list.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -222,7 +250,6 @@ OnClickListener, OnItemClickListener {
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(this, WriteGroupMessageActivity.class);
|
||||
i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
|
||||
i.putExtra("net.sf.briar.GROUP_NAME", groupName);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@@ -232,7 +259,7 @@ OnClickListener, OnItemClickListener {
|
||||
}
|
||||
|
||||
private void showMessage(int position) {
|
||||
GroupItem item = adapter.getItem(position);
|
||||
GroupMessageHeader item = adapter.getItem(position);
|
||||
Intent i = new Intent(this, ReadGroupMessageActivity.class);
|
||||
i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
|
||||
i.putExtra("net.sf.briar.GROUP_NAME", groupName);
|
||||
@@ -252,17 +279,4 @@ OnClickListener, OnItemClickListener {
|
||||
i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
|
||||
startActivityForResult(i, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int request, int result, Intent data) {
|
||||
if(result == ReadGroupMessageActivity.RESULT_PREV) {
|
||||
int position = request - 1;
|
||||
if(position >= 0 && position < adapter.getCount())
|
||||
showMessage(position);
|
||||
} else if(result == ReadGroupMessageActivity.RESULT_NEXT) {
|
||||
int position = request + 1;
|
||||
if(position >= 0 && position < adapter.getCount())
|
||||
showMessage(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.sf.briar.R;
|
||||
import net.sf.briar.android.widgets.CommonLayoutParams;
|
||||
import net.sf.briar.android.widgets.HorizontalSpace;
|
||||
import net.sf.briar.api.Rating;
|
||||
import net.sf.briar.api.db.GroupMessageHeader;
|
||||
import net.sf.briar.api.messaging.Author;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -26,21 +27,20 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
class GroupAdapter extends ArrayAdapter<GroupItem> {
|
||||
class GroupAdapter extends ArrayAdapter<GroupMessageHeader> {
|
||||
|
||||
GroupAdapter(Context ctx) {
|
||||
super(ctx, android.R.layout.simple_expandable_list_item_1,
|
||||
new ArrayList<GroupItem>());
|
||||
new ArrayList<GroupMessageHeader>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
GroupItem item = getItem(position);
|
||||
GroupMessageHeader item = getItem(position);
|
||||
Context ctx = getContext();
|
||||
// FIXME: Use a RelativeLayout
|
||||
LinearLayout layout = new LinearLayout(ctx);
|
||||
layout.setOrientation(HORIZONTAL);
|
||||
// layout.setGravity(CENTER_VERTICAL);
|
||||
if(!item.isRead()) {
|
||||
Resources res = ctx.getResources();
|
||||
layout.setBackgroundColor(res.getColor(R.color.unread_background));
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.sf.briar.android.groups;
|
||||
|
||||
import net.sf.briar.api.Rating;
|
||||
import net.sf.briar.api.db.GroupMessageHeader;
|
||||
import net.sf.briar.api.messaging.Author;
|
||||
import net.sf.briar.api.messaging.MessageId;
|
||||
|
||||
// This class is not thread-safe
|
||||
class GroupItem {
|
||||
|
||||
private final GroupMessageHeader header;
|
||||
private Rating rating;
|
||||
|
||||
GroupItem(GroupMessageHeader header, Rating rating) {
|
||||
this.header = header;
|
||||
this.rating = rating;
|
||||
}
|
||||
|
||||
MessageId getId() {
|
||||
return header.getId();
|
||||
}
|
||||
|
||||
Author getAuthor() {
|
||||
return header.getAuthor();
|
||||
}
|
||||
|
||||
String getContentType() {
|
||||
return header.getContentType();
|
||||
}
|
||||
|
||||
String getSubject() {
|
||||
return header.getSubject();
|
||||
}
|
||||
|
||||
long getTimestamp() {
|
||||
return header.getTimestamp();
|
||||
}
|
||||
|
||||
boolean isRead() {
|
||||
return header.isRead();
|
||||
}
|
||||
|
||||
Rating getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
void setRating(Rating rating) {
|
||||
this.rating = rating;
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,14 @@ import net.sf.briar.api.db.GroupMessageHeader;
|
||||
import net.sf.briar.api.db.NoSuchSubscriptionException;
|
||||
import net.sf.briar.api.db.event.DatabaseEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseListener;
|
||||
import net.sf.briar.api.db.event.MessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.GroupMessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.MessageExpiredEvent;
|
||||
import net.sf.briar.api.db.event.SubscriptionAddedEvent;
|
||||
import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
|
||||
import net.sf.briar.api.messaging.Author;
|
||||
import net.sf.briar.api.messaging.AuthorFactory;
|
||||
import net.sf.briar.api.messaging.Group;
|
||||
import net.sf.briar.api.messaging.GroupFactory;
|
||||
import net.sf.briar.api.messaging.GroupId;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageFactory;
|
||||
import android.content.Intent;
|
||||
@@ -65,14 +65,16 @@ implements OnClickListener, DatabaseListener {
|
||||
private final BriarServiceConnection serviceConnection =
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private CryptoComponent crypto;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private AuthorFactory authorFactory;
|
||||
@Inject private GroupFactory groupFactory;
|
||||
@Inject private MessageFactory messageFactory;
|
||||
|
||||
private GroupListAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile CryptoComponent crypto;
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
@Inject private volatile AuthorFactory authorFactory;
|
||||
@Inject private volatile GroupFactory groupFactory;
|
||||
@Inject private volatile MessageFactory messageFactory;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
@@ -83,7 +85,7 @@ implements OnClickListener, DatabaseListener {
|
||||
layout.setGravity(CENTER_HORIZONTAL);
|
||||
|
||||
adapter = new GroupListAdapter(this);
|
||||
ListView list = new ListView(this);
|
||||
list = new ListView(this);
|
||||
// Give me all the width and all the unused height
|
||||
list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
|
||||
list.setAdapter(adapter);
|
||||
@@ -112,9 +114,6 @@ implements OnClickListener, DatabaseListener {
|
||||
|
||||
// FIXME: Remove this
|
||||
private void insertFakeMessages() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final GroupFactory groupFactory = this.groupFactory;
|
||||
final MessageFactory messageFactory = this.messageFactory;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -204,18 +203,16 @@ implements OnClickListener, DatabaseListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
reloadGroupList();
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
private void reloadGroupList() {
|
||||
final DatabaseComponent db = this.db;
|
||||
private void loadGroups() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the groups and message headers from the DB
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Loading groups");
|
||||
// Load the subscribed groups from the DB
|
||||
Collection<Group> groups = db.getSubscriptions();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + groups.size() + " groups");
|
||||
@@ -223,20 +220,20 @@ implements OnClickListener, DatabaseListener {
|
||||
for(Group g : groups) {
|
||||
// Filter out restricted groups
|
||||
if(g.getPublicKey() != null) continue;
|
||||
// Load the message headers
|
||||
Collection<GroupMessageHeader> headers;
|
||||
try {
|
||||
headers = db.getMessageHeaders(g.getId());
|
||||
} catch(NoSuchSubscriptionException e) {
|
||||
// We'll reload the list when we get the event
|
||||
continue;
|
||||
continue; // Unsubscribed since getSubscriptions()
|
||||
}
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + headers.size() + " headers");
|
||||
if(!headers.isEmpty())
|
||||
items.add(createItem(g, headers));
|
||||
}
|
||||
// Update the group list
|
||||
updateGroupList(Collections.unmodifiableList(items));
|
||||
// Display the groups in the UI
|
||||
displayGroups(Collections.unmodifiableList(items));
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
@@ -257,16 +254,29 @@ implements OnClickListener, DatabaseListener {
|
||||
return new GroupListItem(group, sort);
|
||||
}
|
||||
|
||||
private void updateGroupList(final Collection<GroupListItem> items) {
|
||||
private void displayGroups(final Collection<GroupListItem> items) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
adapter.clear();
|
||||
for(GroupListItem i : items) adapter.add(i);
|
||||
adapter.sort(GroupComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void selectFirstUnread() {
|
||||
int firstUnread = -1, count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(adapter.getItem(i).getUnreadCount() > 0) {
|
||||
firstUnread = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(count - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -279,21 +289,96 @@ implements OnClickListener, DatabaseListener {
|
||||
}
|
||||
|
||||
public void eventOccurred(DatabaseEvent e) {
|
||||
if(e instanceof MessageAddedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
|
||||
reloadGroupList();
|
||||
if(e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
addToGroup(g.getMessage(), g.isIncoming());
|
||||
} else if(e instanceof MessageExpiredEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
|
||||
reloadGroupList();
|
||||
} else if(e instanceof SubscriptionAddedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
|
||||
reloadGroupList();
|
||||
loadGroups(); // FIXME: Don't reload unnecessarily
|
||||
} else if(e instanceof SubscriptionRemovedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
|
||||
reloadGroupList();
|
||||
removeGroup(((SubscriptionRemovedEvent) e).getGroupId());
|
||||
}
|
||||
}
|
||||
|
||||
private void addToGroup(final Message m, final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
GroupId g = m.getGroup().getId();
|
||||
GroupListItem item = findGroup(g);
|
||||
if(item == null) {
|
||||
loadGroup(g, m, incoming);
|
||||
} else if(item.add(m, incoming)) {
|
||||
adapter.sort(GroupComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
list.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private GroupListItem findGroup(GroupId g) {
|
||||
int count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
GroupListItem item = adapter.getItem(i);
|
||||
if(item.getGroupId().equals(g)) return item;
|
||||
}
|
||||
return null; // Not found
|
||||
}
|
||||
|
||||
private void loadGroup(final GroupId g, final Message m,
|
||||
final boolean incoming) {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the group from the DB and display it in the UI
|
||||
displayGroup(db.getGroup(g), m, incoming);
|
||||
} catch(NoSuchSubscriptionException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for service");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayGroup(final Group g, final Message m,
|
||||
final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
// The item may have been added since loadGroup() was called
|
||||
GroupListItem item = findGroup(g.getId());
|
||||
if(item == null) {
|
||||
adapter.add(new GroupListItem(g, m, incoming));
|
||||
adapter.sort(GroupComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
} else if(item.add(m, incoming)) {
|
||||
adapter.sort(GroupComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
list.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeGroup(final GroupId g) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
GroupListItem item = findGroup(g);
|
||||
if(item != null) {
|
||||
adapter.remove(item);
|
||||
selectFirstUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class GroupComparator implements Comparator<GroupListItem> {
|
||||
|
||||
private static final GroupComparator INSTANCE = new GroupComparator();
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
package net.sf.briar.android.groups;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import net.sf.briar.android.DescendingHeaderComparator;
|
||||
import net.sf.briar.api.db.GroupMessageHeader;
|
||||
import net.sf.briar.api.messaging.Author;
|
||||
import net.sf.briar.api.messaging.Group;
|
||||
import net.sf.briar.api.messaging.GroupId;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageId;
|
||||
|
||||
// This class is not thread-safe
|
||||
class GroupListItem {
|
||||
|
||||
private final Set<MessageId> messageIds = new HashSet<MessageId>();
|
||||
private final Group group;
|
||||
private final String author, subject;
|
||||
private final long timestamp;
|
||||
private final int unread;
|
||||
private String authorName, subject;
|
||||
private long timestamp;
|
||||
private int unread;
|
||||
|
||||
GroupListItem(Group group, List<GroupMessageHeader> headers) {
|
||||
if(headers.isEmpty()) throw new IllegalArgumentException();
|
||||
@@ -22,13 +28,40 @@ class GroupListItem {
|
||||
Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
|
||||
GroupMessageHeader newest = headers.get(0);
|
||||
Author a = newest.getAuthor();
|
||||
if(a == null) author = null;
|
||||
else author = a.getName();
|
||||
if(a == null) authorName = null;
|
||||
else authorName = a.getName();
|
||||
subject = newest.getSubject();
|
||||
timestamp = newest.getTimestamp();
|
||||
int unread = 0;
|
||||
for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
|
||||
this.unread = unread;
|
||||
unread = 0;
|
||||
for(GroupMessageHeader h : headers) {
|
||||
if(!h.isRead()) unread++;
|
||||
if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
GroupListItem(Group group, Message first, boolean incoming) {
|
||||
this.group = group;
|
||||
Author a = first.getAuthor();
|
||||
if(a == null) authorName = null;
|
||||
else authorName = a.getName();
|
||||
subject = first.getSubject();
|
||||
timestamp = first.getTimestamp();
|
||||
unread = incoming ? 1 : 0;
|
||||
messageIds.add(first.getId());
|
||||
}
|
||||
|
||||
boolean add(Message m, boolean incoming) {
|
||||
if(!messageIds.add(m.getId())) return false;
|
||||
if(m.getTimestamp() > timestamp) {
|
||||
// The added message is the newest
|
||||
Author a = m.getAuthor();
|
||||
if(a == null) authorName = null;
|
||||
else authorName = a.getName();
|
||||
subject = m.getSubject();
|
||||
timestamp = m.getTimestamp();
|
||||
}
|
||||
if(incoming) unread++;
|
||||
return true;
|
||||
}
|
||||
|
||||
GroupId getGroupId() {
|
||||
@@ -40,7 +73,7 @@ class GroupListItem {
|
||||
}
|
||||
|
||||
String getAuthorName() {
|
||||
return author;
|
||||
return authorName;
|
||||
}
|
||||
|
||||
String getSubject() {
|
||||
|
||||
@@ -61,13 +61,7 @@ implements OnClickListener {
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private BundleEncrypter bundleEncrypter;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
|
||||
private GroupId groupId = null;
|
||||
private MessageId messageId = null;
|
||||
private AuthorId authorId = null;
|
||||
private String authorName = null;
|
||||
private Rating rating = UNRATED;
|
||||
private boolean read;
|
||||
private ImageView thumb = null;
|
||||
@@ -76,6 +70,12 @@ implements OnClickListener {
|
||||
private ImageButton replyButton = null;
|
||||
private TextView content = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
private volatile MessageId messageId = null;
|
||||
private volatile AuthorId authorId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -91,6 +91,7 @@ implements OnClickListener {
|
||||
if(id == null) throw new IllegalStateException();
|
||||
messageId = new MessageId(id);
|
||||
boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
|
||||
String authorName = null;
|
||||
if(!anonymous) {
|
||||
id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
|
||||
if(id == null) throw new IllegalStateException();
|
||||
@@ -235,8 +236,6 @@ implements OnClickListener {
|
||||
}
|
||||
|
||||
private void setReadInDatabase(final boolean read) {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageId messageId = this.messageId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -266,8 +265,6 @@ implements OnClickListener {
|
||||
}
|
||||
|
||||
private void loadMessageBody() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageId messageId = this.messageId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -334,8 +331,6 @@ implements OnClickListener {
|
||||
}
|
||||
|
||||
private void setRatingInDatabase(final Rating r) {
|
||||
final DatabaseComponent db = this.db;
|
||||
final AuthorId authorId = this.authorId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
@@ -53,18 +53,19 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private BundleEncrypter bundleEncrypter;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private MessageFactory messageFactory;
|
||||
|
||||
private Group group = null;
|
||||
private GroupId groupId = null;
|
||||
private MessageId parentId = null;
|
||||
private GroupNameSpinnerAdapter adapter = null;
|
||||
private Spinner spinner = null;
|
||||
private ImageButton sendButton = null;
|
||||
private EditText content = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
@Inject private volatile MessageFactory messageFactory;
|
||||
private volatile Group group = null;
|
||||
private volatile GroupId groupId = null;
|
||||
private volatile MessageId parentId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -94,7 +95,7 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
spinner = new Spinner(this);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setOnItemSelectedListener(this);
|
||||
loadContactNames();
|
||||
loadGroupList();
|
||||
actionBar.addView(spinner);
|
||||
|
||||
actionBar.addView(new HorizontalSpace(this));
|
||||
@@ -122,24 +123,12 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
serviceConnection, 0);
|
||||
}
|
||||
|
||||
private void loadContactNames() {
|
||||
final DatabaseComponent db = this.db;
|
||||
private void loadGroupList() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
serviceConnection.waitForStartup();
|
||||
final Collection<Group> groups = db.getSubscriptions();
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
for(Group g : groups) {
|
||||
if(g.getId().equals(groupId)) {
|
||||
group = g;
|
||||
spinner.setSelection(adapter.getCount());
|
||||
}
|
||||
adapter.add(g);
|
||||
}
|
||||
}
|
||||
});
|
||||
updateGroupList(db.getSubscriptions());
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
@@ -151,6 +140,20 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGroupList(final Collection<Group> groups) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
for(Group g : groups) {
|
||||
if(g.getId().equals(groupId)) {
|
||||
group = g;
|
||||
spinner.setSelection(adapter.getCount());
|
||||
}
|
||||
adapter.add(g);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
Parcelable p = content.onSaveInstanceState();
|
||||
@@ -175,10 +178,6 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
}
|
||||
|
||||
private void storeMessage(final byte[] body) {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageFactory messageFactory = this.messageFactory;
|
||||
final Group group = this.group;
|
||||
final MessageId parentId = this.parentId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
@@ -35,12 +35,8 @@ implements InvitationListener {
|
||||
|
||||
@Inject private BundleEncrypter bundleEncrypter;
|
||||
@Inject private CryptoComponent crypto;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private InvitationTaskFactory invitationTaskFactory;
|
||||
@Inject private ReferenceManager referenceManager;
|
||||
|
||||
// All of the following must be accessed on the UI thread
|
||||
private AddContactView view = null;
|
||||
private InvitationTask task = null;
|
||||
private long taskHandle = -1;
|
||||
@@ -52,6 +48,10 @@ implements InvitationListener {
|
||||
private boolean localCompared = false, remoteCompared = false;
|
||||
private boolean localMatched = false, remoteMatched = false;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -217,7 +217,6 @@ implements InvitationListener {
|
||||
}
|
||||
|
||||
void addContactAndFinish(final String nickname) {
|
||||
final DatabaseComponent db = this.db;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
@@ -5,10 +5,9 @@ import static android.widget.LinearLayout.VERTICAL;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -28,8 +27,10 @@ import net.sf.briar.api.db.PrivateMessageHeader;
|
||||
import net.sf.briar.api.db.event.ContactRemovedEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseListener;
|
||||
import net.sf.briar.api.db.event.MessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.MessageExpiredEvent;
|
||||
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageId;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@@ -51,14 +52,17 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
|
||||
private final BriarServiceConnection serviceConnection =
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
|
||||
private ContactId contactId = null;
|
||||
// The following fields must only be accessed from the UI thread
|
||||
private Set<MessageId> messageIds = new HashSet<MessageId>();
|
||||
private String contactName = null;
|
||||
private ConversationAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
private volatile ContactId contactId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -104,24 +108,22 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
reloadMessageHeaders();
|
||||
loadHeaders();
|
||||
}
|
||||
|
||||
private void reloadMessageHeaders() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final ContactId contactId = this.contactId;
|
||||
private void loadHeaders() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the message headers from the database
|
||||
// Load the headers from the database
|
||||
Collection<PrivateMessageHeader> headers =
|
||||
db.getPrivateMessageHeaders(contactId);
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + headers.size() + " headers");
|
||||
// Update the conversation
|
||||
updateConversation(headers);
|
||||
// Display the headers in the UI
|
||||
displayHeaders(headers);
|
||||
} catch(NoSuchContactException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
|
||||
finishOnUiThread();
|
||||
@@ -137,26 +139,34 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateConversation(
|
||||
private void displayHeaders(
|
||||
final Collection<PrivateMessageHeader> headers) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
List<PrivateMessageHeader> sort =
|
||||
new ArrayList<PrivateMessageHeader>(headers);
|
||||
Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
|
||||
int firstUnread = -1;
|
||||
messageIds.clear();
|
||||
adapter.clear();
|
||||
for(PrivateMessageHeader h : sort) {
|
||||
if(firstUnread == -1 && !h.isRead())
|
||||
firstUnread = adapter.getCount();
|
||||
for(PrivateMessageHeader h : headers) {
|
||||
messageIds.add(h.getId());
|
||||
adapter.add(h);
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(adapter.getCount() - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
adapter.sort(AscendingHeaderComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void selectFirstUnread() {
|
||||
int firstUnread = -1, count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(!adapter.getItem(i).isRead()) {
|
||||
firstUnread = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(count - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int request, int result, Intent data) {
|
||||
if(result == ReadPrivateMessageActivity.RESULT_PREV) {
|
||||
@@ -182,17 +192,31 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
|
||||
ContactRemovedEvent c = (ContactRemovedEvent) e;
|
||||
if(c.getContactId().equals(contactId)) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
|
||||
finish();
|
||||
finishOnUiThread();
|
||||
}
|
||||
} else if(e instanceof MessageAddedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
|
||||
reloadMessageHeaders();
|
||||
} else if(e instanceof MessageExpiredEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
|
||||
reloadMessageHeaders();
|
||||
loadHeaders(); // FIXME: Don't reload unnecessarily
|
||||
} else if(e instanceof PrivateMessageAddedEvent) {
|
||||
PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
|
||||
if(p.getContactId().equals(contactId))
|
||||
addToConversation(p.getMessage(), p.isIncoming());
|
||||
}
|
||||
}
|
||||
|
||||
private void addToConversation(final Message m, final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if(messageIds.add(m.getId())) {
|
||||
adapter.add(new PrivateMessageHeader(m, !incoming, false,
|
||||
contactId, incoming));
|
||||
adapter.sort(AscendingHeaderComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onClick(View view) {
|
||||
Intent i = new Intent(this, WritePrivateMessageActivity.class);
|
||||
i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package net.sf.briar.android.messages;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
class ConversationComparator implements Comparator<ConversationListItem> {
|
||||
|
||||
static final ConversationComparator INSTANCE = new ConversationComparator();
|
||||
|
||||
public int compare(ConversationListItem a, ConversationListItem b) {
|
||||
// The item with the newest message comes first
|
||||
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
|
||||
if(aTime > bTime) return -1;
|
||||
if(aTime < bTime) return 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -26,11 +27,13 @@ import net.sf.briar.api.ContactId;
|
||||
import net.sf.briar.api.db.DatabaseComponent;
|
||||
import net.sf.briar.api.db.DatabaseExecutor;
|
||||
import net.sf.briar.api.db.DbException;
|
||||
import net.sf.briar.api.db.NoSuchContactException;
|
||||
import net.sf.briar.api.db.PrivateMessageHeader;
|
||||
import net.sf.briar.api.db.event.ContactRemovedEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseEvent;
|
||||
import net.sf.briar.api.db.event.DatabaseListener;
|
||||
import net.sf.briar.api.db.event.MessageAddedEvent;
|
||||
import net.sf.briar.api.db.event.MessageExpiredEvent;
|
||||
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageFactory;
|
||||
import android.content.Intent;
|
||||
@@ -52,11 +55,13 @@ implements OnClickListener, DatabaseListener {
|
||||
private final BriarServiceConnection serviceConnection =
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private MessageFactory messageFactory;
|
||||
|
||||
private ConversationListAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
@Inject private volatile MessageFactory messageFactory;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
@@ -67,7 +72,7 @@ implements OnClickListener, DatabaseListener {
|
||||
layout.setGravity(CENTER_HORIZONTAL);
|
||||
|
||||
adapter = new ConversationListAdapter(this);
|
||||
ListView list = new ListView(this);
|
||||
list = new ListView(this);
|
||||
// Give me all the width and all the unused height
|
||||
list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
|
||||
list.setAdapter(adapter);
|
||||
@@ -96,8 +101,6 @@ implements OnClickListener, DatabaseListener {
|
||||
|
||||
// FIXME: Remove this
|
||||
private void insertFakeMessages() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageFactory messageFactory = this.messageFactory;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -160,29 +163,26 @@ implements OnClickListener, DatabaseListener {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
reloadMessageHeaders();
|
||||
loadHeaders();
|
||||
}
|
||||
|
||||
private void reloadMessageHeaders() {
|
||||
final DatabaseComponent db = this.db;
|
||||
private void loadHeaders() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the contact list from the database
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Loading contacts");
|
||||
Collection<Contact> contacts = db.getContacts();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + contacts.size() + " contacts");
|
||||
// Load the message headers from the database
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Loading headers");
|
||||
// Load the headers from the database
|
||||
Collection<PrivateMessageHeader> headers =
|
||||
db.getPrivateMessageHeaders();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded " + headers.size() + " headers");
|
||||
// Update the conversation list
|
||||
updateConversationList(contacts, headers);
|
||||
// Display the headers in the UI
|
||||
displayHeaders(contacts, headers);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
@@ -195,7 +195,7 @@ implements OnClickListener, DatabaseListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateConversationList(final Collection<Contact> contacts,
|
||||
private void displayHeaders(final Collection<Contact> contacts,
|
||||
final Collection<PrivateMessageHeader> headers) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
@@ -203,6 +203,7 @@ implements OnClickListener, DatabaseListener {
|
||||
for(ConversationListItem i : sortHeaders(contacts, headers))
|
||||
adapter.add(i);
|
||||
adapter.sort(ConversationComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -230,6 +231,18 @@ implements OnClickListener, DatabaseListener {
|
||||
return list;
|
||||
}
|
||||
|
||||
private void selectFirstUnread() {
|
||||
int firstUnread = -1, count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(adapter.getItem(i).getUnreadCount() > 0) {
|
||||
firstUnread = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(firstUnread == -1) list.setSelection(count - 1);
|
||||
else list.setSelection(firstUnread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -242,12 +255,108 @@ implements OnClickListener, DatabaseListener {
|
||||
}
|
||||
|
||||
public void eventOccurred(DatabaseEvent e) {
|
||||
if(e instanceof MessageAddedEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
|
||||
reloadMessageHeaders();
|
||||
if(e instanceof ContactRemovedEvent) {
|
||||
removeContact(((ContactRemovedEvent) e).getContactId());
|
||||
} else if(e instanceof MessageExpiredEvent) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
|
||||
reloadMessageHeaders();
|
||||
loadHeaders(); // FIXME: Don't reload unnecessarily
|
||||
} else if(e instanceof PrivateMessageAddedEvent) {
|
||||
PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
|
||||
addToConversation(p.getContactId(), p.getMessage(), p.isIncoming());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeContact(final ContactId c) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
ConversationListItem item = findConversation(c);
|
||||
if(item != null) {
|
||||
adapter.remove(item);
|
||||
selectFirstUnread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConversationListItem findConversation(ContactId c) {
|
||||
int count = adapter.getCount();
|
||||
for(int i = 0; i < count; i++) {
|
||||
ConversationListItem item = adapter.getItem(i);
|
||||
if(item.getContactId().equals(c)) return item;
|
||||
}
|
||||
return null; // Not found
|
||||
}
|
||||
|
||||
private void addToConversation(final ContactId c, final Message m,
|
||||
final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
ConversationListItem item = findConversation(c);
|
||||
if(item == null) {
|
||||
loadContact(c, m, incoming);
|
||||
} else if(item.add(m, incoming)) {
|
||||
adapter.sort(ConversationComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
list.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadContact(final ContactId c, final Message m,
|
||||
final boolean incoming) {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
// Wait for the service to be bound and started
|
||||
serviceConnection.waitForStartup();
|
||||
// Load the contact from the DB and display it in the UI
|
||||
displayContact(db.getContact(c), m, incoming);
|
||||
} catch(NoSuchContactException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for service");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayContact(final Contact c, final Message m,
|
||||
final boolean incoming) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
// The item may have been added since loadContact() was called
|
||||
ConversationListItem item = findConversation(c.getId());
|
||||
if(item == null) {
|
||||
adapter.add(new ConversationListItem(c, m, incoming));
|
||||
adapter.sort(ConversationComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
} else if(item.add(m, incoming)) {
|
||||
adapter.sort(ConversationComparator.INSTANCE);
|
||||
selectFirstUnread();
|
||||
list.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ConversationComparator
|
||||
implements Comparator<ConversationListItem> {
|
||||
|
||||
static final ConversationComparator INSTANCE =
|
||||
new ConversationComparator();
|
||||
|
||||
public int compare(ConversationListItem a, ConversationListItem b) {
|
||||
// The item with the newest message comes first
|
||||
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
|
||||
if(aTime > bTime) return -1;
|
||||
if(aTime < bTime) return 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
package net.sf.briar.android.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import net.sf.briar.android.DescendingHeaderComparator;
|
||||
import net.sf.briar.api.Contact;
|
||||
import net.sf.briar.api.ContactId;
|
||||
import net.sf.briar.api.db.PrivateMessageHeader;
|
||||
import net.sf.briar.api.messaging.Message;
|
||||
import net.sf.briar.api.messaging.MessageId;
|
||||
|
||||
// This class is not thread-safe
|
||||
class ConversationListItem {
|
||||
|
||||
private final Set<MessageId> messageIds = new HashSet<MessageId>();
|
||||
private final Contact contact;
|
||||
private final String subject;
|
||||
private final long timestamp;
|
||||
private final int unread;
|
||||
private String subject;
|
||||
private long timestamp;
|
||||
private int unread;
|
||||
|
||||
ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) {
|
||||
if(headers.isEmpty()) throw new IllegalArgumentException();
|
||||
@@ -21,9 +27,30 @@ class ConversationListItem {
|
||||
Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
|
||||
subject = headers.get(0).getSubject();
|
||||
timestamp = headers.get(0).getTimestamp();
|
||||
int unread = 0;
|
||||
for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++;
|
||||
this.unread = unread;
|
||||
unread = 0;
|
||||
for(PrivateMessageHeader h : headers) {
|
||||
if(!h.isRead()) unread++;
|
||||
if(!messageIds.add(h.getId())) throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
ConversationListItem(Contact contact, Message first, boolean incoming) {
|
||||
this.contact = contact;
|
||||
subject = first.getSubject();
|
||||
timestamp = first.getTimestamp();
|
||||
unread = incoming ? 1 : 0;
|
||||
messageIds.add(first.getId());
|
||||
}
|
||||
|
||||
boolean add(Message m, boolean incoming) {
|
||||
if(!messageIds.add(m.getId())) return false;
|
||||
if(m.getTimestamp() > timestamp) {
|
||||
// The added message is the newest
|
||||
subject = m.getSubject();
|
||||
timestamp = m.getTimestamp();
|
||||
}
|
||||
if(incoming) unread++;
|
||||
return true;
|
||||
}
|
||||
|
||||
ContactId getContactId() {
|
||||
|
||||
@@ -53,16 +53,17 @@ implements OnClickListener {
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private BundleEncrypter bundleEncrypter;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
|
||||
private ContactId contactId = null;
|
||||
private MessageId messageId = null;
|
||||
private boolean read;
|
||||
private ImageButton readButton = null, prevButton = null, nextButton = null;
|
||||
private ImageButton replyButton = null;
|
||||
private TextView content = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
private volatile MessageId messageId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -186,8 +187,6 @@ implements OnClickListener {
|
||||
}
|
||||
|
||||
private void setReadInDatabase(final boolean read) {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageId messageId = this.messageId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
@@ -217,8 +216,6 @@ implements OnClickListener {
|
||||
}
|
||||
|
||||
private void loadMessageBody() {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageId messageId = this.messageId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
@@ -53,17 +53,18 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
new BriarServiceConnection();
|
||||
|
||||
@Inject private BundleEncrypter bundleEncrypter;
|
||||
@Inject private DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private Executor dbExecutor;
|
||||
@Inject private MessageFactory messageFactory;
|
||||
|
||||
private ContactId contactId = null;
|
||||
private MessageId parentId = null;
|
||||
private ContactNameSpinnerAdapter adapter = null;
|
||||
private Spinner spinner = null;
|
||||
private ImageButton sendButton = null;
|
||||
private EditText content = null;
|
||||
|
||||
// Fields that are accessed from DB threads must be volatile
|
||||
@Inject private volatile DatabaseComponent db;
|
||||
@Inject @DatabaseExecutor private volatile Executor dbExecutor;
|
||||
@Inject private volatile MessageFactory messageFactory;
|
||||
private volatile ContactId contactId = null;
|
||||
private volatile MessageId parentId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(null);
|
||||
@@ -93,7 +94,7 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
spinner = new Spinner(this);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setOnItemSelectedListener(this);
|
||||
loadContactNames();
|
||||
loadContactList();
|
||||
actionBar.addView(spinner);
|
||||
|
||||
actionBar.addView(new HorizontalSpace(this));
|
||||
@@ -121,22 +122,12 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
serviceConnection, 0);
|
||||
}
|
||||
|
||||
private void loadContactNames() {
|
||||
final DatabaseComponent db = this.db;
|
||||
private void loadContactList() {
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
serviceConnection.waitForStartup();
|
||||
final Collection<Contact> contacts = db.getContacts();
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
for(Contact c : contacts) {
|
||||
if(c.getId().equals(contactId))
|
||||
spinner.setSelection(adapter.getCount());
|
||||
adapter.add(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
updateContactList(db.getContacts());
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
@@ -148,6 +139,18 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateContactList(final Collection<Contact> contacts) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
for(Contact c : contacts) {
|
||||
if(c.getId().equals(contactId))
|
||||
spinner.setSelection(adapter.getCount());
|
||||
adapter.add(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
Parcelable p = content.onSaveInstanceState();
|
||||
@@ -172,10 +175,6 @@ implements OnClickListener, OnItemSelectedListener {
|
||||
}
|
||||
|
||||
private void storeMessage(final byte[] body) {
|
||||
final DatabaseComponent db = this.db;
|
||||
final MessageFactory messageFactory = this.messageFactory;
|
||||
final ContactId contactId = this.contactId;
|
||||
final MessageId parentId = this.parentId;
|
||||
dbExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user