Removed complex premature optimisations from DB/UI interaction.

This commit is contained in:
akwizgran
2013-03-15 16:35:14 +00:00
parent 8b6bbd77ab
commit b8e97b0bc1
7 changed files with 123 additions and 407 deletions

View File

@@ -4,13 +4,8 @@ import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL; import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static net.sf.briar.api.Rating.UNRATED;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -21,7 +16,6 @@ import net.sf.briar.android.BriarService;
import net.sf.briar.android.BriarService.BriarServiceConnection; import net.sf.briar.android.BriarService.BriarServiceConnection;
import net.sf.briar.android.widgets.CommonLayoutParams; import net.sf.briar.android.widgets.CommonLayoutParams;
import net.sf.briar.android.widgets.HorizontalBorder; import net.sf.briar.android.widgets.HorizontalBorder;
import net.sf.briar.api.Rating;
import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DatabaseExecutor; import net.sf.briar.api.db.DatabaseExecutor;
import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.DbException;
@@ -34,10 +28,7 @@ import net.sf.briar.api.db.event.MessageExpiredEvent;
import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.RatingChangedEvent;
import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
import net.sf.briar.api.messaging.Author; 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.GroupId;
import net.sf.briar.api.messaging.Message;
import net.sf.briar.api.messaging.MessageId;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -58,11 +49,7 @@ OnClickListener, OnItemClickListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
private final Map<AuthorId, Rating> ratingCache =
new ConcurrentHashMap<AuthorId, Rating>();
// The following fields must only be accessed from the UI thread
private final Set<MessageId> messageIds = new HashSet<MessageId>();
private String groupName = null; private String groupName = null;
private GroupAdapter adapter = null; private GroupAdapter adapter = null;
private ListView list = null; private ListView list = null;
@@ -107,8 +94,6 @@ OnClickListener, OnItemClickListener {
setContentView(layout); setContentView(layout);
// Listen for messages and groups being added or removed
db.addListener(this);
// Bind to the service so we can wait for the DB to be opened // Bind to the service so we can wait for the DB to be opened
bindService(new Intent(BriarService.class.getName()), bindService(new Intent(BriarService.class.getName()),
serviceConnection, 0); serviceConnection, 0);
@@ -117,6 +102,7 @@ OnClickListener, OnItemClickListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
db.addListener(this);
loadHeaders(); loadHeaders();
} }
@@ -129,8 +115,6 @@ OnClickListener, OnItemClickListener {
// Load the headers from the database // Load the headers from the database
Collection<GroupMessageHeader> headers = Collection<GroupMessageHeader> headers =
db.getMessageHeaders(groupId); db.getMessageHeaders(groupId);
if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers");
// Display the headers in the UI // Display the headers in the UI
displayHeaders(headers); displayHeaders(headers);
} catch(NoSuchSubscriptionException e) { } catch(NoSuchSubscriptionException e) {
@@ -151,15 +135,8 @@ OnClickListener, OnItemClickListener {
private void displayHeaders(final Collection<GroupMessageHeader> headers) { private void displayHeaders(final Collection<GroupMessageHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
ratingCache.clear();
messageIds.clear();
adapter.clear(); adapter.clear();
for(GroupMessageHeader h : headers) { for(GroupMessageHeader h : headers) adapter.add(h);
Author a = h.getAuthor();
if(a != null) ratingCache.put(a.getId(), h.getRating());
messageIds.add(h.getId());
adapter.add(h);
}
adapter.sort(AscendingHeaderComparator.INSTANCE); adapter.sort(AscendingHeaderComparator.INSTANCE);
selectFirstUnread(); selectFirstUnread();
} }
@@ -191,27 +168,27 @@ OnClickListener, OnItemClickListener {
} }
} }
@Override
public void onPause() {
db.removeListener(this);
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
db.removeListener(this);
unbindService(serviceConnection); unbindService(serviceConnection);
} }
// FIXME: Load operations may overlap, resulting in an inconsistent view
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof GroupMessageAddedEvent) { if(e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
Message m = g.getMessage(); if(g.getMessage().getGroup().getId().equals(groupId))
if(m.getGroup().getId().equals(groupId)) loadHeaders();
loadRatingOrAddToGroup(m, g.isIncoming());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); loadHeaders(); // FIXME: Don't reload everything
loadHeaders(); // FIXME: Don't reload unnecessarily
} else if(e instanceof RatingChangedEvent) { } else if(e instanceof RatingChangedEvent) {
RatingChangedEvent r = (RatingChangedEvent) e; loadHeaders();
AuthorId a = r.getAuthorId();
ratingCache.remove(a);
updateRating(a, r.getRating());
} else if(e instanceof SubscriptionRemovedEvent) { } else if(e instanceof SubscriptionRemovedEvent) {
if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) { if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
@@ -220,81 +197,6 @@ OnClickListener, OnItemClickListener {
} }
} }
private void loadRatingOrAddToGroup(Message m, boolean incoming) {
Author a = m.getAuthor();
if(a == null) {
addToGroup(m, UNRATED, incoming);
} else {
Rating r = ratingCache.get(a.getId());
if(r == null) loadRating(m, incoming);
else addToGroup(m, r, incoming);
}
}
private void addToGroup(final Message m, final Rating r,
final boolean incoming) {
runOnUiThread(new Runnable() {
public void run() {
if(messageIds.add(m.getId())) {
adapter.add(new GroupMessageHeader(m.getId(), m.getParent(),
m.getContentType(), m.getSubject(),
m.getTimestamp(),!incoming, false,
m.getGroup().getId(), m.getAuthor(), 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
AuthorId a = m.getAuthor().getId();
Rating r = db.getRating(a);
// Cache the rating
ratingCache.put(a, r);
// 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();
}
}
});
}
private void updateRating(final AuthorId a, final Rating r) {
runOnUiThread(new Runnable() {
public void run() {
boolean affected = false;
int count = adapter.getCount();
for(int i = 0; i < count; i++) {
GroupMessageHeader h = adapter.getItem(i);
Author author = h.getAuthor();
if(author != null && author.getId().equals(a)) {
adapter.remove(h);
adapter.insert(new GroupMessageHeader(h.getId(),
h.getParent(), h.getContentType(),
h.getSubject(), h.getTimestamp(), h.isRead(),
h.isStarred(), h.getGroupId(), h.getAuthor(),
r), i);
affected = true;
}
}
if(affected) list.invalidate();
}
});
}
public void onClick(View view) { public void onClick(View view) {
Intent i = new Intent(this, WriteGroupMessageActivity.class); Intent i = new Intent(this, WriteGroupMessageActivity.class);
i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
@@ -313,10 +215,7 @@ OnClickListener, OnItemClickListener {
i.putExtra("net.sf.briar.GROUP_NAME", groupName); i.putExtra("net.sf.briar.GROUP_NAME", groupName);
i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes()); i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
Author author = item.getAuthor(); Author author = item.getAuthor();
if(author == null) { if(author != null) {
i.putExtra("net.sf.briar.ANONYMOUS", true);
} else {
i.putExtra("net.sf.briar.ANONYMOUS", false);
i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes()); i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName()); i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
i.putExtra("net.sf.briar.RATING", item.getRating().toString()); i.putExtra("net.sf.briar.RATING", item.getRating().toString());

View File

@@ -14,7 +14,6 @@ import java.security.PrivateKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -24,7 +23,6 @@ import net.sf.briar.R;
import net.sf.briar.android.BriarActivity; import net.sf.briar.android.BriarActivity;
import net.sf.briar.android.BriarService; import net.sf.briar.android.BriarService;
import net.sf.briar.android.BriarService.BriarServiceConnection; import net.sf.briar.android.BriarService.BriarServiceConnection;
import net.sf.briar.android.DescendingHeaderComparator;
import net.sf.briar.android.widgets.CommonLayoutParams; import net.sf.briar.android.widgets.CommonLayoutParams;
import net.sf.briar.android.widgets.HorizontalBorder; import net.sf.briar.android.widgets.HorizontalBorder;
import net.sf.briar.api.ContactId; import net.sf.briar.api.ContactId;
@@ -102,8 +100,6 @@ implements OnClickListener, DatabaseListener {
setContentView(layout); setContentView(layout);
// Listen for messages and groups being added or removed
db.addListener(this);
// Bind to the service so we can wait for the DB to be opened // Bind to the service so we can wait for the DB to be opened
bindService(new Intent(BriarService.class.getName()), bindService(new Intent(BriarService.class.getName()),
serviceConnection, 0); serviceConnection, 0);
@@ -203,37 +199,31 @@ implements OnClickListener, DatabaseListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadGroups(); db.addListener(this);
loadHeaders();
} }
private void loadGroups() { private void loadHeaders() {
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
// Wait for the service to be bound and started // Wait for the service to be bound and started
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
// Load the subscribed groups from the DB // Load the subscribed groups from the DB
Collection<Group> groups = db.getSubscriptions(); for(Group g : db.getSubscriptions()) {
if(LOG.isLoggable(INFO))
LOG.info("Loaded " + groups.size() + " groups");
List<GroupListItem> items = new ArrayList<GroupListItem>();
for(Group g : groups) {
// Filter out restricted groups // Filter out restricted groups
if(g.getPublicKey() != null) continue; if(g.getPublicKey() != null) continue;
// Load the message headers
Collection<GroupMessageHeader> headers;
try { try {
headers = db.getMessageHeaders(g.getId()); // Load the headers from the database
Collection<GroupMessageHeader> headers =
db.getMessageHeaders(g.getId());
// Display the headers in the UI
displayHeaders(g, headers);
} catch(NoSuchSubscriptionException e) { } catch(NoSuchSubscriptionException e) {
continue; // Unsubscribed since getSubscriptions() if(LOG.isLoggable(INFO))
LOG.info("Subscription removed");
} }
if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers");
if(!headers.isEmpty())
items.add(createItem(g, headers));
} }
// Display the groups in the UI
displayGroups(Collections.unmodifiableList(items));
} catch(DbException e) { } catch(DbException e) {
if(LOG.isLoggable(WARNING)) if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
@@ -246,25 +236,34 @@ implements OnClickListener, DatabaseListener {
}); });
} }
private GroupListItem createItem(Group group, private void displayHeaders(final Group g,
Collection<GroupMessageHeader> headers) { final Collection<GroupMessageHeader> headers) {
List<GroupMessageHeader> sort =
new ArrayList<GroupMessageHeader>(headers);
Collections.sort(sort, DescendingHeaderComparator.INSTANCE);
return new GroupListItem(group, sort);
}
private void displayGroups(final Collection<GroupListItem> items) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
adapter.clear(); // Remove the old item, if any
for(GroupListItem i : items) adapter.add(i); GroupListItem item = findGroup(g.getId());
adapter.sort(GroupComparator.INSTANCE); if(item != null) adapter.remove(item);
// Add a new item if there are any headers to display
if(!headers.isEmpty()) {
List<GroupMessageHeader> headerList =
new ArrayList<GroupMessageHeader>(headers);
adapter.add(new GroupListItem(g, headerList));
adapter.sort(GroupComparator.INSTANCE);
}
selectFirstUnread(); selectFirstUnread();
} }
}); });
} }
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 selectFirstUnread() { private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount(); int firstUnread = -1, count = adapter.getCount();
for(int i = 0; i < count; i++) { for(int i = 0; i < count; i++) {
@@ -277,10 +276,14 @@ implements OnClickListener, DatabaseListener {
else list.setSelection(firstUnread); else list.setSelection(firstUnread);
} }
@Override
public void onPause() {
db.removeListener(this);
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
db.removeListener(this);
unbindService(serviceConnection); unbindService(serviceConnection);
} }
@@ -288,52 +291,24 @@ implements OnClickListener, DatabaseListener {
startActivity(new Intent(this, WriteGroupMessageActivity.class)); startActivity(new Intent(this, WriteGroupMessageActivity.class));
} }
// FIXME: Load operations may overlap, resulting in an inconsistent view
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof GroupMessageAddedEvent) { if(e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
addToGroup(g.getMessage(), g.isIncoming()); loadHeaders(g.getMessage().getGroup().getId());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); loadHeaders(); // FIXME: Don't reload everything
loadGroups(); // FIXME: Don't reload unnecessarily
} else if(e instanceof SubscriptionRemovedEvent) { } else if(e instanceof SubscriptionRemovedEvent) {
removeGroup(((SubscriptionRemovedEvent) e).getGroupId()); removeGroup(((SubscriptionRemovedEvent) e).getGroupId());
} }
} }
private void addToGroup(final Message m, final boolean incoming) { private void loadHeaders(final GroupId g) {
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() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
// Wait for the service to be bound and started
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
// Load the group from the DB and display it in the UI displayHeaders(db.getGroup(g), db.getMessageHeaders(g));
displayGroup(db.getGroup(g), m, incoming);
} catch(NoSuchSubscriptionException e) { } catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
} catch(DbException e) { } catch(DbException e) {
@@ -348,25 +323,6 @@ implements OnClickListener, DatabaseListener {
}); });
} }
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) { private void removeGroup(final GroupId g) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {

View File

@@ -1,26 +1,20 @@
package net.sf.briar.android.groups; package net.sf.briar.android.groups;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.android.DescendingHeaderComparator;
import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.GroupMessageHeader;
import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.Author;
import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.Group;
import net.sf.briar.api.messaging.GroupId; 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 { class GroupListItem {
private final Set<MessageId> messageIds = new HashSet<MessageId>();
private final Group group; private final Group group;
private String authorName, subject; private final String authorName, subject;
private long timestamp; private final long timestamp;
private int unread; private final int unread;
GroupListItem(Group group, List<GroupMessageHeader> headers) { GroupListItem(Group group, List<GroupMessageHeader> headers) {
if(headers.isEmpty()) throw new IllegalArgumentException(); if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -32,36 +26,9 @@ class GroupListItem {
else authorName = a.getName(); else authorName = a.getName();
subject = newest.getSubject(); subject = newest.getSubject();
timestamp = newest.getTimestamp(); timestamp = newest.getTimestamp();
unread = 0; int unread = 0;
for(GroupMessageHeader h : headers) { for(GroupMessageHeader h : headers) if(!h.isRead()) unread++;
if(!h.isRead()) unread++; this.unread = 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() { GroupId getGroupId() {

View File

@@ -90,11 +90,9 @@ implements OnClickListener {
id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID"); id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
if(id == null) throw new IllegalStateException(); if(id == null) throw new IllegalStateException();
messageId = new MessageId(id); messageId = new MessageId(id);
boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
String authorName = null; String authorName = null;
if(!anonymous) { id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID"); if(id != null) {
if(id == null) throw new IllegalStateException();
authorId = new AuthorId(id); authorId = new AuthorId(id);
authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME"); authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
if(authorName == null) throw new IllegalStateException(); if(authorName == null) throw new IllegalStateException();

View File

@@ -6,8 +6,6 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -29,8 +27,6 @@ import net.sf.briar.api.db.event.DatabaseEvent;
import net.sf.briar.api.db.event.DatabaseListener; import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.MessageExpiredEvent;
import net.sf.briar.api.db.event.PrivateMessageAddedEvent; 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.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@@ -52,8 +48,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
// The following fields must only be accessed from the UI thread
private Set<MessageId> messageIds = new HashSet<MessageId>();
private String contactName = null; private String contactName = null;
private ConversationAdapter adapter = null; private ConversationAdapter adapter = null;
private ListView list = null; private ListView list = null;
@@ -98,8 +92,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
setContentView(layout); setContentView(layout);
// Listen for messages being added or removed
db.addListener(this);
// Bind to the service so we can wait for the DB to be opened // Bind to the service so we can wait for the DB to be opened
bindService(new Intent(BriarService.class.getName()), bindService(new Intent(BriarService.class.getName()),
serviceConnection, 0); serviceConnection, 0);
@@ -108,6 +100,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
db.addListener(this);
loadHeaders(); loadHeaders();
} }
@@ -120,8 +113,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
// Load the headers from the database // Load the headers from the database
Collection<PrivateMessageHeader> headers = Collection<PrivateMessageHeader> headers =
db.getPrivateMessageHeaders(contactId); db.getPrivateMessageHeaders(contactId);
if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers");
// Display the headers in the UI // Display the headers in the UI
displayHeaders(headers); displayHeaders(headers);
} catch(NoSuchContactException e) { } catch(NoSuchContactException e) {
@@ -143,12 +134,8 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
final Collection<PrivateMessageHeader> headers) { final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
messageIds.clear();
adapter.clear(); adapter.clear();
for(PrivateMessageHeader h : headers) { for(PrivateMessageHeader h : headers) adapter.add(h);
messageIds.add(h.getId());
adapter.add(h);
}
adapter.sort(AscendingHeaderComparator.INSTANCE); adapter.sort(AscendingHeaderComparator.INSTANCE);
selectFirstUnread(); selectFirstUnread();
} }
@@ -180,13 +167,18 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
} }
} }
@Override
public void onPause() {
db.removeListener(this);
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
db.removeListener(this);
unbindService(serviceConnection); unbindService(serviceConnection);
} }
// FIXME: Load operations may overlap, resulting in an inconsistent view
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof ContactRemovedEvent) { if(e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
@@ -195,30 +187,13 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
finishOnUiThread(); finishOnUiThread();
} }
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); loadHeaders(); // FIXME: Don't reload everything
loadHeaders(); // FIXME: Don't reload unnecessarily
} else if(e instanceof PrivateMessageAddedEvent) { } else if(e instanceof PrivateMessageAddedEvent) {
PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; if(((PrivateMessageAddedEvent) e).getContactId().equals(contactId))
if(p.getContactId().equals(contactId)) loadHeaders();
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.getId(),
m.getParent(), m.getContentType(), m.getSubject(),
m.getTimestamp(), !incoming, false, contactId,
incoming));
adapter.sort(AscendingHeaderComparator.INSTANCE);
selectFirstUnread();
}
}
});
}
public void onClick(View view) { public void onClick(View view) {
Intent i = new Intent(this, WritePrivateMessageActivity.class); Intent i = new Intent(this, WritePrivateMessageActivity.class);
i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt());

View File

@@ -10,9 +10,7 @@ import java.security.GeneralSecurityException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -89,8 +87,6 @@ implements OnClickListener, DatabaseListener {
setContentView(layout); setContentView(layout);
// Listen for messages being added or removed
db.addListener(this);
// Bind to the service so we can wait for the DB to be opened // Bind to the service so we can wait for the DB to be opened
bindService(new Intent(BriarService.class.getName()), bindService(new Intent(BriarService.class.getName()),
serviceConnection, 0); serviceConnection, 0);
@@ -163,6 +159,7 @@ implements OnClickListener, DatabaseListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
db.addListener(this);
loadHeaders(); loadHeaders();
} }
@@ -173,16 +170,18 @@ implements OnClickListener, DatabaseListener {
// Wait for the service to be bound and started // Wait for the service to be bound and started
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
// Load the contact list from the database // Load the contact list from the database
Collection<Contact> contacts = db.getContacts(); for(Contact c : db.getContacts()) {
if(LOG.isLoggable(INFO)) try {
LOG.info("Loaded " + contacts.size() + " contacts"); // Load the headers from the database
// Load the headers from the database Collection<PrivateMessageHeader> headers =
Collection<PrivateMessageHeader> headers = db.getPrivateMessageHeaders(c.getId());
db.getPrivateMessageHeaders(); // Display the headers in the UI
if(LOG.isLoggable(INFO)) displayHeaders(c, headers);
LOG.info("Loaded " + headers.size() + " headers"); } catch(NoSuchContactException e) {
// Display the headers in the UI if(LOG.isLoggable(INFO))
displayHeaders(contacts, headers); LOG.info("Contact removed");
}
}
} catch(DbException e) { } catch(DbException e) {
if(LOG.isLoggable(WARNING)) if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
@@ -195,40 +194,32 @@ implements OnClickListener, DatabaseListener {
}); });
} }
private void displayHeaders(final Collection<Contact> contacts, private void displayHeaders(final Contact c,
final Collection<PrivateMessageHeader> headers) { final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
adapter.clear(); // Remove the old item, if any
for(ConversationListItem i : sortHeaders(contacts, headers)) ConversationListItem item = findConversation(c.getId());
adapter.add(i); if(item != null) adapter.remove(item);
adapter.sort(ConversationComparator.INSTANCE); // Add a new item if there are any headers to display
if(!headers.isEmpty()) {
List<PrivateMessageHeader> headerList =
new ArrayList<PrivateMessageHeader>(headers);
adapter.add(new ConversationListItem(c, headerList));
adapter.sort(ConversationComparator.INSTANCE);
}
selectFirstUnread(); selectFirstUnread();
} }
}); });
} }
private List<ConversationListItem> sortHeaders(Collection<Contact> contacts, private ConversationListItem findConversation(ContactId c) {
Collection<PrivateMessageHeader> headers) { int count = adapter.getCount();
// Group the headers into conversations, one per contact for(int i = 0; i < count; i++) {
Map<ContactId, List<PrivateMessageHeader>> map = ConversationListItem item = adapter.getItem(i);
new HashMap<ContactId, List<PrivateMessageHeader>>(); if(item.getContactId().equals(c)) return item;
for(Contact c : contacts)
map.put(c.getId(), new ArrayList<PrivateMessageHeader>());
for(PrivateMessageHeader h : headers) {
ContactId id = h.getContactId();
List<PrivateMessageHeader> conversation = map.get(id);
// Ignore header if the contact was added after db.getContacts()
if(conversation != null) conversation.add(h);
} }
// Create a list item for each non-empty conversation return null; // Not found
List<ConversationListItem> list = new ArrayList<ConversationListItem>();
for(Contact c : contacts) {
List<PrivateMessageHeader> conversation = map.get(c.getId());
if(!conversation.isEmpty())
list.add(new ConversationListItem(c, conversation));
}
return list;
} }
private void selectFirstUnread() { private void selectFirstUnread() {
@@ -243,10 +234,14 @@ implements OnClickListener, DatabaseListener {
else list.setSelection(firstUnread); else list.setSelection(firstUnread);
} }
@Override
public void onPause() {
db.removeListener(this);
}
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
db.removeListener(this);
unbindService(serviceConnection); unbindService(serviceConnection);
} }
@@ -254,19 +249,18 @@ implements OnClickListener, DatabaseListener {
startActivity(new Intent(this, WritePrivateMessageActivity.class)); startActivity(new Intent(this, WritePrivateMessageActivity.class));
} }
// FIXME: Load operations may overlap, resulting in an inconsistent view
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof ContactRemovedEvent) { if(e instanceof ContactRemovedEvent) {
removeContact(((ContactRemovedEvent) e).getContactId()); removeConversation(((ContactRemovedEvent) e).getContactId());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); loadHeaders(); // FIXME: Don't reload everything
loadHeaders(); // FIXME: Don't reload unnecessarily
} else if(e instanceof PrivateMessageAddedEvent) { } else if(e instanceof PrivateMessageAddedEvent) {
PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; loadHeaders(((PrivateMessageAddedEvent) e).getContactId());
addToConversation(p.getContactId(), p.getMessage(), p.isIncoming());
} }
} }
private void removeContact(final ContactId c) { private void removeConversation(final ContactId c) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
ConversationListItem item = findConversation(c); ConversationListItem item = findConversation(c);
@@ -278,40 +272,13 @@ implements OnClickListener, DatabaseListener {
}); });
} }
private ConversationListItem findConversation(ContactId c) { private void loadHeaders(final 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() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
// Wait for the service to be bound and started
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
// Load the contact from the DB and display it in the UI Contact contact = db.getContact(c);
displayContact(db.getContact(c), m, incoming); displayHeaders(contact, db.getPrivateMessageHeaders(c));
} catch(NoSuchContactException e) { } catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
} catch(DbException e) { } catch(DbException e) {
@@ -326,25 +293,6 @@ implements OnClickListener, DatabaseListener {
}); });
} }
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 private static class ConversationComparator
implements Comparator<ConversationListItem> { implements Comparator<ConversationListItem> {

View File

@@ -1,25 +1,19 @@
package net.sf.briar.android.messages; package net.sf.briar.android.messages;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.android.DescendingHeaderComparator;
import net.sf.briar.api.Contact; import net.sf.briar.api.Contact;
import net.sf.briar.api.ContactId; import net.sf.briar.api.ContactId;
import net.sf.briar.api.db.PrivateMessageHeader; 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 { class ConversationListItem {
private final Set<MessageId> messageIds = new HashSet<MessageId>();
private final Contact contact; private final Contact contact;
private String subject; private final String subject;
private long timestamp; private final long timestamp;
private int unread; private final int unread;
ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) { ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) {
if(headers.isEmpty()) throw new IllegalArgumentException(); if(headers.isEmpty()) throw new IllegalArgumentException();
@@ -27,30 +21,9 @@ class ConversationListItem {
Collections.sort(headers, DescendingHeaderComparator.INSTANCE); Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
subject = headers.get(0).getSubject(); subject = headers.get(0).getSubject();
timestamp = headers.get(0).getTimestamp(); timestamp = headers.get(0).getTimestamp();
unread = 0; int unread = 0;
for(PrivateMessageHeader h : headers) { for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++;
if(!h.isRead()) unread++; this.unread = 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() { ContactId getContactId() {