diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index e402626affe6154e434dfd3d5c4bd8ce01cefbef..fc8fcd5289bfedca1eb5bb9377338417d8ce7865 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -69,11 +69,11 @@ android:label="@string/messages_title" > </activity> <activity - android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity" + android:name=".android.messages.ReadPrivateMessageActivity" android:label="@string/messages_title" > </activity> <activity - android:name="net.sf.briar.android.messages.WritePrivateMessageActivity" + android:name=".android.messages.WritePrivateMessageActivity" android:label="@string/compose_message_title" > </activity> </application> diff --git a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java deleted file mode 100644 index 8e9207c6fc1731c4f0ef1e288d2c358885c1b13f..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java +++ /dev/null @@ -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()); - } -} \ No newline at end of file diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java index fe5f673e9c463e3fdfe458824a53f105fbae4e93..5b01e6bb4fa69ae8ede754686b2d10c8d4ec82b7 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java @@ -6,6 +6,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import java.util.Collection; +import java.util.Comparator; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -46,12 +47,13 @@ implements OnClickListener, DatabaseListener, ConnectionListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; @Inject private ConnectionRegistry connectionRegistry; - private ContactListAdapter adapter = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -87,8 +89,11 @@ implements OnClickListener, DatabaseListener, ConnectionListener { serviceConnection, 0); // Add some fake contacts to the database in a background thread - // FIXME: Remove this - final DatabaseComponent db = this.db; + insertFakeContacts(); + } + + // FIXME: Remove this + private void insertFakeContacts() { dbExecutor.execute(new Runnable() { public void run() { try { @@ -117,28 +122,10 @@ implements OnClickListener, DatabaseListener, ConnectionListener { @Override public void onResume() { super.onResume(); - reloadContactList(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - db.removeListener(this); - connectionRegistry.removeListener(this); - unbindService(serviceConnection); - } - - public void onClick(View view) { - startActivity(new Intent(this, AddContactActivity.class)); - } - - public void eventOccurred(DatabaseEvent e) { - if(e instanceof ContactAddedEvent) reloadContactList(); - else if(e instanceof ContactRemovedEvent) reloadContactList(); + loadContacts(); } - private void reloadContactList() { - final DatabaseComponent db = this.db; + private void loadContacts() { dbExecutor.execute(new Runnable() { public void run() { try { @@ -148,8 +135,8 @@ implements OnClickListener, DatabaseListener, ConnectionListener { Collection<Contact> contacts = db.getContacts(); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + contacts.size() + " contacts"); - // Update the contact list - updateContactList(contacts); + // Display the contacts in the UI + displayContacts(contacts); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -162,7 +149,7 @@ implements OnClickListener, DatabaseListener, ConnectionListener { }); } - private void updateContactList(final Collection<Contact> contacts) { + private void displayContacts(final Collection<Contact> contacts) { runOnUiThread(new Runnable() { public void run() { adapter.clear(); @@ -175,6 +162,24 @@ implements OnClickListener, DatabaseListener, ConnectionListener { }); } + @Override + public void onDestroy() { + super.onDestroy(); + db.removeListener(this); + connectionRegistry.removeListener(this); + unbindService(serviceConnection); + } + + public void onClick(View view) { + startActivity(new Intent(this, AddContactActivity.class)); + } + + public void eventOccurred(DatabaseEvent e) { + // These events should be rare, so just reload the list + if(e instanceof ContactAddedEvent) loadContacts(); + else if(e instanceof ContactRemovedEvent) loadContacts(); + } + public void contactConnected(ContactId c) { setConnected(c, true); } @@ -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()); + } + } } diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java index 148e73402b2de8c645c9531ee32e981ef9f88bfa..5873b07ce789a1eb9cde1e2fb1f7a3b8c7f75008 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java @@ -6,12 +6,9 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static net.sf.briar.api.Rating.UNRATED; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -30,13 +27,13 @@ import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.MessageAddedEvent; +import net.sf.briar.api.db.event.GroupMessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; -import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.messaging.Author; -import net.sf.briar.api.messaging.AuthorId; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.Message; +import net.sf.briar.api.messaging.MessageId; import android.content.Intent; import android.os.Bundle; import android.view.View; @@ -58,14 +55,17 @@ OnClickListener, OnItemClickListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - - private GroupId groupId = null; + // The following fields must only be accessed from the UI thread + private final Set<MessageId> messageIds = new HashSet<MessageId>(); private String groupName = null; private GroupAdapter adapter = null; private ListView list = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + private volatile GroupId groupId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -111,32 +111,22 @@ OnClickListener, OnItemClickListener { @Override public void onResume() { super.onResume(); - reloadMessageHeaders(); + loadHeaders(); } - private void reloadMessageHeaders() { - final DatabaseComponent db = this.db; - final GroupId groupId = this.groupId; + private void loadHeaders() { dbExecutor.execute(new Runnable() { public void run() { try { // Wait for the service to be bound and started serviceConnection.waitForStartup(); - // Load the message headers from the database + // Load the headers from the database Collection<GroupMessageHeader> headers = db.getMessageHeaders(groupId); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + headers.size() + " headers"); - // Load the ratings for the authors - Map<Author, Rating> ratings = new HashMap<Author, Rating>(); - for(GroupMessageHeader h : headers) { - Author a = h.getAuthor(); - if(a != null && !ratings.containsKey(a)) - ratings.put(a, db.getRating(a.getId())); - } - ratings = Collections.unmodifiableMap(ratings); - // Update the conversation - updateConversation(headers, ratings); + // Display the headers in the UI + displayHeaders(headers); } catch(NoSuchSubscriptionException e) { if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); finishOnUiThread(); @@ -152,29 +142,46 @@ OnClickListener, OnItemClickListener { }); } - private void updateConversation( - final Collection<GroupMessageHeader> headers, - final Map<Author, Rating> ratings) { + private void displayHeaders(final Collection<GroupMessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { - List<GroupMessageHeader> sort = - new ArrayList<GroupMessageHeader>(headers); - Collections.sort(sort, AscendingHeaderComparator.INSTANCE); - int firstUnread = -1; + messageIds.clear(); adapter.clear(); - for(GroupMessageHeader h : sort) { - if(firstUnread == -1 && !h.isRead()) - firstUnread = adapter.getCount(); - Author a = h.getAuthor(); - if(a == null) adapter.add(new GroupItem(h, UNRATED)); - else adapter.add(new GroupItem(h, ratings.get(a))); + for(GroupMessageHeader h : headers) { + messageIds.add(h.getId()); + adapter.add(h); } - if(firstUnread == -1) list.setSelection(adapter.getCount() - 1); - else list.setSelection(firstUnread); + adapter.sort(AscendingHeaderComparator.INSTANCE); + selectFirstUnread(); } }); } + private void selectFirstUnread() { + int firstUnread = -1, count = adapter.getCount(); + for(int i = 0; i < count; i++) { + if(!adapter.getItem(i).isRead()) { + firstUnread = i; + break; + } + } + if(firstUnread == -1) list.setSelection(count - 1); + else list.setSelection(firstUnread); + } + + @Override + public void onActivityResult(int request, int result, Intent data) { + if(result == ReadGroupMessageActivity.RESULT_PREV) { + int position = request - 1; + if(position >= 0 && position < adapter.getCount()) + showMessage(position); + } else if(result == ReadGroupMessageActivity.RESULT_NEXT) { + int position = request + 1; + if(position >= 0 && position < adapter.getCount()) + showMessage(position); + } + } + @Override public void onDestroy() { super.onDestroy(); @@ -183,38 +190,59 @@ OnClickListener, OnItemClickListener { } public void eventOccurred(DatabaseEvent e) { - if(e instanceof MessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - reloadMessageHeaders(); + if(e instanceof GroupMessageAddedEvent) { + GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; + Message m = g.getMessage(); + if(m.getGroup().getId().equals(groupId)) + loadRatingOrAddToGroup(m, g.isIncoming()); } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); - reloadMessageHeaders(); - } else if(e instanceof RatingChangedEvent) { - RatingChangedEvent r = (RatingChangedEvent) e; - updateRating(r.getAuthorId(), r.getRating()); + loadHeaders(); // FIXME: Don't reload unnecessarily } else if(e instanceof SubscriptionRemovedEvent) { - SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e; - if(s.getGroupId().equals(groupId)) { + if(((SubscriptionRemovedEvent) e).getGroupId().equals(groupId)) { if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); finishOnUiThread(); } } } - private void updateRating(final AuthorId a, final Rating r) { + private void loadRatingOrAddToGroup(Message m, boolean incoming) { + // FIXME: Cache ratings to avoid hitting the DB + if(m.getAuthor() == null) addToGroup(m, UNRATED, incoming); + else loadRating(m, incoming); + } + + private void addToGroup(final Message m, final Rating r, + final boolean incoming) { runOnUiThread(new Runnable() { public void run() { - boolean affected = false; - int count = adapter.getCount(); - for(int i = 0; i < count; i++) { - GroupItem item = adapter.getItem(i); - Author author = item.getAuthor(); - if(author != null && author.getId().equals(a)) { - item.setRating(r); - affected = true; - } + if(messageIds.add(m.getId())) { + adapter.add(new GroupMessageHeader(m, !incoming, false, r)); + adapter.sort(AscendingHeaderComparator.INSTANCE); + selectFirstUnread(); + } + } + }); + } + + private void loadRating(final Message m, final boolean incoming) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + // Wait for the service to be bound and started + serviceConnection.waitForStartup(); + // Load the rating from the database + Rating r = db.getRating(m.getAuthor().getId()); + // Display the message + addToGroup(m, r, incoming); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); } - if(affected) list.invalidate(); } }); } @@ -222,7 +250,6 @@ OnClickListener, OnItemClickListener { public void onClick(View view) { Intent i = new Intent(this, WriteGroupMessageActivity.class); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); - i.putExtra("net.sf.briar.GROUP_NAME", groupName); startActivity(i); } @@ -232,7 +259,7 @@ OnClickListener, OnItemClickListener { } private void showMessage(int position) { - GroupItem item = adapter.getItem(position); + GroupMessageHeader item = adapter.getItem(position); Intent i = new Intent(this, ReadGroupMessageActivity.class); i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); i.putExtra("net.sf.briar.GROUP_NAME", groupName); @@ -252,17 +279,4 @@ OnClickListener, OnItemClickListener { i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1); startActivityForResult(i, position); } - - @Override - public void onActivityResult(int request, int result, Intent data) { - if(result == ReadGroupMessageActivity.RESULT_PREV) { - int position = request - 1; - if(position >= 0 && position < adapter.getCount()) - showMessage(position); - } else if(result == ReadGroupMessageActivity.RESULT_NEXT) { - int position = request + 1; - if(position >= 0 && position < adapter.getCount()) - showMessage(position); - } - } } diff --git a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java index 463785884c9d264b4f02d25800bec2b3e7b6b1a9..c908c0332a184683e66838663319ebdd63b1ce77 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java @@ -15,6 +15,7 @@ import net.sf.briar.R; import net.sf.briar.android.widgets.CommonLayoutParams; import net.sf.briar.android.widgets.HorizontalSpace; import net.sf.briar.api.Rating; +import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.messaging.Author; import android.content.Context; import android.content.res.Resources; @@ -26,21 +27,20 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -class GroupAdapter extends ArrayAdapter<GroupItem> { +class GroupAdapter extends ArrayAdapter<GroupMessageHeader> { GroupAdapter(Context ctx) { super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList<GroupItem>()); + new ArrayList<GroupMessageHeader>()); } @Override public View getView(int position, View convertView, ViewGroup parent) { - GroupItem item = getItem(position); + GroupMessageHeader item = getItem(position); Context ctx = getContext(); // FIXME: Use a RelativeLayout LinearLayout layout = new LinearLayout(ctx); layout.setOrientation(HORIZONTAL); - // layout.setGravity(CENTER_VERTICAL); if(!item.isRead()) { Resources res = ctx.getResources(); layout.setBackgroundColor(res.getColor(R.color.unread_background)); diff --git a/briar-android/src/net/sf/briar/android/groups/GroupItem.java b/briar-android/src/net/sf/briar/android/groups/GroupItem.java deleted file mode 100644 index 3e6cb182136fd264721795132e4b5178b242a142..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/groups/GroupItem.java +++ /dev/null @@ -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; - } -} diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java index ee38bfd8d8b997cb04d839d6007388daec71178c..48a977e842bd755c47632c92271cba2e94a7b50e 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -36,14 +36,14 @@ import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.NoSuchSubscriptionException; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.MessageAddedEvent; +import net.sf.briar.api.db.event.GroupMessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; -import net.sf.briar.api.db.event.SubscriptionAddedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.AuthorFactory; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupFactory; +import net.sf.briar.api.messaging.GroupId; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageFactory; import android.content.Intent; @@ -65,14 +65,16 @@ implements OnClickListener, DatabaseListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); - @Inject private CryptoComponent crypto; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - @Inject private AuthorFactory authorFactory; - @Inject private GroupFactory groupFactory; - @Inject private MessageFactory messageFactory; - private GroupListAdapter adapter = null; + private ListView list = null; + + // Fields that are accessed from DB threads must be volatile + @Inject private volatile CryptoComponent crypto; + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Inject private volatile AuthorFactory authorFactory; + @Inject private volatile GroupFactory groupFactory; + @Inject private volatile MessageFactory messageFactory; @Override public void onCreate(Bundle state) { @@ -83,7 +85,7 @@ implements OnClickListener, DatabaseListener { layout.setGravity(CENTER_HORIZONTAL); adapter = new GroupListAdapter(this); - ListView list = new ListView(this); + list = new ListView(this); // Give me all the width and all the unused height list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); list.setAdapter(adapter); @@ -112,9 +114,6 @@ implements OnClickListener, DatabaseListener { // FIXME: Remove this private void insertFakeMessages() { - final DatabaseComponent db = this.db; - final GroupFactory groupFactory = this.groupFactory; - final MessageFactory messageFactory = this.messageFactory; dbExecutor.execute(new Runnable() { public void run() { try { @@ -204,18 +203,16 @@ implements OnClickListener, DatabaseListener { @Override public void onResume() { super.onResume(); - reloadGroupList(); + loadGroups(); } - private void reloadGroupList() { - final DatabaseComponent db = this.db; + private void loadGroups() { dbExecutor.execute(new Runnable() { public void run() { try { // Wait for the service to be bound and started serviceConnection.waitForStartup(); - // Load the groups and message headers from the DB - if(LOG.isLoggable(INFO)) LOG.info("Loading groups"); + // Load the subscribed groups from the DB Collection<Group> groups = db.getSubscriptions(); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + groups.size() + " groups"); @@ -223,20 +220,20 @@ implements OnClickListener, DatabaseListener { for(Group g : groups) { // Filter out restricted groups if(g.getPublicKey() != null) continue; + // Load the message headers Collection<GroupMessageHeader> headers; try { headers = db.getMessageHeaders(g.getId()); } catch(NoSuchSubscriptionException e) { - // We'll reload the list when we get the event - continue; + continue; // Unsubscribed since getSubscriptions() } if(LOG.isLoggable(INFO)) LOG.info("Loaded " + headers.size() + " headers"); if(!headers.isEmpty()) items.add(createItem(g, headers)); } - // Update the group list - updateGroupList(Collections.unmodifiableList(items)); + // Display the groups in the UI + displayGroups(Collections.unmodifiableList(items)); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -257,16 +254,29 @@ implements OnClickListener, DatabaseListener { return new GroupListItem(group, sort); } - private void updateGroupList(final Collection<GroupListItem> items) { + private void displayGroups(final Collection<GroupListItem> items) { runOnUiThread(new Runnable() { public void run() { adapter.clear(); for(GroupListItem i : items) adapter.add(i); adapter.sort(GroupComparator.INSTANCE); + selectFirstUnread(); } }); } + private void selectFirstUnread() { + int firstUnread = -1, count = adapter.getCount(); + for(int i = 0; i < count; i++) { + if(adapter.getItem(i).getUnreadCount() > 0) { + firstUnread = i; + break; + } + } + if(firstUnread == -1) list.setSelection(count - 1); + else list.setSelection(firstUnread); + } + @Override public void onDestroy() { super.onDestroy(); @@ -279,19 +289,94 @@ implements OnClickListener, DatabaseListener { } public void eventOccurred(DatabaseEvent e) { - if(e instanceof MessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - reloadGroupList(); + if(e instanceof GroupMessageAddedEvent) { + GroupMessageAddedEvent g = (GroupMessageAddedEvent) e; + addToGroup(g.getMessage(), g.isIncoming()); } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); - reloadGroupList(); - } else if(e instanceof SubscriptionAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); - reloadGroupList(); + loadGroups(); // FIXME: Don't reload unnecessarily } else if(e instanceof SubscriptionRemovedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); - reloadGroupList(); + removeGroup(((SubscriptionRemovedEvent) e).getGroupId()); + } + } + + private void addToGroup(final Message m, final boolean incoming) { + runOnUiThread(new Runnable() { + public void run() { + GroupId g = m.getGroup().getId(); + GroupListItem item = findGroup(g); + if(item == null) { + loadGroup(g, m, incoming); + } else if(item.add(m, incoming)) { + adapter.sort(GroupComparator.INSTANCE); + selectFirstUnread(); + list.invalidate(); + } + } + }); + } + + private GroupListItem findGroup(GroupId g) { + int count = adapter.getCount(); + for(int i = 0; i < count; i++) { + GroupListItem item = adapter.getItem(i); + if(item.getGroupId().equals(g)) return item; } + return null; // Not found + } + + private void loadGroup(final GroupId g, final Message m, + final boolean incoming) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + // Wait for the service to be bound and started + serviceConnection.waitForStartup(); + // Load the group from the DB and display it in the UI + displayGroup(db.getGroup(g), m, incoming); + } catch(NoSuchSubscriptionException e) { + if(LOG.isLoggable(INFO)) LOG.info("Subscription removed"); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + } + }); + } + + private void displayGroup(final Group g, final Message m, + final boolean incoming) { + runOnUiThread(new Runnable() { + public void run() { + // The item may have been added since loadGroup() was called + GroupListItem item = findGroup(g.getId()); + if(item == null) { + adapter.add(new GroupListItem(g, m, incoming)); + adapter.sort(GroupComparator.INSTANCE); + selectFirstUnread(); + } else if(item.add(m, incoming)) { + adapter.sort(GroupComparator.INSTANCE); + selectFirstUnread(); + list.invalidate(); + } + } + }); + } + + private void removeGroup(final GroupId g) { + runOnUiThread(new Runnable() { + public void run() { + GroupListItem item = findGroup(g); + if(item != null) { + adapter.remove(item); + selectFirstUnread(); + } + } + }); } private static class GroupComparator implements Comparator<GroupListItem> { diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java index 38a4513c21cb24679762f1373a093e72e00feab5..918e272d4851e8f7c8a845c974944c0a30734513 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java @@ -1,20 +1,26 @@ package net.sf.briar.android.groups; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.Message; +import net.sf.briar.api.messaging.MessageId; +// This class is not thread-safe class GroupListItem { + private final Set<MessageId> messageIds = new HashSet<MessageId>(); private final Group group; - private final String author, subject; - private final long timestamp; - private final int unread; + private String authorName, subject; + private long timestamp; + private int unread; GroupListItem(Group group, List<GroupMessageHeader> headers) { if(headers.isEmpty()) throw new IllegalArgumentException(); @@ -22,13 +28,40 @@ class GroupListItem { Collections.sort(headers, DescendingHeaderComparator.INSTANCE); GroupMessageHeader newest = headers.get(0); Author a = newest.getAuthor(); - if(a == null) author = null; - else author = a.getName(); + if(a == null) authorName = null; + else authorName = a.getName(); subject = newest.getSubject(); timestamp = newest.getTimestamp(); - int unread = 0; - for(GroupMessageHeader h : headers) if(!h.isRead()) unread++; - this.unread = unread; + unread = 0; + for(GroupMessageHeader h : headers) { + if(!h.isRead()) unread++; + if(!messageIds.add(h.getId())) throw new IllegalArgumentException(); + } + } + + GroupListItem(Group group, Message first, boolean incoming) { + this.group = group; + Author a = first.getAuthor(); + if(a == null) authorName = null; + else authorName = a.getName(); + subject = first.getSubject(); + timestamp = first.getTimestamp(); + unread = incoming ? 1 : 0; + messageIds.add(first.getId()); + } + + boolean add(Message m, boolean incoming) { + if(!messageIds.add(m.getId())) return false; + if(m.getTimestamp() > timestamp) { + // The added message is the newest + Author a = m.getAuthor(); + if(a == null) authorName = null; + else authorName = a.getName(); + subject = m.getSubject(); + timestamp = m.getTimestamp(); + } + if(incoming) unread++; + return true; } GroupId getGroupId() { @@ -40,7 +73,7 @@ class GroupListItem { } String getAuthorName() { - return author; + return authorName; } String getSubject() { diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java index 9a43b56d4deaf9f72a0d56fc34509496acc25d9f..845c7b9b1e7e3fb80922d8740331f00d93f527e5 100644 --- a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java @@ -61,13 +61,7 @@ implements OnClickListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - private GroupId groupId = null; - private MessageId messageId = null; - private AuthorId authorId = null; - private String authorName = null; private Rating rating = UNRATED; private boolean read; private ImageView thumb = null; @@ -76,6 +70,12 @@ implements OnClickListener { private ImageButton replyButton = null; private TextView content = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + private volatile MessageId messageId = null; + private volatile AuthorId authorId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -91,6 +91,7 @@ implements OnClickListener { if(id == null) throw new IllegalStateException(); messageId = new MessageId(id); boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false); + String authorName = null; if(!anonymous) { id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID"); if(id == null) throw new IllegalStateException(); @@ -235,8 +236,6 @@ implements OnClickListener { } private void setReadInDatabase(final boolean read) { - final DatabaseComponent db = this.db; - final MessageId messageId = this.messageId; dbExecutor.execute(new Runnable() { public void run() { try { @@ -266,8 +265,6 @@ implements OnClickListener { } private void loadMessageBody() { - final DatabaseComponent db = this.db; - final MessageId messageId = this.messageId; dbExecutor.execute(new Runnable() { public void run() { try { @@ -334,8 +331,6 @@ implements OnClickListener { } private void setRatingInDatabase(final Rating r) { - final DatabaseComponent db = this.db; - final AuthorId authorId = this.authorId; dbExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java index 7a1a93c06f3ac3cb5ad32e72d014c289a2877988..6dd8792ea63efbaa88f8f77c5eb5b27271f45d05 100644 --- a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java @@ -53,18 +53,19 @@ implements OnClickListener, OnItemSelectedListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - @Inject private MessageFactory messageFactory; - - private Group group = null; - private GroupId groupId = null; - private MessageId parentId = null; private GroupNameSpinnerAdapter adapter = null; private Spinner spinner = null; private ImageButton sendButton = null; private EditText content = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Inject private volatile MessageFactory messageFactory; + private volatile Group group = null; + private volatile GroupId groupId = null; + private volatile MessageId parentId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -94,7 +95,7 @@ implements OnClickListener, OnItemSelectedListener { spinner = new Spinner(this); spinner.setAdapter(adapter); spinner.setOnItemSelectedListener(this); - loadContactNames(); + loadGroupList(); actionBar.addView(spinner); actionBar.addView(new HorizontalSpace(this)); @@ -122,24 +123,12 @@ implements OnClickListener, OnItemSelectedListener { serviceConnection, 0); } - private void loadContactNames() { - final DatabaseComponent db = this.db; + private void loadGroupList() { dbExecutor.execute(new Runnable() { public void run() { try { serviceConnection.waitForStartup(); - final Collection<Group> groups = db.getSubscriptions(); - runOnUiThread(new Runnable() { - public void run() { - for(Group g : groups) { - if(g.getId().equals(groupId)) { - group = g; - spinner.setSelection(adapter.getCount()); - } - adapter.add(g); - } - } - }); + updateGroupList(db.getSubscriptions()); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -151,6 +140,20 @@ implements OnClickListener, OnItemSelectedListener { }); } + private void updateGroupList(final Collection<Group> groups) { + runOnUiThread(new Runnable() { + public void run() { + for(Group g : groups) { + if(g.getId().equals(groupId)) { + group = g; + spinner.setSelection(adapter.getCount()); + } + adapter.add(g); + } + } + }); + } + @Override public void onSaveInstanceState(Bundle state) { Parcelable p = content.onSaveInstanceState(); @@ -175,10 +178,6 @@ implements OnClickListener, OnItemSelectedListener { } private void storeMessage(final byte[] body) { - final DatabaseComponent db = this.db; - final MessageFactory messageFactory = this.messageFactory; - final Group group = this.group; - final MessageId parentId = this.parentId; dbExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java index 4c246c491396bf6f4094c9306c80278db9f5aeb4..6bc03686f8711cc1432e094ac1da96c21c87a8dd 100644 --- a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java +++ b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java @@ -35,12 +35,8 @@ implements InvitationListener { @Inject private BundleEncrypter bundleEncrypter; @Inject private CryptoComponent crypto; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; @Inject private InvitationTaskFactory invitationTaskFactory; @Inject private ReferenceManager referenceManager; - - // All of the following must be accessed on the UI thread private AddContactView view = null; private InvitationTask task = null; private long taskHandle = -1; @@ -52,6 +48,10 @@ implements InvitationListener { private boolean localCompared = false, remoteCompared = false; private boolean localMatched = false, remoteMatched = false; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -217,7 +217,6 @@ implements InvitationListener { } void addContactAndFinish(final String nickname) { - final DatabaseComponent db = this.db; dbExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java index aefd64d0da5fd689eba1009d2c03fb752b05a1a3..ef7252eeda94f8d810a8ee1dc48f887713408fa8 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java @@ -5,10 +5,9 @@ import static android.widget.LinearLayout.VERTICAL; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -28,8 +27,10 @@ import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; +import net.sf.briar.api.db.event.PrivateMessageAddedEvent; +import net.sf.briar.api.messaging.Message; +import net.sf.briar.api.messaging.MessageId; import android.content.Intent; import android.os.Bundle; import android.view.View; @@ -51,14 +52,17 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - - private ContactId contactId = null; + // The following fields must only be accessed from the UI thread + private Set<MessageId> messageIds = new HashSet<MessageId>(); private String contactName = null; private ConversationAdapter adapter = null; private ListView list = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + private volatile ContactId contactId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -104,24 +108,22 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { @Override public void onResume() { super.onResume(); - reloadMessageHeaders(); + loadHeaders(); } - private void reloadMessageHeaders() { - final DatabaseComponent db = this.db; - final ContactId contactId = this.contactId; + private void loadHeaders() { dbExecutor.execute(new Runnable() { public void run() { try { // Wait for the service to be bound and started serviceConnection.waitForStartup(); - // Load the message headers from the database + // Load the headers from the database Collection<PrivateMessageHeader> headers = db.getPrivateMessageHeaders(contactId); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + headers.size() + " headers"); - // Update the conversation - updateConversation(headers); + // Display the headers in the UI + displayHeaders(headers); } catch(NoSuchContactException e) { if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); finishOnUiThread(); @@ -137,26 +139,34 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { }); } - private void updateConversation( + private void displayHeaders( final Collection<PrivateMessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { - List<PrivateMessageHeader> sort = - new ArrayList<PrivateMessageHeader>(headers); - Collections.sort(sort, AscendingHeaderComparator.INSTANCE); - int firstUnread = -1; + messageIds.clear(); adapter.clear(); - for(PrivateMessageHeader h : sort) { - if(firstUnread == -1 && !h.isRead()) - firstUnread = adapter.getCount(); + for(PrivateMessageHeader h : headers) { + messageIds.add(h.getId()); adapter.add(h); } - if(firstUnread == -1) list.setSelection(adapter.getCount() - 1); - else list.setSelection(firstUnread); + adapter.sort(AscendingHeaderComparator.INSTANCE); + selectFirstUnread(); } }); } + private void selectFirstUnread() { + int firstUnread = -1, count = adapter.getCount(); + for(int i = 0; i < count; i++) { + if(!adapter.getItem(i).isRead()) { + firstUnread = i; + break; + } + } + if(firstUnread == -1) list.setSelection(count - 1); + else list.setSelection(firstUnread); + } + @Override public void onActivityResult(int request, int result, Intent data) { if(result == ReadPrivateMessageActivity.RESULT_PREV) { @@ -182,17 +192,31 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { ContactRemovedEvent c = (ContactRemovedEvent) e; if(c.getContactId().equals(contactId)) { if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); - finish(); + finishOnUiThread(); } - } else if(e instanceof MessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - reloadMessageHeaders(); } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); - reloadMessageHeaders(); + loadHeaders(); // FIXME: Don't reload unnecessarily + } else if(e instanceof PrivateMessageAddedEvent) { + PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; + if(p.getContactId().equals(contactId)) + addToConversation(p.getMessage(), p.isIncoming()); } } + private void addToConversation(final Message m, final boolean incoming) { + runOnUiThread(new Runnable() { + public void run() { + if(messageIds.add(m.getId())) { + adapter.add(new PrivateMessageHeader(m, !incoming, false, + contactId, incoming)); + adapter.sort(AscendingHeaderComparator.INSTANCE); + selectFirstUnread(); + } + } + }); + } + public void onClick(View view) { Intent i = new Intent(this, WritePrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java b/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java deleted file mode 100644 index fcf0b179991c325eb1ed87942245f9d9a0d57824..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/messages/ConversationComparator.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java index 5eafcf3367303550aabe2763b095eb8e5155d700..4734728148051e43fb0193c8ff5e4cb3d3308159 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,11 +27,13 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DatabaseExecutor; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.NoSuchContactException; import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.db.event.ContactRemovedEvent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; +import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageFactory; import android.content.Intent; @@ -52,11 +55,13 @@ implements OnClickListener, DatabaseListener { private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - @Inject private MessageFactory messageFactory; - private ConversationListAdapter adapter = null; + private ListView list = null; + + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Inject private volatile MessageFactory messageFactory; @Override public void onCreate(Bundle state) { @@ -67,7 +72,7 @@ implements OnClickListener, DatabaseListener { layout.setGravity(CENTER_HORIZONTAL); adapter = new ConversationListAdapter(this); - ListView list = new ListView(this); + list = new ListView(this); // Give me all the width and all the unused height list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); list.setAdapter(adapter); @@ -96,8 +101,6 @@ implements OnClickListener, DatabaseListener { // FIXME: Remove this private void insertFakeMessages() { - final DatabaseComponent db = this.db; - final MessageFactory messageFactory = this.messageFactory; dbExecutor.execute(new Runnable() { public void run() { try { @@ -160,29 +163,26 @@ implements OnClickListener, DatabaseListener { @Override public void onResume() { super.onResume(); - reloadMessageHeaders(); + loadHeaders(); } - private void reloadMessageHeaders() { - final DatabaseComponent db = this.db; + private void loadHeaders() { dbExecutor.execute(new Runnable() { public void run() { try { // Wait for the service to be bound and started serviceConnection.waitForStartup(); // Load the contact list from the database - if(LOG.isLoggable(INFO)) LOG.info("Loading contacts"); Collection<Contact> contacts = db.getContacts(); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + contacts.size() + " contacts"); - // Load the message headers from the database - if(LOG.isLoggable(INFO)) LOG.info("Loading headers"); + // Load the headers from the database Collection<PrivateMessageHeader> headers = db.getPrivateMessageHeaders(); if(LOG.isLoggable(INFO)) LOG.info("Loaded " + headers.size() + " headers"); - // Update the conversation list - updateConversationList(contacts, headers); + // Display the headers in the UI + displayHeaders(contacts, headers); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -195,7 +195,7 @@ implements OnClickListener, DatabaseListener { }); } - private void updateConversationList(final Collection<Contact> contacts, + private void displayHeaders(final Collection<Contact> contacts, final Collection<PrivateMessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { @@ -203,6 +203,7 @@ implements OnClickListener, DatabaseListener { for(ConversationListItem i : sortHeaders(contacts, headers)) adapter.add(i); adapter.sort(ConversationComparator.INSTANCE); + selectFirstUnread(); } }); } @@ -230,6 +231,18 @@ implements OnClickListener, DatabaseListener { return list; } + private void selectFirstUnread() { + int firstUnread = -1, count = adapter.getCount(); + for(int i = 0; i < count; i++) { + if(adapter.getItem(i).getUnreadCount() > 0) { + firstUnread = i; + break; + } + } + if(firstUnread == -1) list.setSelection(count - 1); + else list.setSelection(firstUnread); + } + @Override public void onDestroy() { super.onDestroy(); @@ -242,12 +255,108 @@ implements OnClickListener, DatabaseListener { } public void eventOccurred(DatabaseEvent e) { - if(e instanceof MessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - reloadMessageHeaders(); + if(e instanceof ContactRemovedEvent) { + removeContact(((ContactRemovedEvent) e).getContactId()); } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message removed, reloading"); - reloadMessageHeaders(); + loadHeaders(); // FIXME: Don't reload unnecessarily + } else if(e instanceof PrivateMessageAddedEvent) { + PrivateMessageAddedEvent p = (PrivateMessageAddedEvent) e; + addToConversation(p.getContactId(), p.getMessage(), p.isIncoming()); + } + } + + private void removeContact(final ContactId c) { + runOnUiThread(new Runnable() { + public void run() { + ConversationListItem item = findConversation(c); + if(item != null) { + adapter.remove(item); + selectFirstUnread(); + } + } + }); + } + + private ConversationListItem findConversation(ContactId c) { + int count = adapter.getCount(); + for(int i = 0; i < count; i++) { + ConversationListItem item = adapter.getItem(i); + if(item.getContactId().equals(c)) return item; + } + return null; // Not found + } + + private void addToConversation(final ContactId c, final Message m, + final boolean incoming) { + runOnUiThread(new Runnable() { + public void run() { + ConversationListItem item = findConversation(c); + if(item == null) { + loadContact(c, m, incoming); + } else if(item.add(m, incoming)) { + adapter.sort(ConversationComparator.INSTANCE); + selectFirstUnread(); + list.invalidate(); + } + } + }); + } + + private void loadContact(final ContactId c, final Message m, + final boolean incoming) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + // Wait for the service to be bound and started + serviceConnection.waitForStartup(); + // Load the contact from the DB and display it in the UI + displayContact(db.getContact(c), m, incoming); + } catch(NoSuchContactException e) { + if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + } + }); + } + + private void displayContact(final Contact c, final Message m, + final boolean incoming) { + runOnUiThread(new Runnable() { + public void run() { + // The item may have been added since loadContact() was called + ConversationListItem item = findConversation(c.getId()); + if(item == null) { + adapter.add(new ConversationListItem(c, m, incoming)); + adapter.sort(ConversationComparator.INSTANCE); + selectFirstUnread(); + } else if(item.add(m, incoming)) { + adapter.sort(ConversationComparator.INSTANCE); + selectFirstUnread(); + list.invalidate(); + } + } + }); + } + + private static class ConversationComparator + implements Comparator<ConversationListItem> { + + static final ConversationComparator INSTANCE = + new ConversationComparator(); + + public int compare(ConversationListItem a, ConversationListItem b) { + // The item with the newest message comes first + long aTime = a.getTimestamp(), bTime = b.getTimestamp(); + if(aTime > bTime) return -1; + if(aTime < bTime) return 1; + return 0; } } } diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java index eec30a91c80947d7cd4142db16e009f7c4c50991..edec2e49b360cfd2a0b1e4190f460a8cb0528f43 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java @@ -1,19 +1,25 @@ package net.sf.briar.android.messages; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import net.sf.briar.android.DescendingHeaderComparator; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; import net.sf.briar.api.db.PrivateMessageHeader; +import net.sf.briar.api.messaging.Message; +import net.sf.briar.api.messaging.MessageId; +// This class is not thread-safe class ConversationListItem { + private final Set<MessageId> messageIds = new HashSet<MessageId>(); private final Contact contact; - private final String subject; - private final long timestamp; - private final int unread; + private String subject; + private long timestamp; + private int unread; ConversationListItem(Contact contact, List<PrivateMessageHeader> headers) { if(headers.isEmpty()) throw new IllegalArgumentException(); @@ -21,9 +27,30 @@ class ConversationListItem { Collections.sort(headers, DescendingHeaderComparator.INSTANCE); subject = headers.get(0).getSubject(); timestamp = headers.get(0).getTimestamp(); - int unread = 0; - for(PrivateMessageHeader h : headers) if(!h.isRead()) unread++; - this.unread = unread; + unread = 0; + for(PrivateMessageHeader h : headers) { + if(!h.isRead()) unread++; + if(!messageIds.add(h.getId())) throw new IllegalArgumentException(); + } + } + + ConversationListItem(Contact contact, Message first, boolean incoming) { + this.contact = contact; + subject = first.getSubject(); + timestamp = first.getTimestamp(); + unread = incoming ? 1 : 0; + messageIds.add(first.getId()); + } + + boolean add(Message m, boolean incoming) { + if(!messageIds.add(m.getId())) return false; + if(m.getTimestamp() > timestamp) { + // The added message is the newest + subject = m.getSubject(); + timestamp = m.getTimestamp(); + } + if(incoming) unread++; + return true; } ContactId getContactId() { diff --git a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java index 3183aae7cffb1cc18cdd506f5d65e030727ed9a1..30a17a264bafe5526a1c83a8eaa79bcf622deb4a 100644 --- a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java @@ -53,16 +53,17 @@ implements OnClickListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - private ContactId contactId = null; - private MessageId messageId = null; private boolean read; private ImageButton readButton = null, prevButton = null, nextButton = null; private ImageButton replyButton = null; private TextView content = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + private volatile MessageId messageId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -186,8 +187,6 @@ implements OnClickListener { } private void setReadInDatabase(final boolean read) { - final DatabaseComponent db = this.db; - final MessageId messageId = this.messageId; dbExecutor.execute(new Runnable() { public void run() { try { @@ -217,8 +216,6 @@ implements OnClickListener { } private void loadMessageBody() { - final DatabaseComponent db = this.db; - final MessageId messageId = this.messageId; dbExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java index ced62326ac5f737bc66264c187b63a5f85d0ed69..589cdc31ef974b6dbb0673f5fff611d0b0c75445 100644 --- a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java @@ -53,17 +53,18 @@ implements OnClickListener, OnItemSelectedListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; - @Inject private DatabaseComponent db; - @Inject @DatabaseExecutor private Executor dbExecutor; - @Inject private MessageFactory messageFactory; - - private ContactId contactId = null; - private MessageId parentId = null; private ContactNameSpinnerAdapter adapter = null; private Spinner spinner = null; private ImageButton sendButton = null; private EditText content = null; + // Fields that are accessed from DB threads must be volatile + @Inject private volatile DatabaseComponent db; + @Inject @DatabaseExecutor private volatile Executor dbExecutor; + @Inject private volatile MessageFactory messageFactory; + private volatile ContactId contactId = null; + private volatile MessageId parentId = null; + @Override public void onCreate(Bundle state) { super.onCreate(null); @@ -93,7 +94,7 @@ implements OnClickListener, OnItemSelectedListener { spinner = new Spinner(this); spinner.setAdapter(adapter); spinner.setOnItemSelectedListener(this); - loadContactNames(); + loadContactList(); actionBar.addView(spinner); actionBar.addView(new HorizontalSpace(this)); @@ -121,22 +122,12 @@ implements OnClickListener, OnItemSelectedListener { serviceConnection, 0); } - private void loadContactNames() { - final DatabaseComponent db = this.db; + private void loadContactList() { dbExecutor.execute(new Runnable() { public void run() { try { serviceConnection.waitForStartup(); - final Collection<Contact> contacts = db.getContacts(); - runOnUiThread(new Runnable() { - public void run() { - for(Contact c : contacts) { - if(c.getId().equals(contactId)) - spinner.setSelection(adapter.getCount()); - adapter.add(c); - } - } - }); + updateContactList(db.getContacts()); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -148,6 +139,18 @@ implements OnClickListener, OnItemSelectedListener { }); } + private void updateContactList(final Collection<Contact> contacts) { + runOnUiThread(new Runnable() { + public void run() { + for(Contact c : contacts) { + if(c.getId().equals(contactId)) + spinner.setSelection(adapter.getCount()); + adapter.add(c); + } + } + }); + } + @Override public void onSaveInstanceState(Bundle state) { Parcelable p = content.onSaveInstanceState(); @@ -172,10 +175,6 @@ implements OnClickListener, OnItemSelectedListener { } private void storeMessage(final byte[] body) { - final DatabaseComponent db = this.db; - final MessageFactory messageFactory = this.messageFactory; - final ContactId contactId = this.contactId; - final MessageId parentId = this.parentId; dbExecutor.execute(new Runnable() { public void run() { try { diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index b930322fb4e59ec2b8424d7d9603c114cc4a8432..3e466d1918fd02dce4b0f4a45b8b7a6ef01b778f 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -157,9 +157,15 @@ public interface DatabaseComponent { /** Returns the configuration for the given transport. */ TransportConfig getConfig(TransportId t) throws DbException; + /** Returns the contact with the given ID. */ + Contact getContact(ContactId c) throws DbException; + /** Returns all contacts. */ 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. */ TransportProperties getLocalProperties(TransportId t) throws DbException; diff --git a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java index 543f34a939b1eaed183c11b3efc9c26ddf0fb52e..a0d796a7483b4c9c60a653bd8b53041cd2b9d9c6 100644 --- a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java @@ -1,20 +1,31 @@ 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.GroupId; +import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; public class GroupMessageHeader extends MessageHeader { private final GroupId groupId; private final Author author; + private final Rating rating; public GroupMessageHeader(MessageId id, MessageId parent, 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); this.groupId = groupId; 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. */ @@ -28,4 +39,12 @@ public class GroupMessageHeader extends MessageHeader { public Author getAuthor() { return author; } + + /** + * Returns the rating for the message's author, or Rating.UNRATED if this + * is an anonymous message. + */ + public Rating getRating() { + return rating; + } } diff --git a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java index af55211c66fb691f233ce82816525d03fca2ff29..0add5ab62ae1f48162e2c88f8574a6feab978f58 100644 --- a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java @@ -1,6 +1,7 @@ package net.sf.briar.api.db; import net.sf.briar.api.ContactId; +import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; public class PrivateMessageHeader extends MessageHeader { @@ -16,6 +17,12 @@ public class PrivateMessageHeader extends MessageHeader { 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 * recipient (if outgoing) of this message. diff --git a/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..acf9ca49bc4c39c3d16ad2cda5e832158a18d97e --- /dev/null +++ b/briar-api/src/net/sf/briar/api/db/event/GroupMessageAddedEvent.java @@ -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; + } +} diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java deleted file mode 100644 index d28dd1664d455c6188cc1c520d919cf1798ef215..0000000000000000000000000000000000000000 --- a/briar-api/src/net/sf/briar/api/db/event/MessageAddedEvent.java +++ /dev/null @@ -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 { - -} diff --git a/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java index 4a5ae2c688144c237f5d260e80ad37e86dc3f2d5..2d338abf962ff59badd5b88b17bd0b801769a72c 100644 --- a/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/MessageReceivedEvent.java @@ -1,6 +1,17 @@ package net.sf.briar.api.db.event; +import net.sf.briar.api.ContactId; + /** An event that is broadcast when a message is received. */ public class MessageReceivedEvent extends DatabaseEvent { + private final ContactId contactId; + + public MessageReceivedEvent(ContactId contactId) { + this.contactId = contactId; + } + + public ContactId getContactId() { + return contactId; + } } diff --git a/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..45bcd16bd38d875fb5ce5e587f619d90139a213f --- /dev/null +++ b/briar-api/src/net/sf/briar/api/db/event/PrivateMessageAddedEvent.java @@ -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; + } +} diff --git a/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java b/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java index 76b39be75b457673ef884b9b8582ed3dc79364ea..1955d8f9dfc4f9889fc1e1de2301c5240f85efb8 100644 --- a/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java +++ b/briar-api/src/net/sf/briar/api/db/event/SubscriptionAddedEvent.java @@ -1,17 +1,17 @@ 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. */ public class SubscriptionAddedEvent extends DatabaseEvent { - private final GroupId groupId; + private final Group group; - public SubscriptionAddedEvent(GroupId groupId) { - this.groupId = groupId; + public SubscriptionAddedEvent(Group group) { + this.group = group; } - public GroupId getGroupId() { - return groupId; + public Group getGroup() { + return group; } } diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index 97df26ac6e48387952bc523d8b909f842d67eb89..0dc1c9d360deb830ad7479b55a554cf9dc4eb7e3 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -202,6 +202,13 @@ interface Database<T> { */ 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. * <p> @@ -230,6 +237,11 @@ interface Database<T> { */ 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 * message has no parent, or the parent is absent from the database, or the diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index bd34f2e3f59f13064624802e2a0cc93d3d63d275..67287cd99a2036e072755b639be0d9b96a91d5a8 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -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.DatabaseEvent; 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.LocalTransportsUpdatedEvent; -import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; 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.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; @@ -286,7 +287,7 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - if(added) callListeners(new MessageAddedEvent()); + if(added) callListeners(new GroupMessageAddedEvent(m, false)); } /** @@ -399,7 +400,7 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - if(added) callListeners(new MessageAddedEvent()); + if(added) callListeners(new PrivateMessageAddedEvent(m, c, false)); } 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 { contactLock.readLock().lock(); 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) throws DbException { transportLock.readLock().lock(); @@ -1301,8 +1345,12 @@ DatabaseCleaner.Callback { } finally { contactLock.readLock().unlock(); } - callListeners(new MessageReceivedEvent()); - if(added) callListeners(new MessageAddedEvent()); + callListeners(new MessageReceivedEvent(c)); + 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 { subscriptionLock.writeLock().unlock(); } - if(added) callListeners(new SubscriptionAddedEvent(g.getId())); + if(added) callListeners(new SubscriptionAddedEvent(g)); return added; } diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index 0877effb7c0d510349fe5945e34283856abf08b6..e0d7150e58a5b3ac94b7296c983f7ffc81ab33e8 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -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) throws DbException { 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) throws DbException { PreparedStatement ps = null; @@ -1228,10 +1274,12 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT messageId, parentId, authorId, authorName," + String sql = "SELECT messageId, parentId, m.authorId, authorName," + " authorKey, contentType, subject, timestamp, read," - + " starred" - + " FROM messages" + + " starred, rating" + + " FROM messages AS m" + + " LEFT OUTER JOIN ratings AS r" + + " ON m.authorId = r.authorId" + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); @@ -1242,13 +1290,18 @@ abstract class JdbcDatabase implements Database<Connection> { MessageId id = new MessageId(rs.getBytes(1)); byte[] b = rs.getBytes(2); MessageId parent = b == null ? null : new MessageId(b); - Author author = null; + Author author; + Rating rating; b = rs.getBytes(3); - if(b != null) { + if(b == null) { + author = null; + rating = UNRATED; + } else { AuthorId authorId = new AuthorId(b); String authorName = rs.getString(4); byte[] authorKey = rs.getBytes(5); author = new Author(authorId, authorName, authorKey); + rating = Rating.values()[rs.getByte(11)]; } String contentType = rs.getString(6); String subject = rs.getString(7); @@ -1256,7 +1309,7 @@ abstract class JdbcDatabase implements Database<Connection> { boolean read = rs.getBoolean(9); boolean starred = rs.getBoolean(10); headers.add(new GroupMessageHeader(id, parent, contentType, - subject, timestamp, read, starred, g, author)); + subject, timestamp, read, starred, g, author, rating)); } rs.close(); ps.close(); diff --git a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java index 2681ec67b5ed200457b0641d3ea0f2521203066f..c0604546f76b93f697afadca3f5ed2817a0ddf0d 100644 --- a/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java +++ b/briar-core/src/net/sf/briar/messaging/duplex/DuplexConnection.java @@ -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.DatabaseEvent; 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.LocalTransportsUpdatedEvent; -import net.sf.briar.api.db.event.MessageAddedEvent; import net.sf.briar.api.db.event.MessageExpiredEvent; 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.RemoteRetentionTimeUpdatedEvent; import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; @@ -134,6 +135,9 @@ abstract class DuplexConnection implements DatabaseListener { if(e instanceof ContactRemovedEvent) { ContactRemovedEvent c = (ContactRemovedEvent) e; 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) { dbExecutor.execute(new GenerateRetentionUpdate()); } else if(e instanceof LocalSubscriptionsUpdatedEvent) { @@ -143,11 +147,15 @@ abstract class DuplexConnection implements DatabaseListener { dbExecutor.execute(new GenerateSubscriptionUpdate()); } else if(e instanceof LocalTransportsUpdatedEvent) { dbExecutor.execute(new GenerateTransportUpdates()); - } else if(e instanceof MessageAddedEvent) { - if(canSendOffer.getAndSet(false)) - dbExecutor.execute(new GenerateOffer()); } 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) { RatingChangedEvent r = (RatingChangedEvent) e; if(r.getRating() == GOOD && canSendOffer.getAndSet(false)) diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index 63d8330ada7c3ab8f24aff24e9497add7fa6cfff..f1622c17c0a47868aca3abc686fde13521df47a1 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -20,12 +20,14 @@ import net.sf.briar.api.TransportConfig; import net.sf.briar.api.TransportProperties; import net.sf.briar.api.db.DatabaseComponent; 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.event.ContactAddedEvent; import net.sf.briar.api.db.event.ContactRemovedEvent; 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.MessageAddedEvent; +import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.db.event.RatingChangedEvent; import net.sf.briar.api.db.event.SubscriptionAddedEvent; 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); context.checking(new Expectations() {{ // 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)); - exactly(27).of(database).containsContact(txn, contactId); + exactly(28).of(database).containsContact(txn, contactId); will(returnValue(false)); - exactly(27).of(database).abortTransaction(txn); + exactly(28).of(database).abortTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, shutdown); @@ -572,6 +574,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase { fail(); } catch(NoSuchContactException expected) {} + try { + db.getContact(contactId); + fail(); + } catch(NoSuchContactException expected) {} + try { db.getVisibleSubscriptions(contactId); fail(); @@ -660,6 +667,53 @@ public abstract class DatabaseComponentTest extends BriarTestCase { 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 public void testVariousMethodsThrowExceptionIfTransportIsMissing() throws Exception { @@ -1454,7 +1508,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).setSendability(txn, messageId, 0); oneOf(database).commitTransaction(txn); // 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, shutdown); @@ -1485,7 +1540,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).addStatus(txn, contactId, messageId, false); // 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, shutdown); diff --git a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java index 105518b8f53537393fde5308e8a1f4115ea6e357..b87285eb61654c51464869bd015e99e6a83badfd 100644 --- a/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java +++ b/briar-tests/src/net/sf/briar/messaging/simplex/SimplexMessagingIntegrationTest.java @@ -15,7 +15,7 @@ import net.sf.briar.api.crypto.KeyManager; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.event.DatabaseEvent; import net.sf.briar.api.db.event.DatabaseListener; -import net.sf.briar.api.db.event.MessageAddedEvent; +import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageFactory; import net.sf.briar.api.messaging.MessageVerifier; @@ -186,14 +186,14 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { messageVerifier, db, connRegistry, connWriterFactory, packetWriterFactory, ctx, transport); // No messages should have been added yet - assertFalse(listener.messagesAdded); + assertFalse(listener.messageAdded); // Read whatever needs to be read simplex.read(); assertTrue(transport.getDisposed()); assertFalse(transport.getException()); assertTrue(transport.getRecognised()); // The private message from Alice should have been added - assertTrue(listener.messagesAdded); + assertTrue(listener.messageAdded); // Clean up km.stop(); db.close(); @@ -206,10 +206,10 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { private static class MessageListener implements DatabaseListener { - private boolean messagesAdded = false; + private boolean messageAdded = false; public void eventOccurred(DatabaseEvent e) { - if(e instanceof MessageAddedEvent) messagesAdded = true; + if(e instanceof PrivateMessageAddedEvent) messageAdded = true; } } }