diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 44027595bfa1cdb6c2e4867484687dff76cc21d9..8dcdb7163d7ee18e4a19a2b196d4175e7ebc96f4 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" > </activity> + <activity + android:name=".android.messages.ConversationListActivity" + android:label="@string/messages_title" > + </activity> </application> </manifest> 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 0000000000000000000000000000000000000000..7c25f351188f98caa89792889ae3e27960c16da7 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 0000000000000000000000000000000000000000..c10325fe114dd69e75297bc427d5d1530466ff20 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 0000000000000000000000000000000000000000..7b2e1d5649aff4c8adb60e454b4bd77275dc071d 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 0000000000000000000000000000000000000000..392eeb00c565a513412e8cf90a208c2fe8826e2c 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 0000000000000000000000000000000000000000..674b69b08715b9978186818d728ce4133d9f1ae2 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 0000000000000000000000000000000000000000..da44dd82c3ab07009b3b45b638baceb90fcff514 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 0000000000000000000000000000000000000000..7ff6c8d0e68f491e420faa9a955c9a6c19f8ea1e 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 75b51e2dbd556eb642e19017bc30c30d06723209..79236a39603272358460928e4e20ae5da0d13a27 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -12,7 +12,7 @@ <string name="contact_list_title">Contacts</string> <string name="contact_connected">Connected</string> <string name="contact_last_connected">Last connected <br /> %s</string> - <string name="add_contact_button">Add a contact</string> + <string name="add_contact_button">New Contact</string> <string name="add_contact_title">Add a Contact</string> <string name="same_network">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.</string> <string name="wifi_not_available">Wi-Fi is not available on this device</string> @@ -39,6 +39,7 @@ <string name="interfering">This could mean that someone is trying to interfere with your connection.</string> <string name="contact_added">Contact added</string> <string name="enter_nickname">Please enter a nickname for this contact:</string> - <string name="add_another_contact_button">Add another contact</string> + <string name="messages_title">Messages</string> + <string name="compose_button">New Message</string> <string name="done_button">Done</string> </resources> diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java index 65b8fd01d7be51c2b93f53f5a7fd0cbf8e29c89f..07fe3d0b8c0ea586c7247044e6d999aea23dde84 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 b12ba8db66b37edf321abe476acae876e24dc2bf..53143108ea60ce1c2313bcf5b42e283177c71b53 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<Contact> contacts = db.getContacts(); + Collection<Contact> 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 69dcecfd8eb2c5e583a06a9aac32e271c12d6f00..1849d60ede19774a6eab6c344d97a04a568bcd9f 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<ContactListItem> { - 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<ContactListItem>()); } @@ -34,14 +34,14 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> { 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<ContactListItem> { 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 99d2a79fcc707dfc4b6be43eaf24ee4d86f44eac..47747e3a1bc267c2e85d4aace6af8129294f2766 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<ContactListItem> { - @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 0000000000000000000000000000000000000000..a45ac9c8bfcb23608c6425a34531014a9660ed87 --- /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<ConversationListItem> 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<PrivateMessageHeader> 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<Contact> contacts = db.getContacts(); + if(LOG.isLoggable(INFO)) + LOG.info("Loaded " + contacts.size() + " contacts"); + // Load the message 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); + } 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<Contact> contacts, + final Collection<PrivateMessageHeader> headers) { + runOnUiThread(new Runnable() { + public void run() { + adapter.clear(); + for(ConversationListItem i : sortHeaders(contacts, headers)) + adapter.add(i); + adapter.sort(ConversationListItem.COMPARATOR); + } + }); + } + + private List<ConversationListItem> sortHeaders(Collection<Contact> contacts, + Collection<PrivateMessageHeader> headers) { + // Group the headers into conversations, one per contact + Map<ContactId, List<PrivateMessageHeader>> map = + new HashMap<ContactId, List<PrivateMessageHeader>>(); + for(Contact c : contacts) + map.put(c.getId(), new ArrayList<PrivateMessageHeader>()); + for(PrivateMessageHeader h : headers) { + ContactId id = h.getContactId(); + List<PrivateMessageHeader> conversation = map.get(id); + // Ignore header if the contact was added after db.getContacts() + if(conversation != null) conversation.add(h); + } + // Create a list item for each non-empty conversation + List<ConversationListItem> list = new ArrayList<ConversationListItem>(); + for(Contact c : contacts) { + List<PrivateMessageHeader> conversation = map.get(c.getId()); + if(!conversation.isEmpty()) + list.add(new ConversationListItem(c, conversation)); + } + return list; + } +} 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 0000000000000000000000000000000000000000..f0ef7ec79ff4f41a1b4c1bf01caae5a464813c78 --- /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<ConversationListItem> { + + ConversationListAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<ConversationListItem>()); + } + + @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 0000000000000000000000000000000000000000..711329ec3bfdb302121854bb4411f0c1899e0513 --- /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<ConversationListItem> COMPARATOR = + new ItemComparator(); + + private static final Comparator<PrivateMessageHeader> HEADER_COMPARATOR = + new HeaderComparator(); + + private final Contact contact; + private final List<PrivateMessageHeader> headers; + private final boolean read, starred; + + ConversationListItem(Contact contact, List<PrivateMessageHeader> 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<PrivateMessageHeader> { + + 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<ConversationListItem> { + + 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)); + } + } +}