diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 44027595b..8dcdb7163 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -40,5 +40,9 @@
android:name=".android.invitation.AddContactActivity"
android:label="@string/add_contact_title" >
+
+
diff --git a/briar-android/res/drawable-hdpi/rating_important.png b/briar-android/res/drawable-hdpi/rating_important.png
new file mode 100644
index 000000000..7c25f3511
Binary files /dev/null and b/briar-android/res/drawable-hdpi/rating_important.png differ
diff --git a/briar-android/res/drawable-hdpi/rating_not_important.png b/briar-android/res/drawable-hdpi/rating_not_important.png
new file mode 100644
index 000000000..c10325fe1
Binary files /dev/null and b/briar-android/res/drawable-hdpi/rating_not_important.png differ
diff --git a/briar-android/res/drawable-mdpi/rating_important.png b/briar-android/res/drawable-mdpi/rating_important.png
new file mode 100644
index 000000000..7b2e1d564
Binary files /dev/null and b/briar-android/res/drawable-mdpi/rating_important.png differ
diff --git a/briar-android/res/drawable-mdpi/rating_not_important.png b/briar-android/res/drawable-mdpi/rating_not_important.png
new file mode 100644
index 000000000..392eeb00c
Binary files /dev/null and b/briar-android/res/drawable-mdpi/rating_not_important.png differ
diff --git a/briar-android/res/drawable-xhdpi/content_new_email.png b/briar-android/res/drawable-xhdpi/content_new_email.png
new file mode 100644
index 000000000..674b69b08
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/content_new_email.png differ
diff --git a/briar-android/res/drawable-xhdpi/rating_important.png b/briar-android/res/drawable-xhdpi/rating_important.png
new file mode 100644
index 000000000..da44dd82c
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/rating_important.png differ
diff --git a/briar-android/res/drawable-xhdpi/rating_not_important.png b/briar-android/res/drawable-xhdpi/rating_not_important.png
new file mode 100644
index 000000000..7ff6c8d0e
Binary files /dev/null and b/briar-android/res/drawable-xhdpi/rating_not_important.png differ
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 75b51e2db..79236a396 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -12,7 +12,7 @@
Contacts
Connected
Last connected <br /> %s
- Add a contact
+ New Contact
Add a Contact
Briar can add contacts via Wi-Fi or Bluetooth. For security reasons, you must be face-to-face to add someone as a contact. To use Wi-Fi you must both be connected to the same network.
Wi-Fi is not available on this device
@@ -39,6 +39,7 @@
This could mean that someone is trying to interfere with your connection.
Contact added
Please enter a nickname for this contact:
- Add another contact
+ Messages
+ New Message
Done
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 65b8fd01d..07fe3d0b8 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -12,6 +12,7 @@ import net.sf.briar.R;
import net.sf.briar.android.BriarService.BriarBinder;
import net.sf.briar.android.BriarService.BriarServiceConnection;
import net.sf.briar.android.contact.ContactListActivity;
+import net.sf.briar.android.messages.ConversationListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
@@ -78,7 +79,8 @@ public class HomeScreenActivity extends BriarActivity {
messagesButton.setText(R.string.messages_button);
messagesButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
- // FIXME: Hook this button up to an activity
+ startActivity(new Intent(HomeScreenActivity.this,
+ ConversationListActivity.class));
}
});
buttons.add(messagesButton);
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 b12ba8db6..53143108e 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java
@@ -59,26 +59,25 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
@Override
public void onCreate(Bundle state) {
super.onCreate(null);
- if(LOG.isLoggable(INFO)) LOG.info("Created");
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new ContactListAdapter(this);
- ListView listView = new ListView(this);
+ ListView list = new ListView(this);
// Give me all the width and all the unused height
- listView.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT,
- 1f));
- listView.setAdapter(adapter);
- layout.addView(listView);
+ list.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1f));
+ list.setAdapter(adapter);
+ layout.addView(list);
Button addContactButton = new Button(this);
- LayoutParams lp = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
- addContactButton.setLayoutParams(lp);
+ addContactButton.setBackgroundResource(0);
+ addContactButton.setLayoutParams(new LayoutParams(MATCH_PARENT,
+ WRAP_CONTENT));
+ addContactButton.setCompoundDrawablesWithIntrinsicBounds(0,
+ R.drawable.social_add_person, 0, 0);
addContactButton.setText(R.string.add_contact_button);
- addContactButton.setCompoundDrawablesWithIntrinsicBounds(
- R.drawable.social_add_person, 0, 0, 0);
addContactButton.setOnClickListener(this);
layout.addView(addContactButton);
@@ -146,7 +145,7 @@ implements OnClickListener, DatabaseListener, ConnectionListener {
IBinder binder = serviceConnection.waitForBinder();
((BriarBinder) binder).getService().waitForStartup();
// Load the contacts from the database
- final Collection contacts = db.getContacts();
+ Collection contacts = db.getContacts();
if(LOG.isLoggable(INFO))
LOG.info("Loaded " + contacts.size() + " contacts");
// Update the contact list
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
index 69dcecfd8..1849d60ed 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java
@@ -20,8 +20,8 @@ import android.widget.TextView;
class ContactListAdapter extends ArrayAdapter {
- ContactListAdapter(Context context) {
- super(context, android.R.layout.simple_expandable_list_item_1,
+ ContactListAdapter(Context ctx) {
+ super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList());
}
@@ -34,14 +34,14 @@ class ContactListAdapter extends ArrayAdapter {
layout.setGravity(CENTER);
ImageView bulb = new ImageView(ctx);
- if(item.getConnected()) bulb.setImageResource(R.drawable.green_bulb);
- else bulb.setImageResource(R.drawable.grey_bulb);
bulb.setPadding(5, 5, 5, 5);
+ if(item.isConnected()) bulb.setImageResource(R.drawable.green_bulb);
+ else bulb.setImageResource(R.drawable.grey_bulb);
layout.addView(bulb);
TextView name = new TextView(ctx);
// Give me all the unused width
- name.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1f));
+ name.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1));
name.setTextSize(18);
name.setText(item.getName());
layout.addView(name);
@@ -49,7 +49,7 @@ class ContactListAdapter extends ArrayAdapter {
TextView connected = new TextView(ctx);
connected.setTextSize(12);
connected.setPadding(5, 0, 5, 0);
- if(item.getConnected()) {
+ if(item.isConnected()) {
connected.setText(R.string.contact_connected);
} else {
String format = ctx.getResources().getString(
diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
index 99d2a79fc..47747e3a1 100644
--- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
+++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java
@@ -30,7 +30,7 @@ class ContactListItem {
return contact.getLastConnected();
}
- boolean getConnected() {
+ boolean isConnected() {
return connected;
}
@@ -40,7 +40,6 @@ class ContactListItem {
private static class ItemComparator implements Comparator {
- @Override
public int compare(ContactListItem a, ContactListItem b) {
return String.CASE_INSENSITIVE_ORDER.compare(a.contact.getName(),
b.contact.getName());
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
new file mode 100644
index 000000000..a45ac9c8b
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java
@@ -0,0 +1,225 @@
+package net.sf.briar.android.messages;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarBinder;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.api.Contact;
+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.PrivateMessageHeader;
+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.messaging.Message;
+import net.sf.briar.api.messaging.MessageFactory;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class ConversationListActivity extends BriarActivity
+implements OnClickListener, DatabaseListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(ConversationListActivity.class.getName());
+
+ private final BriarServiceConnection serviceConnection =
+ new BriarServiceConnection();
+
+ @Inject private DatabaseComponent db;
+ @Inject @DatabaseExecutor private Executor dbExecutor;
+ @Inject private MessageFactory messageFactory;
+
+ private ArrayAdapter adapter = null;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(null);
+ LinearLayout layout = new LinearLayout(this);
+ layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ layout.setOrientation(VERTICAL);
+ layout.setGravity(CENTER_HORIZONTAL);
+
+ adapter = new ConversationListAdapter(this);
+ ListView list = new ListView(this);
+ // Give me all the width and all the unused height
+ list.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1f));
+ list.setAdapter(adapter);
+ layout.addView(list);
+
+ Button composeButton = new Button(this);
+ composeButton.setBackgroundResource(0);
+ composeButton.setLayoutParams(new LayoutParams(MATCH_PARENT,
+ WRAP_CONTENT));
+ composeButton.setCompoundDrawablesWithIntrinsicBounds(0,
+ R.drawable.content_new_email, 0, 0);
+ composeButton.setText(R.string.compose_button);
+ composeButton.setOnClickListener(this);
+ layout.addView(composeButton);
+
+ setContentView(layout);
+
+ // Listen for messages being added or removed
+ db.addListener(this);
+ // Bind to the service so we can wait for the DB to be opened
+ bindService(new Intent(BriarService.class.getName()),
+ serviceConnection, 0);
+ // Load the message headers from the DB
+ reloadMessageHeaders();
+
+ // Add some fake messages to the database in a background thread
+ // FIXME: Remove this
+ dbExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ // Wait for the service to be bound and started
+ IBinder binder = serviceConnection.waitForBinder();
+ ((BriarBinder) binder).getService().waitForStartup();
+ if(LOG.isLoggable(INFO)) LOG.info("Service started");
+ Collection headers =
+ db.getPrivateMessageHeaders();
+ if(headers.isEmpty()) {
+ // Insert a fake contact
+ ContactId contactId = db.addContact("Carol");
+ // Insert some messages to the contact
+ Message m = messageFactory.createPrivateMessage(null,
+ "First message's subject",
+ "First message's body".getBytes("UTF-8"));
+ db.addLocalPrivateMessage(m, contactId);
+ db.setStarredFlag(m.getId(), true);
+ Thread.sleep(2000);
+ m = messageFactory.createPrivateMessage(m.getId(),
+ "Second message's subject",
+ "Second message's body".getBytes("UTF-8"));
+ db.addLocalPrivateMessage(m, contactId);
+ db.setReadFlag(m.getId(), true);
+ }
+ } catch(DbException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ } catch(GeneralSecurityException 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();
+ } catch(IOException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ db.removeListener(this);
+ unbindService(serviceConnection);
+ }
+
+ public void onClick(View view) {
+ // FIXME: Hook this button up to an activity
+ }
+
+ public void eventOccurred(DatabaseEvent e) {
+ if(e instanceof MessageAddedEvent) reloadMessageHeaders();
+ else if(e instanceof MessageExpiredEvent) reloadMessageHeaders();
+ }
+
+ private void reloadMessageHeaders() {
+ dbExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ // Wait for the service to be bound and started
+ IBinder binder = serviceConnection.waitForBinder();
+ ((BriarBinder) binder).getService().waitForStartup();
+ // Load the contact list from the database
+ Collection contacts = db.getContacts();
+ if(LOG.isLoggable(INFO))
+ LOG.info("Loaded " + contacts.size() + " contacts");
+ // Load the message headers from the database
+ Collection headers =
+ db.getPrivateMessageHeaders();
+ if(LOG.isLoggable(INFO))
+ LOG.info("Loaded " + headers.size() + " headers");
+ // Update the conversation list
+ updateConversationList(contacts, headers);
+ } 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 updateConversationList(final Collection contacts,
+ final Collection headers) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ adapter.clear();
+ for(ConversationListItem i : sortHeaders(contacts, headers))
+ adapter.add(i);
+ adapter.sort(ConversationListItem.COMPARATOR);
+ }
+ });
+ }
+
+ private List sortHeaders(Collection contacts,
+ Collection headers) {
+ // Group the headers into conversations, one per contact
+ Map> map =
+ new HashMap>();
+ for(Contact c : contacts)
+ map.put(c.getId(), new ArrayList());
+ for(PrivateMessageHeader h : headers) {
+ ContactId id = h.getContactId();
+ List conversation = map.get(id);
+ // Ignore header if the contact was added after db.getContacts()
+ if(conversation != null) conversation.add(h);
+ }
+ // Create a list item for each non-empty conversation
+ List list = new ArrayList();
+ for(Contact c : contacts) {
+ List conversation = map.get(c.getId());
+ if(!conversation.isEmpty())
+ list.add(new ConversationListItem(c, conversation));
+ }
+ return list;
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
new file mode 100644
index 000000000..f0ef7ec79
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java
@@ -0,0 +1,75 @@
+package net.sf.briar.android.messages;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.LEFT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+
+class ConversationListAdapter extends ArrayAdapter {
+
+ ConversationListAdapter(Context ctx) {
+ super(ctx, android.R.layout.simple_expandable_list_item_1,
+ new ArrayList());
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ConversationListItem item = getItem(position);
+ Context ctx = getContext();
+ LinearLayout layout = new LinearLayout(ctx);
+ layout.setOrientation(HORIZONTAL);
+ layout.setGravity(CENTER);
+
+ ImageView star = new ImageView(ctx);
+ star.setPadding(5, 5, 5, 5);
+ if(item.getStarred())
+ star.setImageResource(R.drawable.rating_important);
+ else star.setImageResource(R.drawable.rating_not_important);
+ layout.addView(star);
+
+ LinearLayout innerLayout = new LinearLayout(ctx);
+ // Give me all the unused width
+ innerLayout.setLayoutParams(new LayoutParams(WRAP_CONTENT,
+ WRAP_CONTENT, 1));
+ innerLayout.setOrientation(VERTICAL);
+ innerLayout.setGravity(LEFT);
+ innerLayout.setPadding(0, 5, 0, 5);
+
+ TextView name = new TextView(ctx);
+ name.setTextSize(18);
+ name.setText(item.getName() + " (" + item.getLength() + ")");
+ innerLayout.addView(name);
+
+ TextView subject = new TextView(ctx);
+ subject.setTextSize(14);
+ if(!item.getRead()) subject.setTypeface(null, BOLD);
+ subject.setText(item.getSubject());
+ innerLayout.addView(subject);
+ layout.addView(innerLayout);
+
+ TextView date = new TextView(ctx);
+ date.setTextSize(14);
+ date.setPadding(5, 0, 10, 0);
+ long then = item.getTimestamp(), now = System.currentTimeMillis();
+ date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+ layout.addView(date);
+
+ return layout;
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
new file mode 100644
index 000000000..711329ec3
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java
@@ -0,0 +1,81 @@
+package net.sf.briar.android.messages;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import net.sf.briar.api.Contact;
+import net.sf.briar.api.db.PrivateMessageHeader;
+
+class ConversationListItem {
+
+ static final Comparator COMPARATOR =
+ new ItemComparator();
+
+ private static final Comparator HEADER_COMPARATOR =
+ new HeaderComparator();
+
+ private final Contact contact;
+ private final List headers;
+ private final boolean read, starred;
+
+ ConversationListItem(Contact contact, List headers) {
+ if(headers.isEmpty()) throw new IllegalArgumentException();
+ Collections.sort(headers, HEADER_COMPARATOR);
+ boolean read = false, starred = false;
+ for(PrivateMessageHeader h : headers) {
+ read &= h.getRead();
+ starred |= h.getStarred();
+ }
+ this.contact = contact;
+ this.headers = headers;
+ this.read = read;
+ this.starred = starred;
+ }
+
+ String getName() {
+ return contact.getName();
+ }
+
+ String getSubject() {
+ return headers.get(0).getSubject();
+ }
+
+ long getTimestamp() {
+ return headers.get(0).getTimestamp();
+ }
+
+ boolean getRead() {
+ return read;
+ }
+
+ boolean getStarred() {
+ return starred;
+ }
+
+ int getLength() {
+ return headers.size();
+ }
+
+ private static class HeaderComparator
+ implements Comparator {
+
+ public int compare(PrivateMessageHeader a, PrivateMessageHeader b) {
+ // The newest message comes first
+ long aTime = a.getTimestamp(), bTime = b.getTimestamp();
+ if(aTime > bTime) return -1;
+ if(aTime < bTime) return 1;
+ return 0;
+ }
+ }
+
+ private static class ItemComparator
+ implements Comparator {
+
+ public int compare(ConversationListItem a, ConversationListItem b) {
+ // The item with the newest message comes first
+ return HEADER_COMPARATOR.compare(a.headers.get(0),
+ b.headers.get(0));
+ }
+ }
+}