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:
akwizgran
2013-03-14 20:58:20 +00:00
parent c783958d94
commit 23ab23a931
31 changed files with 909 additions and 424 deletions

View File

@@ -69,11 +69,11 @@
android:label="@string/messages_title" > android:label="@string/messages_title" >
</activity> </activity>
<activity <activity
android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity" android:name=".android.messages.ReadPrivateMessageActivity"
android:label="@string/messages_title" > android:label="@string/messages_title" >
</activity> </activity>
<activity <activity
android:name="net.sf.briar.android.messages.WritePrivateMessageActivity" android:name=".android.messages.WritePrivateMessageActivity"
android:label="@string/compose_message_title" > android:label="@string/compose_message_title" >
</activity> </activity>
</application> </application>

View File

@@ -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());
}
}

View File

@@ -6,6 +6,7 @@ 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.Comparator;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -46,12 +47,13 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private DatabaseComponent db;
@Inject @DatabaseExecutor private Executor dbExecutor;
@Inject private ConnectionRegistry connectionRegistry; @Inject private ConnectionRegistry connectionRegistry;
private ContactListAdapter adapter = null; 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -87,8 +89,11 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
serviceConnection, 0); serviceConnection, 0);
// Add some fake contacts to the database in a background thread // Add some fake contacts to the database in a background thread
// FIXME: Remove this insertFakeContacts();
final DatabaseComponent db = this.db; }
// FIXME: Remove this
private void insertFakeContacts() {
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -117,7 +122,44 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
@Override @Override
public void onResume() { public void onResume() {
super.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 @Override
@@ -133,46 +175,9 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
} }
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof ContactAddedEvent) reloadContactList(); // These events should be rare, so just reload the list
else if(e instanceof ContactRemovedEvent) reloadContactList(); if(e instanceof ContactAddedEvent) loadContacts();
} else if(e instanceof ContactRemovedEvent) loadContacts();
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);
}
});
} }
public void contactConnected(ContactId c) { 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());
}
}
} }

View File

@@ -6,12 +6,9 @@ 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 static net.sf.briar.api.Rating.UNRATED;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.HashSet;
import java.util.HashMap; import java.util.Set;
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;
@@ -30,13 +27,13 @@ import net.sf.briar.api.db.GroupMessageHeader;
import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.NoSuchSubscriptionException;
import net.sf.briar.api.db.event.DatabaseEvent; 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.MessageAddedEvent; import net.sf.briar.api.db.event.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.MessageExpiredEvent; 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.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,14 +55,17 @@ OnClickListener, OnItemClickListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private DatabaseComponent db; // The following fields must only be accessed from the UI thread
@Inject @DatabaseExecutor private Executor dbExecutor; private final Set<MessageId> messageIds = new HashSet<MessageId>();
private GroupId groupId = null;
private String groupName = null; private String groupName = null;
private GroupAdapter adapter = null; private GroupAdapter adapter = null;
private ListView list = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -111,32 +111,22 @@ OnClickListener, OnItemClickListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
reloadMessageHeaders(); loadHeaders();
} }
private void reloadMessageHeaders() { private void loadHeaders() {
final DatabaseComponent db = this.db;
final GroupId groupId = this.groupId;
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 message 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)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers"); LOG.info("Loaded " + headers.size() + " headers");
// Load the ratings for the authors // Display the headers in the UI
Map<Author, Rating> ratings = new HashMap<Author, Rating>(); displayHeaders(headers);
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);
} catch(NoSuchSubscriptionException e) { } catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
finishOnUiThread(); finishOnUiThread();
@@ -152,29 +142,46 @@ OnClickListener, OnItemClickListener {
}); });
} }
private void updateConversation( private void displayHeaders(final Collection<GroupMessageHeader> headers) {
final Collection<GroupMessageHeader> headers,
final Map<Author, Rating> ratings) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
List<GroupMessageHeader> sort = messageIds.clear();
new ArrayList<GroupMessageHeader>(headers);
Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
int firstUnread = -1;
adapter.clear(); adapter.clear();
for(GroupMessageHeader h : sort) { for(GroupMessageHeader h : headers) {
if(firstUnread == -1 && !h.isRead()) messageIds.add(h.getId());
firstUnread = adapter.getCount(); adapter.add(h);
Author a = h.getAuthor();
if(a == null) adapter.add(new GroupItem(h, UNRATED));
else adapter.add(new GroupItem(h, ratings.get(a)));
} }
if(firstUnread == -1) list.setSelection(adapter.getCount() - 1); adapter.sort(AscendingHeaderComparator.INSTANCE);
else list.setSelection(firstUnread); 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 @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@@ -183,38 +190,59 @@ OnClickListener, OnItemClickListener {
} }
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof MessageAddedEvent) { if(e instanceof GroupMessageAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
reloadMessageHeaders(); Message m = g.getMessage();
if(m.getGroup().getId().equals(groupId))
loadRatingOrAddToGroup(m, g.isIncoming());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
reloadMessageHeaders(); loadHeaders(); // FIXME: Don't reload unnecessarily
} else if(e instanceof RatingChangedEvent) {
RatingChangedEvent r = (RatingChangedEvent) e;
updateRating(r.getAuthorId(), r.getRating());
} else if(e instanceof SubscriptionRemovedEvent) { } else if(e instanceof SubscriptionRemovedEvent) {
SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e; if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) {
if(s.getGroupId().equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
finishOnUiThread(); 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() { runOnUiThread(new Runnable() {
public void run() { public void run() {
boolean affected = false; if(messageIds.add(m.getId())) {
int count = adapter.getCount(); adapter.add(new GroupMessageHeader(m, !incoming, false, r));
for(int i = 0; i < count; i++) { adapter.sort(AscendingHeaderComparator.INSTANCE);
GroupItem item = adapter.getItem(i); selectFirstUnread();
Author author = item.getAuthor(); }
if(author != null && author.getId().equals(a)) { }
item.setRating(r); });
affected = true; }
}
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) { 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());
i.putExtra("net.sf.briar.GROUP_NAME", groupName);
startActivity(i); startActivity(i);
} }
@@ -232,7 +259,7 @@ OnClickListener, OnItemClickListener {
} }
private void showMessage(int position) { private void showMessage(int position) {
GroupItem item = adapter.getItem(position); GroupMessageHeader item = adapter.getItem(position);
Intent i = new Intent(this, ReadGroupMessageActivity.class); Intent i = new Intent(this, ReadGroupMessageActivity.class);
i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
i.putExtra("net.sf.briar.GROUP_NAME", groupName); i.putExtra("net.sf.briar.GROUP_NAME", groupName);
@@ -252,17 +279,4 @@ OnClickListener, OnItemClickListener {
i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1); i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1);
startActivityForResult(i, position); 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);
}
}
} }

View File

@@ -15,6 +15,7 @@ import net.sf.briar.R;
import net.sf.briar.android.widgets.CommonLayoutParams; import net.sf.briar.android.widgets.CommonLayoutParams;
import net.sf.briar.android.widgets.HorizontalSpace; import net.sf.briar.android.widgets.HorizontalSpace;
import net.sf.briar.api.Rating; 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.Author;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
@@ -26,21 +27,20 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
class GroupAdapter extends ArrayAdapter<GroupItem> { class GroupAdapter extends ArrayAdapter<GroupMessageHeader> {
GroupAdapter(Context ctx) { GroupAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1, super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<GroupItem>()); new ArrayList<GroupMessageHeader>());
} }
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
GroupItem item = getItem(position); GroupMessageHeader item = getItem(position);
Context ctx = getContext(); Context ctx = getContext();
// FIXME: Use a RelativeLayout // FIXME: Use a RelativeLayout
LinearLayout layout = new LinearLayout(ctx); LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL); layout.setOrientation(HORIZONTAL);
// layout.setGravity(CENTER_VERTICAL);
if(!item.isRead()) { if(!item.isRead()) {
Resources res = ctx.getResources(); Resources res = ctx.getResources();
layout.setBackgroundColor(res.getColor(R.color.unread_background)); layout.setBackgroundColor(res.getColor(R.color.unread_background));

View File

@@ -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;
}
}

View File

@@ -36,14 +36,14 @@ import net.sf.briar.api.db.GroupMessageHeader;
import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.NoSuchSubscriptionException;
import net.sf.briar.api.db.event.DatabaseEvent; 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.MessageAddedEvent; import net.sf.briar.api.db.event.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.MessageExpiredEvent; 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.db.event.SubscriptionRemovedEvent;
import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.Author;
import net.sf.briar.api.messaging.AuthorFactory; import net.sf.briar.api.messaging.AuthorFactory;
import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.Group;
import net.sf.briar.api.messaging.GroupFactory; 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.Message;
import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageFactory;
import android.content.Intent; import android.content.Intent;
@@ -65,14 +65,16 @@ implements OnClickListener, DatabaseListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); 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 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
@@ -83,7 +85,7 @@ implements OnClickListener, DatabaseListener {
layout.setGravity(CENTER_HORIZONTAL); layout.setGravity(CENTER_HORIZONTAL);
adapter = new GroupListAdapter(this); adapter = new GroupListAdapter(this);
ListView list = new ListView(this); list = new ListView(this);
// Give me all the width and all the unused height // Give me all the width and all the unused height
list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
list.setAdapter(adapter); list.setAdapter(adapter);
@@ -112,9 +114,6 @@ implements OnClickListener, DatabaseListener {
// FIXME: Remove this // FIXME: Remove this
private void insertFakeMessages() { private void insertFakeMessages() {
final DatabaseComponent db = this.db;
final GroupFactory groupFactory = this.groupFactory;
final MessageFactory messageFactory = this.messageFactory;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -204,18 +203,16 @@ implements OnClickListener, DatabaseListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
reloadGroupList(); loadGroups();
} }
private void reloadGroupList() { private void loadGroups() {
final DatabaseComponent db = this.db;
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 groups and message headers from the DB // Load the subscribed groups from the DB
if(LOG.isLoggable(INFO)) LOG.info("Loading groups");
Collection<Group> groups = db.getSubscriptions(); Collection<Group> groups = db.getSubscriptions();
if(LOG.isLoggable(INFO)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + groups.size() + " groups"); LOG.info("Loaded " + groups.size() + " groups");
@@ -223,20 +220,20 @@ implements OnClickListener, DatabaseListener {
for(Group g : groups) { 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; Collection<GroupMessageHeader> headers;
try { try {
headers = db.getMessageHeaders(g.getId()); headers = db.getMessageHeaders(g.getId());
} catch(NoSuchSubscriptionException e) { } catch(NoSuchSubscriptionException e) {
// We'll reload the list when we get the event continue; // Unsubscribed since getSubscriptions()
continue;
} }
if(LOG.isLoggable(INFO)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers"); LOG.info("Loaded " + headers.size() + " headers");
if(!headers.isEmpty()) if(!headers.isEmpty())
items.add(createItem(g, headers)); items.add(createItem(g, headers));
} }
// Update the group list // Display the groups in the UI
updateGroupList(Collections.unmodifiableList(items)); 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);
@@ -257,16 +254,29 @@ implements OnClickListener, DatabaseListener {
return new GroupListItem(group, sort); return new GroupListItem(group, sort);
} }
private void updateGroupList(final Collection<GroupListItem> items) { private void displayGroups(final Collection<GroupListItem> items) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
adapter.clear(); adapter.clear();
for(GroupListItem i : items) adapter.add(i); for(GroupListItem i : items) adapter.add(i);
adapter.sort(GroupComparator.INSTANCE); 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 @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@@ -279,21 +289,96 @@ implements OnClickListener, DatabaseListener {
} }
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof MessageAddedEvent) { if(e instanceof GroupMessageAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
reloadGroupList(); addToGroup(g.getMessage(), g.isIncoming());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading");
reloadGroupList(); loadGroups(); // FIXME: Don't reload unnecessarily
} else if(e instanceof SubscriptionAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading");
reloadGroupList();
} else if(e instanceof SubscriptionRemovedEvent) { } else if(e instanceof SubscriptionRemovedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); removeGroup(((SubscriptionRemovedEvent) e).getGroupId());
reloadGroupList();
} }
} }
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 class GroupComparator implements Comparator<GroupListItem> {
private static final GroupComparator INSTANCE = new GroupComparator(); private static final GroupComparator INSTANCE = new GroupComparator();

View File

@@ -1,20 +1,26 @@
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 final String author, subject; private String authorName, subject;
private final long timestamp; private long timestamp;
private final int unread; private 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();
@@ -22,13 +28,40 @@ class GroupListItem {
Collections.sort(headers, DescendingHeaderComparator.INSTANCE); Collections.sort(headers, DescendingHeaderComparator.INSTANCE);
GroupMessageHeader newest = headers.get(0); GroupMessageHeader newest = headers.get(0);
Author a = newest.getAuthor(); Author a = newest.getAuthor();
if(a == null) author = null; if(a == null) authorName = null;
else author = a.getName(); else authorName = a.getName();
subject = newest.getSubject(); subject = newest.getSubject();
timestamp = newest.getTimestamp(); timestamp = newest.getTimestamp();
int unread = 0; unread = 0;
for(GroupMessageHeader h : headers) if(!h.isRead()) unread++; for(GroupMessageHeader h : headers) {
this.unread = unread; 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() { GroupId getGroupId() {
@@ -40,7 +73,7 @@ class GroupListItem {
} }
String getAuthorName() { String getAuthorName() {
return author; return authorName;
} }
String getSubject() { String getSubject() {

View File

@@ -61,13 +61,7 @@ implements OnClickListener {
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private BundleEncrypter bundleEncrypter; @Inject private BundleEncrypter bundleEncrypter;
@Inject private DatabaseComponent db;
@Inject @DatabaseExecutor private Executor dbExecutor;
private GroupId groupId = null; private GroupId groupId = null;
private MessageId messageId = null;
private AuthorId authorId = null;
private String authorName = null;
private Rating rating = UNRATED; private Rating rating = UNRATED;
private boolean read; private boolean read;
private ImageView thumb = null; private ImageView thumb = null;
@@ -76,6 +70,12 @@ implements OnClickListener {
private ImageButton replyButton = null; private ImageButton replyButton = null;
private TextView content = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -91,6 +91,7 @@ implements OnClickListener {
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); boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false);
String authorName = null;
if(!anonymous) { if(!anonymous) {
id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID"); id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
if(id == null) throw new IllegalStateException(); if(id == null) throw new IllegalStateException();
@@ -235,8 +236,6 @@ implements OnClickListener {
} }
private void setReadInDatabase(final boolean read) { private void setReadInDatabase(final boolean read) {
final DatabaseComponent db = this.db;
final MessageId messageId = this.messageId;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -266,8 +265,6 @@ implements OnClickListener {
} }
private void loadMessageBody() { private void loadMessageBody() {
final DatabaseComponent db = this.db;
final MessageId messageId = this.messageId;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -334,8 +331,6 @@ implements OnClickListener {
} }
private void setRatingInDatabase(final Rating r) { private void setRatingInDatabase(final Rating r) {
final DatabaseComponent db = this.db;
final AuthorId authorId = this.authorId;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {

View File

@@ -53,18 +53,19 @@ implements OnClickListener, OnItemSelectedListener {
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private BundleEncrypter bundleEncrypter; @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 GroupNameSpinnerAdapter adapter = null;
private Spinner spinner = null; private Spinner spinner = null;
private ImageButton sendButton = null; private ImageButton sendButton = null;
private EditText content = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -94,7 +95,7 @@ implements OnClickListener, OnItemSelectedListener {
spinner = new Spinner(this); spinner = new Spinner(this);
spinner.setAdapter(adapter); spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this); spinner.setOnItemSelectedListener(this);
loadContactNames(); loadGroupList();
actionBar.addView(spinner); actionBar.addView(spinner);
actionBar.addView(new HorizontalSpace(this)); actionBar.addView(new HorizontalSpace(this));
@@ -122,24 +123,12 @@ implements OnClickListener, OnItemSelectedListener {
serviceConnection, 0); serviceConnection, 0);
} }
private void loadContactNames() { private void loadGroupList() {
final DatabaseComponent db = this.db;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
final Collection<Group> groups = db.getSubscriptions(); updateGroupList(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);
}
}
});
} 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);
@@ -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 @Override
public void onSaveInstanceState(Bundle state) { public void onSaveInstanceState(Bundle state) {
Parcelable p = content.onSaveInstanceState(); Parcelable p = content.onSaveInstanceState();
@@ -175,10 +178,6 @@ implements OnClickListener, OnItemSelectedListener {
} }
private void storeMessage(final byte[] body) { 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() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {

View File

@@ -35,12 +35,8 @@ implements InvitationListener {
@Inject private BundleEncrypter bundleEncrypter; @Inject private BundleEncrypter bundleEncrypter;
@Inject private CryptoComponent crypto; @Inject private CryptoComponent crypto;
@Inject private DatabaseComponent db;
@Inject @DatabaseExecutor private Executor dbExecutor;
@Inject private InvitationTaskFactory invitationTaskFactory; @Inject private InvitationTaskFactory invitationTaskFactory;
@Inject private ReferenceManager referenceManager; @Inject private ReferenceManager referenceManager;
// All of the following must be accessed on the UI thread
private AddContactView view = null; private AddContactView view = null;
private InvitationTask task = null; private InvitationTask task = null;
private long taskHandle = -1; private long taskHandle = -1;
@@ -52,6 +48,10 @@ implements InvitationListener {
private boolean localCompared = false, remoteCompared = false; private boolean localCompared = false, remoteCompared = false;
private boolean localMatched = false, remoteMatched = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -217,7 +217,6 @@ implements InvitationListener {
} }
void addContactAndFinish(final String nickname) { void addContactAndFinish(final String nickname) {
final DatabaseComponent db = this.db;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {

View File

@@ -5,10 +5,9 @@ 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 java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.HashSet;
import java.util.List; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; 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.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseEvent; 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.MessageAddedEvent;
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.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;
@@ -51,14 +52,17 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private DatabaseComponent db; // The following fields must only be accessed from the UI thread
@Inject @DatabaseExecutor private Executor dbExecutor; private Set<MessageId> messageIds = new HashSet<MessageId>();
private ContactId contactId = null;
private String contactName = null; private String contactName = null;
private ConversationAdapter adapter = null; private ConversationAdapter adapter = null;
private ListView list = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -104,24 +108,22 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
reloadMessageHeaders(); loadHeaders();
} }
private void reloadMessageHeaders() { private void loadHeaders() {
final DatabaseComponent db = this.db;
final ContactId contactId = this.contactId;
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 message 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)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers"); LOG.info("Loaded " + headers.size() + " headers");
// Update the conversation // Display the headers in the UI
updateConversation(headers); displayHeaders(headers);
} catch(NoSuchContactException e) { } catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
finishOnUiThread(); finishOnUiThread();
@@ -137,26 +139,34 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
}); });
} }
private void updateConversation( private void displayHeaders(
final Collection<PrivateMessageHeader> headers) { final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
List<PrivateMessageHeader> sort = messageIds.clear();
new ArrayList<PrivateMessageHeader>(headers);
Collections.sort(sort, AscendingHeaderComparator.INSTANCE);
int firstUnread = -1;
adapter.clear(); adapter.clear();
for(PrivateMessageHeader h : sort) { for(PrivateMessageHeader h : headers) {
if(firstUnread == -1 && !h.isRead()) messageIds.add(h.getId());
firstUnread = adapter.getCount();
adapter.add(h); adapter.add(h);
} }
if(firstUnread == -1) list.setSelection(adapter.getCount() - 1); adapter.sort(AscendingHeaderComparator.INSTANCE);
else list.setSelection(firstUnread); 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 @Override
public void onActivityResult(int request, int result, Intent data) { public void onActivityResult(int request, int result, Intent data) {
if(result == ReadPrivateMessageActivity.RESULT_PREV) { if(result == ReadPrivateMessageActivity.RESULT_PREV) {
@@ -182,17 +192,31 @@ implements DatabaseListener, OnClickListener, OnItemClickListener {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if(c.getContactId().equals(contactId)) { if(c.getContactId().equals(contactId)) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); 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) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); 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) { 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

@@ -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;
}
}

View File

@@ -9,6 +9,7 @@ import java.io.IOException;
import java.security.GeneralSecurityException; 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.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.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;
import net.sf.briar.api.db.NoSuchContactException;
import net.sf.briar.api.db.PrivateMessageHeader; 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.DatabaseEvent;
import net.sf.briar.api.db.event.DatabaseListener; 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.MessageExpiredEvent;
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.Message;
import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageFactory;
import android.content.Intent; import android.content.Intent;
@@ -52,11 +55,13 @@ implements OnClickListener, DatabaseListener {
private final BriarServiceConnection serviceConnection = private final BriarServiceConnection serviceConnection =
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private DatabaseComponent db;
@Inject @DatabaseExecutor private Executor dbExecutor;
@Inject private MessageFactory messageFactory;
private ConversationListAdapter adapter = null; 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
@@ -67,7 +72,7 @@ implements OnClickListener, DatabaseListener {
layout.setGravity(CENTER_HORIZONTAL); layout.setGravity(CENTER_HORIZONTAL);
adapter = new ConversationListAdapter(this); adapter = new ConversationListAdapter(this);
ListView list = new ListView(this); list = new ListView(this);
// Give me all the width and all the unused height // Give me all the width and all the unused height
list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1);
list.setAdapter(adapter); list.setAdapter(adapter);
@@ -96,8 +101,6 @@ implements OnClickListener, DatabaseListener {
// FIXME: Remove this // FIXME: Remove this
private void insertFakeMessages() { private void insertFakeMessages() {
final DatabaseComponent db = this.db;
final MessageFactory messageFactory = this.messageFactory;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -160,29 +163,26 @@ implements OnClickListener, DatabaseListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
reloadMessageHeaders(); loadHeaders();
} }
private void reloadMessageHeaders() { private void loadHeaders() {
final DatabaseComponent db = this.db;
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 contact list from the database // Load the contact list from the database
if(LOG.isLoggable(INFO)) LOG.info("Loading contacts");
Collection<Contact> contacts = db.getContacts(); Collection<Contact> contacts = db.getContacts();
if(LOG.isLoggable(INFO)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + contacts.size() + " contacts"); LOG.info("Loaded " + contacts.size() + " contacts");
// Load the message headers from the database // Load the headers from the database
if(LOG.isLoggable(INFO)) LOG.info("Loading headers");
Collection<PrivateMessageHeader> headers = Collection<PrivateMessageHeader> headers =
db.getPrivateMessageHeaders(); db.getPrivateMessageHeaders();
if(LOG.isLoggable(INFO)) if(LOG.isLoggable(INFO))
LOG.info("Loaded " + headers.size() + " headers"); LOG.info("Loaded " + headers.size() + " headers");
// Update the conversation list // Display the headers in the UI
updateConversationList(contacts, headers); displayHeaders(contacts, headers);
} 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,7 +195,7 @@ implements OnClickListener, DatabaseListener {
}); });
} }
private void updateConversationList(final Collection<Contact> contacts, private void displayHeaders(final Collection<Contact> contacts,
final Collection<PrivateMessageHeader> headers) { final Collection<PrivateMessageHeader> headers) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
@@ -203,6 +203,7 @@ implements OnClickListener, DatabaseListener {
for(ConversationListItem i : sortHeaders(contacts, headers)) for(ConversationListItem i : sortHeaders(contacts, headers))
adapter.add(i); adapter.add(i);
adapter.sort(ConversationComparator.INSTANCE); adapter.sort(ConversationComparator.INSTANCE);
selectFirstUnread();
} }
}); });
} }
@@ -230,6 +231,18 @@ implements OnClickListener, DatabaseListener {
return list; 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 @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@@ -242,12 +255,108 @@ implements OnClickListener, DatabaseListener {
} }
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof MessageAddedEvent) { if(e instanceof ContactRemovedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); removeContact(((ContactRemovedEvent) e).getContactId());
reloadMessageHeaders();
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); 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;
} }
} }
} }

View File

@@ -1,19 +1,25 @@
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 final String subject; private String subject;
private final long timestamp; private long timestamp;
private final int unread; private 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();
@@ -21,9 +27,30 @@ 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();
int unread = 0; unread = 0;
for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++; for(PrivateMessageHeader h : headers) {
this.unread = unread; 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() { ContactId getContactId() {

View File

@@ -53,16 +53,17 @@ implements OnClickListener {
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private BundleEncrypter bundleEncrypter; @Inject private BundleEncrypter bundleEncrypter;
@Inject private DatabaseComponent db;
@Inject @DatabaseExecutor private Executor dbExecutor;
private ContactId contactId = null; private ContactId contactId = null;
private MessageId messageId = null;
private boolean read; private boolean read;
private ImageButton readButton = null, prevButton = null, nextButton = null; private ImageButton readButton = null, prevButton = null, nextButton = null;
private ImageButton replyButton = null; private ImageButton replyButton = null;
private TextView content = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -186,8 +187,6 @@ implements OnClickListener {
} }
private void setReadInDatabase(final boolean read) { private void setReadInDatabase(final boolean read) {
final DatabaseComponent db = this.db;
final MessageId messageId = this.messageId;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
@@ -217,8 +216,6 @@ implements OnClickListener {
} }
private void loadMessageBody() { private void loadMessageBody() {
final DatabaseComponent db = this.db;
final MessageId messageId = this.messageId;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {

View File

@@ -53,17 +53,18 @@ implements OnClickListener, OnItemSelectedListener {
new BriarServiceConnection(); new BriarServiceConnection();
@Inject private BundleEncrypter bundleEncrypter; @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 ContactNameSpinnerAdapter adapter = null;
private Spinner spinner = null; private Spinner spinner = null;
private ImageButton sendButton = null; private ImageButton sendButton = null;
private EditText content = 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 @Override
public void onCreate(Bundle state) { public void onCreate(Bundle state) {
super.onCreate(null); super.onCreate(null);
@@ -93,7 +94,7 @@ implements OnClickListener, OnItemSelectedListener {
spinner = new Spinner(this); spinner = new Spinner(this);
spinner.setAdapter(adapter); spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this); spinner.setOnItemSelectedListener(this);
loadContactNames(); loadContactList();
actionBar.addView(spinner); actionBar.addView(spinner);
actionBar.addView(new HorizontalSpace(this)); actionBar.addView(new HorizontalSpace(this));
@@ -121,22 +122,12 @@ implements OnClickListener, OnItemSelectedListener {
serviceConnection, 0); serviceConnection, 0);
} }
private void loadContactNames() { private void loadContactList() {
final DatabaseComponent db = this.db;
dbExecutor.execute(new Runnable() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {
serviceConnection.waitForStartup(); serviceConnection.waitForStartup();
final Collection<Contact> contacts = db.getContacts(); updateContactList(db.getContacts());
runOnUiThread(new Runnable() {
public void run() {
for(Contact c : contacts) {
if(c.getId().equals(contactId))
spinner.setSelection(adapter.getCount());
adapter.add(c);
}
}
});
} 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);
@@ -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 @Override
public void onSaveInstanceState(Bundle state) { public void onSaveInstanceState(Bundle state) {
Parcelable p = content.onSaveInstanceState(); Parcelable p = content.onSaveInstanceState();
@@ -172,10 +175,6 @@ implements OnClickListener, OnItemSelectedListener {
} }
private void storeMessage(final byte[] body) { 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() { dbExecutor.execute(new Runnable() {
public void run() { public void run() {
try { try {

View File

@@ -157,9 +157,15 @@ public interface DatabaseComponent {
/** Returns the configuration for the given transport. */ /** Returns the configuration for the given transport. */
TransportConfig getConfig(TransportId t) throws DbException; TransportConfig getConfig(TransportId t) throws DbException;
/** Returns the contact with the given ID. */
Contact getContact(ContactId c) throws DbException;
/** Returns all contacts. */ /** Returns all contacts. */
Collection<Contact> getContacts() throws DbException; Collection<Contact> getContacts() throws DbException;
/** Returns the group with the given ID, if the user subscribes to it. */
Group getGroup(GroupId g) throws DbException;
/** Returns the local transport properties for the given transport. */ /** Returns the local transport properties for the given transport. */
TransportProperties getLocalProperties(TransportId t) throws DbException; TransportProperties getLocalProperties(TransportId t) throws DbException;

View File

@@ -1,20 +1,31 @@
package net.sf.briar.api.db; package net.sf.briar.api.db;
import net.sf.briar.api.Rating;
import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.Author;
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 net.sf.briar.api.messaging.MessageId;
public class GroupMessageHeader extends MessageHeader { public class GroupMessageHeader extends MessageHeader {
private final GroupId groupId; private final GroupId groupId;
private final Author author; private final Author author;
private final Rating rating;
public GroupMessageHeader(MessageId id, MessageId parent, public GroupMessageHeader(MessageId id, MessageId parent,
String contentType, String subject, long timestamp, boolean read, String contentType, String subject, long timestamp, boolean read,
boolean starred, GroupId groupId, Author author) { boolean starred, GroupId groupId, Author author, Rating rating) {
super(id, parent, contentType, subject, timestamp, read, starred); super(id, parent, contentType, subject, timestamp, read, starred);
this.groupId = groupId; this.groupId = groupId;
this.author = author; this.author = author;
this.rating = rating;
}
public GroupMessageHeader(Message m, boolean read, boolean starred,
Rating rating) {
this(m.getId(), m.getParent(), m.getContentType(), m.getSubject(),
m.getTimestamp(), read, starred, m.getGroup().getId(),
m.getAuthor(), rating);
} }
/** Returns the ID of the group to which the message belongs. */ /** Returns the ID of the group to which the message belongs. */
@@ -28,4 +39,12 @@ public class GroupMessageHeader extends MessageHeader {
public Author getAuthor() { public Author getAuthor() {
return author; return author;
} }
/**
* Returns the rating for the message's author, or Rating.UNRATED if this
* is an anonymous message.
*/
public Rating getRating() {
return rating;
}
} }

View File

@@ -1,6 +1,7 @@
package net.sf.briar.api.db; package net.sf.briar.api.db;
import net.sf.briar.api.ContactId; import net.sf.briar.api.ContactId;
import net.sf.briar.api.messaging.Message;
import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.messaging.MessageId;
public class PrivateMessageHeader extends MessageHeader { public class PrivateMessageHeader extends MessageHeader {
@@ -16,6 +17,12 @@ public class PrivateMessageHeader extends MessageHeader {
this.incoming = incoming; this.incoming = incoming;
} }
public PrivateMessageHeader(Message m, boolean read, boolean starred,
ContactId contactId, boolean incoming) {
this(m.getId(), m.getParent(), m.getContentType(), m.getSubject(),
m.getTimestamp(), read, starred, contactId, incoming);
}
/** /**
* Returns the ID of the contact who is the sender (if incoming) or * Returns the ID of the contact who is the sender (if incoming) or
* recipient (if outgoing) of this message. * recipient (if outgoing) of this message.

View File

@@ -0,0 +1,23 @@
package net.sf.briar.api.db.event;
import net.sf.briar.api.messaging.Message;
/** An event that is broadcast when a group message is added to the database. */
public class GroupMessageAddedEvent extends DatabaseEvent {
private final Message message;
private final boolean incoming;
public GroupMessageAddedEvent(Message message, boolean incoming) {
this.message = message;
this.incoming = incoming;
}
public Message getMessage() {
return message;
}
public boolean isIncoming() {
return incoming;
}
}

View File

@@ -1,9 +0,0 @@
package net.sf.briar.api.db.event;
/**
* An event that is broadcast when one or more messages are added to the
* database.
*/
public class MessageAddedEvent extends DatabaseEvent {
}

View File

@@ -1,6 +1,17 @@
package net.sf.briar.api.db.event; package net.sf.briar.api.db.event;
import net.sf.briar.api.ContactId;
/** An event that is broadcast when a message is received. */ /** An event that is broadcast when a message is received. */
public class MessageReceivedEvent extends DatabaseEvent { public class MessageReceivedEvent extends DatabaseEvent {
private final ContactId contactId;
public MessageReceivedEvent(ContactId contactId) {
this.contactId = contactId;
}
public ContactId getContactId() {
return contactId;
}
} }

View File

@@ -0,0 +1,33 @@
package net.sf.briar.api.db.event;
import net.sf.briar.api.ContactId;
import net.sf.briar.api.messaging.Message;
/**
* An event that is broadcast when a private message is added to the database.
*/
public class PrivateMessageAddedEvent extends DatabaseEvent {
private final Message message;
private final ContactId contactId;
private final boolean incoming;
public PrivateMessageAddedEvent(Message message, ContactId contactId,
boolean incoming) {
this.message = message;
this.contactId = contactId;
this.incoming = incoming;
}
public Message getMessage() {
return message;
}
public ContactId getContactId() {
return contactId;
}
public boolean isIncoming() {
return incoming;
}
}

View File

@@ -1,17 +1,17 @@
package net.sf.briar.api.db.event; package net.sf.briar.api.db.event;
import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.Group;
/** An event that is broadcast when the user subscribes to a group. */ /** An event that is broadcast when the user subscribes to a group. */
public class SubscriptionAddedEvent extends DatabaseEvent { public class SubscriptionAddedEvent extends DatabaseEvent {
private final GroupId groupId; private final Group group;
public SubscriptionAddedEvent(GroupId groupId) { public SubscriptionAddedEvent(Group group) {
this.groupId = groupId; this.group = group;
} }
public GroupId getGroupId() { public Group getGroup() {
return groupId; return group;
} }
} }

View File

@@ -202,6 +202,13 @@ interface Database<T> {
*/ */
TransportConfig getConfig(T txn, TransportId t) throws DbException; TransportConfig getConfig(T txn, TransportId t) throws DbException;
/**
* Returns the contact with the given ID.
* <p>
* Locking: contact read, window read.
*/
Contact getContact(T txn, ContactId c) throws DbException;
/** /**
* Returns the IDs of all contacts. * Returns the IDs of all contacts.
* <p> * <p>
@@ -230,6 +237,11 @@ interface Database<T> {
*/ */
long getFreeSpace() throws DbException; long getFreeSpace() throws DbException;
/**
* Returns the group with the given ID, if the user subscribes to it.
*/
Group getGroup(T txn, GroupId g) throws DbException;
/** /**
* Returns the parent of the given group message, or null if either the * Returns the parent of the given group message, or null if either the
* message has no parent, or the parent is absent from the database, or the * message has no parent, or the parent is absent from the database, or the

View File

@@ -41,11 +41,12 @@ import net.sf.briar.api.db.event.ContactAddedEvent;
import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseEvent; 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.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent; import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
import net.sf.briar.api.db.event.MessageAddedEvent;
import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.MessageExpiredEvent;
import net.sf.briar.api.db.event.MessageReceivedEvent; import net.sf.briar.api.db.event.MessageReceivedEvent;
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.RatingChangedEvent;
import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent;
import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
@@ -286,7 +287,7 @@ DatabaseCleaner.Callback {
} finally { } finally {
contactLock.readLock().unlock(); contactLock.readLock().unlock();
} }
if(added) callListeners(new MessageAddedEvent()); if(added) callListeners(new GroupMessageAddedEvent(m, false));
} }
/** /**
@@ -399,7 +400,7 @@ DatabaseCleaner.Callback {
} finally { } finally {
contactLock.readLock().unlock(); contactLock.readLock().unlock();
} }
if(added) callListeners(new MessageAddedEvent()); if(added) callListeners(new PrivateMessageAddedEvent(m, c, false));
} }
public void addSecrets(Collection<TemporarySecret> secrets) public void addSecrets(Collection<TemporarySecret> secrets)
@@ -844,6 +845,30 @@ DatabaseCleaner.Callback {
} }
} }
public Contact getContact(ContactId c) throws DbException {
contactLock.readLock().lock();
try {
windowLock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if(!db.containsContact(txn, c))
throw new NoSuchContactException();
Contact contact = db.getContact(txn, c);
db.commitTransaction(txn);
return contact;
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
windowLock.readLock().unlock();
}
} finally {
contactLock.readLock().unlock();
}
}
public Collection<Contact> getContacts() throws DbException { public Collection<Contact> getContacts() throws DbException {
contactLock.readLock().lock(); contactLock.readLock().lock();
try { try {
@@ -866,6 +891,25 @@ DatabaseCleaner.Callback {
} }
} }
public Group getGroup(GroupId g) throws DbException {
subscriptionLock.readLock().lock();
try {
T txn = db.startTransaction();
try {
if(!db.containsSubscription(txn, g))
throw new NoSuchSubscriptionException();
Group group = db.getGroup(txn, g);
db.commitTransaction(txn);
return group;
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
subscriptionLock.readLock().unlock();
}
}
public TransportProperties getLocalProperties(TransportId t) public TransportProperties getLocalProperties(TransportId t)
throws DbException { throws DbException {
transportLock.readLock().lock(); transportLock.readLock().lock();
@@ -1301,8 +1345,12 @@ DatabaseCleaner.Callback {
} finally { } finally {
contactLock.readLock().unlock(); contactLock.readLock().unlock();
} }
callListeners(new MessageReceivedEvent()); callListeners(new MessageReceivedEvent(c));
if(added) callListeners(new MessageAddedEvent()); if(added) {
if(m.getGroup() == null)
callListeners(new PrivateMessageAddedEvent(m, c, true));
else callListeners(new GroupMessageAddedEvent(m, true));
}
} }
/** /**
@@ -1795,7 +1843,7 @@ DatabaseCleaner.Callback {
} finally { } finally {
subscriptionLock.writeLock().unlock(); subscriptionLock.writeLock().unlock();
} }
if(added) callListeners(new SubscriptionAddedEvent(g.getId())); if(added) callListeners(new SubscriptionAddedEvent(g));
return added; return added;
} }

View File

@@ -1047,6 +1047,31 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public Contact getContact(Connection txn, ContactId c) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT name, lastConnected"
+ " FROM contacts AS c"
+ " JOIN connectionTimes AS ct"
+ " ON c.contactId = ct.contactId"
+ " WHERE c.contactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
rs = ps.executeQuery();
if(!rs.next()) throw new DbStateException();
String name = rs.getString(1);
long lastConnected = rs.getLong(2);
rs.close();
ps.close();
return new Contact(c, name, lastConnected);
} catch(SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Collection<ContactId> getContactIds(Connection txn) public Collection<ContactId> getContactIds(Connection txn)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -1124,6 +1149,27 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
public Group getGroup(Connection txn, GroupId g) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT name, key FROM groups WHERE groupId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes());
rs = ps.executeQuery();
if(!rs.next()) throw new DbStateException();
String name = rs.getString(1);
byte[] publicKey = rs.getBytes(2);
rs.close();
ps.close();
return new Group(g, name, publicKey);
} catch(SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public MessageId getGroupMessageParent(Connection txn, MessageId m) public MessageId getGroupMessageParent(Connection txn, MessageId m)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -1228,10 +1274,12 @@ abstract class JdbcDatabase implements Database<Connection> {
PreparedStatement ps = null; PreparedStatement ps = null;
ResultSet rs = null; ResultSet rs = null;
try { try {
String sql = "SELECT messageId, parentId, authorId, authorName," String sql = "SELECT messageId, parentId, m.authorId, authorName,"
+ " authorKey, contentType, subject, timestamp, read," + " authorKey, contentType, subject, timestamp, read,"
+ " starred" + " starred, rating"
+ " FROM messages" + " FROM messages AS m"
+ " LEFT OUTER JOIN ratings AS r"
+ " ON m.authorId = r.authorId"
+ " WHERE groupId = ?"; + " WHERE groupId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setBytes(1, g.getBytes()); ps.setBytes(1, g.getBytes());
@@ -1242,13 +1290,18 @@ abstract class JdbcDatabase implements Database<Connection> {
MessageId id = new MessageId(rs.getBytes(1)); MessageId id = new MessageId(rs.getBytes(1));
byte[] b = rs.getBytes(2); byte[] b = rs.getBytes(2);
MessageId parent = b == null ? null : new MessageId(b); MessageId parent = b == null ? null : new MessageId(b);
Author author = null; Author author;
Rating rating;
b = rs.getBytes(3); b = rs.getBytes(3);
if(b != null) { if(b == null) {
author = null;
rating = UNRATED;
} else {
AuthorId authorId = new AuthorId(b); AuthorId authorId = new AuthorId(b);
String authorName = rs.getString(4); String authorName = rs.getString(4);
byte[] authorKey = rs.getBytes(5); byte[] authorKey = rs.getBytes(5);
author = new Author(authorId, authorName, authorKey); author = new Author(authorId, authorName, authorKey);
rating = Rating.values()[rs.getByte(11)];
} }
String contentType = rs.getString(6); String contentType = rs.getString(6);
String subject = rs.getString(7); String subject = rs.getString(7);
@@ -1256,7 +1309,7 @@ abstract class JdbcDatabase implements Database<Connection> {
boolean read = rs.getBoolean(9); boolean read = rs.getBoolean(9);
boolean starred = rs.getBoolean(10); boolean starred = rs.getBoolean(10);
headers.add(new GroupMessageHeader(id, parent, contentType, headers.add(new GroupMessageHeader(id, parent, contentType,
subject, timestamp, read, starred, g, author)); subject, timestamp, read, starred, g, author, rating));
} }
rs.close(); rs.close();
ps.close(); ps.close();

View File

@@ -30,11 +30,12 @@ import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseEvent; 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.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent; import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
import net.sf.briar.api.db.event.MessageAddedEvent;
import net.sf.briar.api.db.event.MessageExpiredEvent; import net.sf.briar.api.db.event.MessageExpiredEvent;
import net.sf.briar.api.db.event.MessageReceivedEvent; import net.sf.briar.api.db.event.MessageReceivedEvent;
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.RatingChangedEvent;
import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteRetentionTimeUpdatedEvent;
import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent;
@@ -134,6 +135,9 @@ abstract class DuplexConnection implements DatabaseListener {
if(e instanceof ContactRemovedEvent) { if(e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if(contactId.equals(c.getContactId())) dispose(false, true); if(contactId.equals(c.getContactId())) dispose(false, true);
} else if(e instanceof GroupMessageAddedEvent) {
if(canSendOffer.getAndSet(false))
dbExecutor.execute(new GenerateOffer());
} else if(e instanceof MessageExpiredEvent) { } else if(e instanceof MessageExpiredEvent) {
dbExecutor.execute(new GenerateRetentionUpdate()); dbExecutor.execute(new GenerateRetentionUpdate());
} else if(e instanceof LocalSubscriptionsUpdatedEvent) { } else if(e instanceof LocalSubscriptionsUpdatedEvent) {
@@ -143,11 +147,15 @@ abstract class DuplexConnection implements DatabaseListener {
dbExecutor.execute(new GenerateSubscriptionUpdate()); dbExecutor.execute(new GenerateSubscriptionUpdate());
} else if(e instanceof LocalTransportsUpdatedEvent) { } else if(e instanceof LocalTransportsUpdatedEvent) {
dbExecutor.execute(new GenerateTransportUpdates()); dbExecutor.execute(new GenerateTransportUpdates());
} else if(e instanceof MessageAddedEvent) {
if(canSendOffer.getAndSet(false))
dbExecutor.execute(new GenerateOffer());
} else if(e instanceof MessageReceivedEvent) { } else if(e instanceof MessageReceivedEvent) {
dbExecutor.execute(new GenerateAcks()); if(((MessageReceivedEvent) e).getContactId().equals(contactId))
dbExecutor.execute(new GenerateAcks());
} else if(e instanceof PrivateMessageAddedEvent) {
PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e;
if(!p.isIncoming() && p.getContactId().equals(contactId)) {
if(canSendOffer.getAndSet(false))
dbExecutor.execute(new GenerateOffer());
}
} else if(e instanceof RatingChangedEvent) { } else if(e instanceof RatingChangedEvent) {
RatingChangedEvent r = (RatingChangedEvent) e; RatingChangedEvent r = (RatingChangedEvent) e;
if(r.getRating() == GOOD && canSendOffer.getAndSet(false)) if(r.getRating() == GOOD && canSendOffer.getAndSet(false))

View File

@@ -20,12 +20,14 @@ import net.sf.briar.api.TransportConfig;
import net.sf.briar.api.TransportProperties; import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.NoSuchContactException; import net.sf.briar.api.db.NoSuchContactException;
import net.sf.briar.api.db.NoSuchSubscriptionException;
import net.sf.briar.api.db.NoSuchTransportException; import net.sf.briar.api.db.NoSuchTransportException;
import net.sf.briar.api.db.event.ContactAddedEvent; import net.sf.briar.api.db.event.ContactAddedEvent;
import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseListener; import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.db.event.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent; import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.RatingChangedEvent;
import net.sf.briar.api.db.event.SubscriptionAddedEvent; import net.sf.briar.api.db.event.SubscriptionAddedEvent;
import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
@@ -503,11 +505,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
final ShutdownManager shutdown = context.mock(ShutdownManager.class); final ShutdownManager shutdown = context.mock(ShutdownManager.class);
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the contact is in the DB (which it's not) // Check whether the contact is in the DB (which it's not)
exactly(27).of(database).startTransaction(); exactly(28).of(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
exactly(27).of(database).containsContact(txn, contactId); exactly(28).of(database).containsContact(txn, contactId);
will(returnValue(false)); will(returnValue(false));
exactly(27).of(database).abortTransaction(txn); exactly(28).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, cleaner, DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown); shutdown);
@@ -572,6 +574,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
fail(); fail();
} catch(NoSuchContactException expected) {} } catch(NoSuchContactException expected) {}
try {
db.getContact(contactId);
fail();
} catch(NoSuchContactException expected) {}
try { try {
db.getVisibleSubscriptions(contactId); db.getVisibleSubscriptions(contactId);
fail(); fail();
@@ -660,6 +667,53 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
@Test
public void testVariousMethodsThrowExceptionIfSubscriptionIsMissing()
throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
final Database<Object> database = context.mock(Database.class);
final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
final ShutdownManager shutdown = context.mock(ShutdownManager.class);
context.checking(new Expectations() {{
// Check whether the subscription is in the DB (which it's not)
exactly(5).of(database).startTransaction();
will(returnValue(txn));
exactly(5).of(database).containsTransport(txn, transportId);
will(returnValue(false));
exactly(5).of(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
try {
db.getGroup(groupId);
fail();
} catch(NoSuchSubscriptionException expected) {}
try {
db.getMessageHeaders(groupId);
fail();
} catch(NoSuchSubscriptionException expected) {}
try {
db.getVisibility(groupId);
fail();
} catch(NoSuchSubscriptionException expected) {}
try {
db.setVisibility(groupId, Collections.<ContactId>emptyList());
fail();
} catch(NoSuchSubscriptionException expected) {}
try {
db.unsubscribe(groupId);
fail();
} catch(NoSuchSubscriptionException expected) {}
context.assertIsSatisfied();
}
@Test @Test
public void testVariousMethodsThrowExceptionIfTransportIsMissing() public void testVariousMethodsThrowExceptionIfTransportIsMissing()
throws Exception { throws Exception {
@@ -1454,7 +1508,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
oneOf(database).setSendability(txn, messageId, 0); oneOf(database).setSendability(txn, messageId, 0);
oneOf(database).commitTransaction(txn); oneOf(database).commitTransaction(txn);
// The message was added, so the listener should be called // The message was added, so the listener should be called
oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class))); oneOf(listener).eventOccurred(with(any(
GroupMessageAddedEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, cleaner, DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown); shutdown);
@@ -1485,7 +1540,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).setReadFlag(txn, messageId, true);
oneOf(database).addStatus(txn, contactId, messageId, false); oneOf(database).addStatus(txn, contactId, messageId, false);
// The message was added, so the listener should be called // The message was added, so the listener should be called
oneOf(listener).eventOccurred(with(any(MessageAddedEvent.class))); oneOf(listener).eventOccurred(with(any(
PrivateMessageAddedEvent.class)));
}}); }});
DatabaseComponent db = createDatabaseComponent(database, cleaner, DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown); shutdown);

View File

@@ -15,7 +15,7 @@ import net.sf.briar.api.crypto.KeyManager;
import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.event.DatabaseEvent; 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.MessageAddedEvent; import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.Message;
import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageFactory;
import net.sf.briar.api.messaging.MessageVerifier; import net.sf.briar.api.messaging.MessageVerifier;
@@ -186,14 +186,14 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
messageVerifier, db, connRegistry, connWriterFactory, messageVerifier, db, connRegistry, connWriterFactory,
packetWriterFactory, ctx, transport); packetWriterFactory, ctx, transport);
// No messages should have been added yet // No messages should have been added yet
assertFalse(listener.messagesAdded); assertFalse(listener.messageAdded);
// Read whatever needs to be read // Read whatever needs to be read
simplex.read(); simplex.read();
assertTrue(transport.getDisposed()); assertTrue(transport.getDisposed());
assertFalse(transport.getException()); assertFalse(transport.getException());
assertTrue(transport.getRecognised()); assertTrue(transport.getRecognised());
// The private message from Alice should have been added // The private message from Alice should have been added
assertTrue(listener.messagesAdded); assertTrue(listener.messageAdded);
// Clean up // Clean up
km.stop(); km.stop();
db.close(); db.close();
@@ -206,10 +206,10 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
private static class MessageListener implements DatabaseListener { private static class MessageListener implements DatabaseListener {
private boolean messagesAdded = false; private boolean messageAdded = false;
public void eventOccurred(DatabaseEvent e) { public void eventOccurred(DatabaseEvent e) {
if(e instanceof MessageAddedEvent) messagesAdded = true; if(e instanceof PrivateMessageAddedEvent) messageAdded = true;
} }
} }
} }