diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index e5158b9efb1f20aea845751f3f58c27970102401..02a4e1d7f60a429aabcee09d51400f2aa4906d1d 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -82,19 +82,15 @@ android:label="@string/add_contact_title" > </activity> <activity - android:name=".android.messages.ConversationActivity" + android:name="net.sf.briar.android.contact.ConversationActivity" android:label="@string/app_name" > </activity> <activity - android:name=".android.messages.ConversationListActivity" - android:label="@string/messages_title" > - </activity> - <activity - android:name=".android.messages.ReadPrivateMessageActivity" + android:name="net.sf.briar.android.contact.ReadPrivateMessageActivity" android:label="@string/app_name" > </activity> <activity - android:name=".android.messages.WritePrivateMessageActivity" + android:name="net.sf.briar.android.contact.WritePrivateMessageActivity" android:label="@string/new_message_title" > </activity> </application> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 00d884d357814277c9db288eab9e79b64d634a3c..00426309219cb6e6574a3931c76a376f093a219f 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -12,7 +12,6 @@ <string name="try_again">Wrong password, try again:</string> <string name="expiry_warning">This software has expired.\nPlease install a newer version.</string> <string name="contact_list_button">Contacts</string> - <string name="messages_button">Messages</string> <string name="forums_button">Forums</string> <string name="synchronize_button">Synchronize</string> <string name="quit_button">Quit</string> diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java index b9374dd7d4af57791c43d0ba84e099a61d19ee58..6495889b6a3dc6fbe17234504e996fea0784c10e 100644 --- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java +++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java @@ -26,7 +26,6 @@ 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.groups.GroupListActivity; -import net.sf.briar.android.messages.ConversationListActivity; import net.sf.briar.api.LocalAuthor; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.android.ReferenceManager; @@ -315,20 +314,6 @@ public class HomeScreenActivity extends RoboActivity { }); buttons.add(contactsButton); - Button messagesButton = new Button(this); - messagesButton.setLayoutParams(matchMatch); - messagesButton.setBackgroundResource(0); - messagesButton.setCompoundDrawablesWithIntrinsicBounds(0, - R.drawable.content_email, 0, 0); - messagesButton.setText(R.string.messages_button); - messagesButton.setOnClickListener(new OnClickListener() { - public void onClick(View view) { - startActivity(new Intent(HomeScreenActivity.this, - ConversationListActivity.class)); - } - }); - buttons.add(messagesButton); - Button forumsButton = new Button(this); forumsButton.setLayoutParams(matchMatch); forumsButton.setBackgroundResource(0); 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 834416420ce8f764967b6bef60d3bdfbd80f15e3..44eb0aad30e4bef22adc6ef6536ba4f67ed7b039 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java @@ -24,6 +24,7 @@ import java.util.logging.Logger; import javax.inject.Inject; import net.sf.briar.R; +import net.sf.briar.android.groups.NoContactsDialog; import net.sf.briar.android.invitation.AddContactActivity; import net.sf.briar.android.util.HorizontalBorder; import net.sf.briar.android.util.HorizontalSpace; @@ -33,28 +34,32 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; 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.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.MessageExpiredEvent; +import net.sf.briar.api.db.event.PrivateMessageAddedEvent; import net.sf.briar.api.lifecycle.LifecycleManager; import net.sf.briar.api.transport.ConnectionListener; import net.sf.briar.api.transport.ConnectionRegistry; -import roboguice.activity.RoboActivity; +import roboguice.activity.RoboFragmentActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.view.View; import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListView; -public class ContactListActivity extends RoboActivity -implements OnClickListener, OnItemClickListener, DatabaseListener, -ConnectionListener { +public class ContactListActivity extends RoboFragmentActivity +implements OnClickListener, DatabaseListener, ConnectionListener, +NoContactsDialog.Listener { private static final Logger LOG = Logger.getLogger(ContactListActivity.class.getName()); @@ -63,7 +68,9 @@ ConnectionListener { private ContactListAdapter adapter = null; private ListView list = null; private ListLoadingProgressBar loading = null; - private ImageButton addContactButton = null, shareButton = null; + private ImageButton addContactButton = null, composeButton = null; + private ImageButton shareButton = null; + private NoContactsDialog noContactsDialog = null; // Fields that are accessed from background threads must be volatile @Inject private volatile DatabaseComponent db; @@ -83,7 +90,7 @@ ConnectionListener { // Give me all the width and all the unused height list.setLayoutParams(MATCH_WRAP_1); list.setAdapter(adapter); - list.setOnItemClickListener(this); + list.setOnItemClickListener(adapter); layout.addView(list); // Show a progress bar while the list is loading @@ -106,6 +113,13 @@ ConnectionListener { footer.addView(addContactButton); footer.addView(new HorizontalSpace(this)); + composeButton = new ImageButton(this); + composeButton.setBackgroundResource(0); + composeButton.setImageResource(R.drawable.content_new_email); + composeButton.setOnClickListener(this); + footer.addView(composeButton); + footer.addView(new HorizontalSpace(this)); + shareButton = new ImageButton(this); shareButton.setBackgroundResource(0); shareButton.setImageResource(R.drawable.social_share); @@ -115,6 +129,12 @@ ConnectionListener { layout.addView(footer); setContentView(layout); + + FragmentManager fm = getSupportFragmentManager(); + Fragment f = fm.findFragmentByTag("NoContactsDialog"); + if(f == null) noContactsDialog = new NoContactsDialog(); + else noContactsDialog = (NoContactsDialog) f; + noContactsDialog.setListener(this); } @Override @@ -122,21 +142,33 @@ ConnectionListener { super.onResume(); db.addListener(this); connectionRegistry.addListener(this); - loadContacts(); + loadHeaders(); } - private void loadContacts() { + private void loadHeaders() { + clearHeaders(); dbUiExecutor.execute(new Runnable() { public void run() { try { lifecycleManager.waitForDatabase(); long now = System.currentTimeMillis(); - Collection<Contact> contacts = db.getContacts(); Map<ContactId, Long> times = db.getLastConnected(); + for(Contact c : db.getContacts()) { + Long lastConnected = times.get(c.getId()); + if(lastConnected == null) continue; + try { + Collection<PrivateMessageHeader> headers = + db.getPrivateMessageHeaders(c.getId()); + displayHeaders(c, lastConnected, headers); + } catch(NoSuchContactException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Contact removed"); + } + } long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) - LOG.info("Load took " + duration + " ms"); - displayContacts(contacts, times); + LOG.info("Full load took " + duration + " ms"); + hideProgressBar(); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -149,25 +181,54 @@ ConnectionListener { }); } - private void displayContacts(final Collection<Contact> contacts, - final Map<ContactId, Long> times) { + private void clearHeaders() { + runOnUiThread(new Runnable() { + public void run() { + list.setVisibility(GONE); + loading.setVisibility(VISIBLE); + adapter.clear(); + adapter.notifyDataSetChanged(); + } + }); + } + + private void displayHeaders(final Contact c, final long lastConnected, + final Collection<PrivateMessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { list.setVisibility(VISIBLE); loading.setVisibility(GONE); - adapter.clear(); - for(Contact c : contacts) { - boolean now = connectionRegistry.isConnected(c.getId()); - Long last = times.get(c.getId()); - if(last != null) - adapter.add(new ContactListItem(c, now, last)); - } + boolean connected = connectionRegistry.isConnected(c.getId()); + // Remove the old item, if any + ContactListItem item = findItem(c.getId()); + if(item != null) adapter.remove(item); + // Add a new item + adapter.add(new ContactListItem(c, connected, lastConnected, + headers)); adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); } }); } + private void hideProgressBar() { + runOnUiThread(new Runnable() { + public void run() { + list.setVisibility(VISIBLE); + loading.setVisibility(GONE); + } + }); + } + + private ContactListItem findItem(ContactId c) { + int count = adapter.getCount(); + for(int i = 0; i < count; i++) { + ContactListItem item = adapter.getItem(i); + if(item.getContactId().equals(c)) return item; + } + return null; // Not found + } + @Override public void onPause() { super.onPause(); @@ -178,6 +239,13 @@ ConnectionListener { public void onClick(View view) { if(view == addContactButton) { startActivity(new Intent(this, AddContactActivity.class)); + } else if(view == composeButton) { + if(adapter.isEmpty()) { + FragmentManager fm = getSupportFragmentManager(); + noContactsDialog.show(fm, "NoContactsDialog"); + } else { + startActivity(new Intent(this, WritePrivateMessageActivity.class)); + } } else if(view == shareButton) { String apkPath = getPackageCodePath(); Intent i = new Intent(ACTION_SEND); @@ -188,14 +256,73 @@ ConnectionListener { } } - public void onItemClick(AdapterView<?> parent, View view, int position, - long id) { - // FIXME: Hook this up to an activity + public void eventOccurred(DatabaseEvent e) { + if(e instanceof ContactAddedEvent) { + loadHeaders(); + } else if(e instanceof ContactRemovedEvent) { + // Reload the conversation, expecting NoSuchContactException + if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading"); + reloadHeaders(((ContactRemovedEvent) e).getContactId()); + } else if(e instanceof MessageExpiredEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); + loadHeaders(); + } else if(e instanceof PrivateMessageAddedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); + reloadHeaders(((PrivateMessageAddedEvent) e).getContactId()); + } } - public void eventOccurred(DatabaseEvent e) { - if(e instanceof ContactAddedEvent) loadContacts(); - else if(e instanceof ContactRemovedEvent) loadContacts(); + private void reloadHeaders(final ContactId c) { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + lifecycleManager.waitForDatabase(); + long now = System.currentTimeMillis(); + Collection<PrivateMessageHeader> headers = + db.getPrivateMessageHeaders(c); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Partial load took " + duration + " ms"); + updateItem(c, headers); + } catch(NoSuchContactException e) { + if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); + removeItem(c); + } 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 database"); + Thread.currentThread().interrupt(); + } + } + }); + } + + private void updateItem(final ContactId c, + final Collection<PrivateMessageHeader> headers) { + runOnUiThread(new Runnable() { + public void run() { + ContactListItem item = findItem(c); + if(item == null) return; + // Replace the item with a new item containing the new headers + adapter.remove(item); + item = new ContactListItem(item.getContact(), + item.isConnected(), item.getLastConnected(), headers); + adapter.add(item); + adapter.sort(ItemComparator.INSTANCE); + adapter.notifyDataSetChanged(); + } + }); + } + + private void removeItem(final ContactId c) { + runOnUiThread(new Runnable() { + public void run() { + ContactListItem item = findItem(c); + if(item != null) adapter.remove(item); + } + }); } public void contactConnected(ContactId c) { @@ -225,6 +352,12 @@ ConnectionListener { }); } + public void contactCreationSelected() { + startActivity(new Intent(this, AddContactActivity.class)); + } + + public void contactCreationCancelled() {} + private static class ItemComparator implements Comparator<ContactListItem> { private static final ItemComparator INSTANCE = new ItemComparator(); 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 3ff0f06dad794ebdd11a6baf4b43d359656fbe10..2c3a988268cd7da93809aabecfad30d02ad89495 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java @@ -8,17 +8,21 @@ import java.util.ArrayList; import net.sf.briar.R; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.text.Html; import android.text.format.DateUtils; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -class ContactListAdapter extends ArrayAdapter<ContactListItem> { +class ContactListAdapter extends ArrayAdapter<ContactListItem> +implements OnItemClickListener { ContactListAdapter(Context ctx) { super(ctx, android.R.layout.simple_expandable_list_item_1, @@ -32,6 +36,9 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> { LinearLayout layout = new LinearLayout(ctx); layout.setOrientation(HORIZONTAL); layout.setGravity(CENTER_VERTICAL); + Resources res = ctx.getResources(); + if(item.getUnreadCount() > 0) + layout.setBackgroundColor(res.getColor(R.color.unread_background)); ImageView bulb = new ImageView(ctx); bulb.setPadding(5, 5, 5, 5); @@ -46,7 +53,10 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> { name.setTextSize(18); name.setMaxLines(1); name.setPadding(0, 10, 10, 10); - name.setText(item.getContactName()); + int unread = item.getUnreadCount(); + String contactName = item.getContactName(); + if(unread > 0) name.setText(contactName + " (" + unread + ")"); + else name.setText(contactName); layout.addView(name); TextView connected = new TextView(ctx); @@ -55,7 +65,6 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> { if(item.isConnected()) { connected.setText(R.string.contact_connected); } else { - Resources res = ctx.getResources(); String format = res.getString(R.string.format_last_connected); long then = item.getLastConnected(); CharSequence ago = DateUtils.getRelativeTimeSpanString(then); @@ -65,4 +74,15 @@ class ContactListAdapter extends ArrayAdapter<ContactListItem> { return layout; } + + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + ContactListItem item = getItem(position); + Intent i = new Intent(getContext(), ConversationActivity.class); + i.putExtra("net.sf.briar.CONTACT_ID", item.getContactId().getInt()); + i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName()); + i.putExtra("net.sf.briar.LOCAL_AUTHOR_ID", + item.getLocalAuthorId().getBytes()); + getContext().startActivity(i); + } } 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 72b9d17e679c1bb1fca9e072d24a021a5da0e969..5787c1158849f0c1acbad1857ccc6c80d61b02a8 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java @@ -1,7 +1,15 @@ package net.sf.briar.android.contact; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import net.sf.briar.android.DescendingHeaderComparator; +import net.sf.briar.api.AuthorId; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; +import net.sf.briar.api.db.PrivateMessageHeader; // This class is not thread-safe class ContactListItem { @@ -9,11 +17,32 @@ class ContactListItem { private final Contact contact; private boolean connected; private long lastConnected; + private final boolean empty; + private final long timestamp; + private final int unread; - ContactListItem(Contact contact, boolean connected, long lastConnected) { + ContactListItem(Contact contact, boolean connected, long lastConnected, + Collection<PrivateMessageHeader> headers) { this.contact = contact; this.connected = connected; this.lastConnected = lastConnected; + empty = headers.isEmpty(); + if(empty) { + timestamp = 0; + unread = 0; + } else { + List<PrivateMessageHeader> list = + new ArrayList<PrivateMessageHeader>(headers); + Collections.sort(list, DescendingHeaderComparator.INSTANCE); + timestamp = list.get(0).getTimestamp(); + int unread = 0; + for(PrivateMessageHeader h : list) if(!h.isRead()) unread++; + this.unread = unread; + } + } + + Contact getContact() { + return contact; } ContactId getContactId() { @@ -39,4 +68,20 @@ class ContactListItem { void setConnected(boolean connected) { this.connected = connected; } + + AuthorId getLocalAuthorId() { + return contact.getLocalAuthorId(); + } + + boolean isEmpty() { + return empty; + } + + long getTimestamp() { + return timestamp; + } + + int getUnreadCount() { + return unread; + } } \ No newline at end of file diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java similarity index 99% rename from briar-android/src/net/sf/briar/android/messages/ConversationActivity.java rename to briar-android/src/net/sf/briar/android/contact/ConversationActivity.java index 2dce8e6dd4dab80272fd550c24a4cd3f4c0222ca..a0fe1020c10af2ae34362ade21cf256ef2686ed8 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ConversationActivity.java @@ -1,4 +1,4 @@ -package net.sf.briar.android.messages; +package net.sf.briar.android.contact; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.View.GONE; diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java similarity index 97% rename from briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java rename to briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java index 863aa88c31e79a5e6fb3ad264748bf30dd042d09..3497efd1034bf429dae68880c20e01670909b5e1 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java +++ b/briar-android/src/net/sf/briar/android/contact/ConversationAdapter.java @@ -1,4 +1,4 @@ -package net.sf.briar.android.messages; +package net.sf.briar.android.contact; import static android.widget.LinearLayout.HORIZONTAL; import static java.text.DateFormat.SHORT; diff --git a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java similarity index 99% rename from briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java rename to briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java index c47b30603b174950f87c3b1ec4f4d6fc71366383..677b12db52346307ddd5d0b80b83488c713e225d 100644 --- a/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ReadPrivateMessageActivity.java @@ -1,4 +1,4 @@ -package net.sf.briar.android.messages; +package net.sf.briar.android.contact; import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_VERTICAL; diff --git a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java similarity index 97% rename from briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java rename to briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java index dfa935cc00afdcd6001f13d2169a22fa1c982d3c..51e6720860cd710d2459952189d2d7bec61763d0 100644 --- a/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/WritePrivateMessageActivity.java @@ -1,4 +1,4 @@ -package net.sf.briar.android.messages; +package net.sf.briar.android.contact; import static android.text.InputType.TYPE_CLASS_TEXT; import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; @@ -19,9 +19,6 @@ import java.util.logging.Logger; import javax.inject.Inject; import net.sf.briar.R; -import net.sf.briar.android.contact.ContactItem; -import net.sf.briar.android.contact.ContactItemComparator; -import net.sf.briar.android.contact.ContactSpinnerAdapter; import net.sf.briar.android.invitation.AddContactActivity; import net.sf.briar.android.util.HorizontalSpace; import net.sf.briar.api.AuthorId; diff --git a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java index e3d12fcc130889c5cc81aaeca6c55fa0b658cb6f..6241ba2f378dd0bb9c9d88f01a9f5dfcf7440d66 100644 --- a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java @@ -19,7 +19,6 @@ import javax.inject.Inject; import net.sf.briar.R; import net.sf.briar.android.contact.SelectContactsDialog; import net.sf.briar.android.invitation.AddContactActivity; -import net.sf.briar.android.messages.NoContactsDialog; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; diff --git a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java index b6d0dff45b06972ff468ec95f489d6bbcc0781f1..dd7bb39812fb381428c3d3d15ee869abd1323329 100644 --- a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java @@ -24,7 +24,6 @@ import javax.inject.Inject; import net.sf.briar.R; import net.sf.briar.android.contact.SelectContactsDialog; import net.sf.briar.android.invitation.AddContactActivity; -import net.sf.briar.android.messages.NoContactsDialog; import net.sf.briar.api.Contact; import net.sf.briar.api.ContactId; import net.sf.briar.api.android.DatabaseUiExecutor; diff --git a/briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java b/briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java similarity index 96% rename from briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java rename to briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java index ddef88e455e78a3f08f37b8b859d6f0e6becf659..dcd39facb81e4f1363b413b3d5da068379f2653d 100644 --- a/briar-android/src/net/sf/briar/android/messages/NoContactsDialog.java +++ b/briar-android/src/net/sf/briar/android/groups/NoContactsDialog.java @@ -1,4 +1,4 @@ -package net.sf.briar.android.messages; +package net.sf.briar.android.groups; import net.sf.briar.R; import android.app.AlertDialog; diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java deleted file mode 100644 index 719e33419bd1348a26617500fce4000640d01a8f..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java +++ /dev/null @@ -1,290 +0,0 @@ -package net.sf.briar.android.messages; - -import static android.view.Gravity.CENTER_HORIZONTAL; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static android.widget.LinearLayout.VERTICAL; -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static net.sf.briar.android.util.CommonLayoutParams.MATCH_MATCH; -import static net.sf.briar.android.util.CommonLayoutParams.MATCH_WRAP_1; - -import java.util.Collection; -import java.util.Comparator; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import net.sf.briar.R; -import net.sf.briar.android.invitation.AddContactActivity; -import net.sf.briar.android.util.HorizontalBorder; -import net.sf.briar.android.util.ListLoadingProgressBar; -import net.sf.briar.api.Contact; -import net.sf.briar.api.ContactId; -import net.sf.briar.api.android.DatabaseUiExecutor; -import net.sf.briar.api.db.DatabaseComponent; -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.MessageExpiredEvent; -import net.sf.briar.api.db.event.PrivateMessageAddedEvent; -import net.sf.briar.api.lifecycle.LifecycleManager; -import roboguice.activity.RoboFragmentActivity; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.ListView; - -public class ConversationListActivity extends RoboFragmentActivity -implements OnClickListener, DatabaseListener, NoContactsDialog.Listener { - - private static final Logger LOG = - Logger.getLogger(ConversationListActivity.class.getName()); - - private ConversationListAdapter adapter = null; - private ListView list = null; - private ListLoadingProgressBar loading = null; - private NoContactsDialog noContactsDialog = null; - - // Fields that are accessed from background threads must be volatile - @Inject private volatile DatabaseComponent db; - @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; - @Inject private volatile LifecycleManager lifecycleManager; - - @Override - public void onCreate(Bundle state) { - super.onCreate(state); - LinearLayout layout = new LinearLayout(this); - layout.setLayoutParams(MATCH_MATCH); - layout.setOrientation(VERTICAL); - layout.setGravity(CENTER_HORIZONTAL); - - adapter = new ConversationListAdapter(this); - list = new ListView(this); - // Give me all the width and all the unused height - list.setLayoutParams(MATCH_WRAP_1); - list.setAdapter(adapter); - list.setOnItemClickListener(adapter); - layout.addView(list); - - // Show a progress bar while the list is loading - list.setVisibility(GONE); - loading = new ListLoadingProgressBar(this); - layout.addView(loading); - - layout.addView(new HorizontalBorder(this)); - - ImageButton composeButton = new ImageButton(this); - composeButton.setBackgroundResource(0); - composeButton.setImageResource(R.drawable.content_new_email); - composeButton.setOnClickListener(this); - layout.addView(composeButton); - - setContentView(layout); - - FragmentManager fm = getSupportFragmentManager(); - Fragment f = fm.findFragmentByTag("NoContactsDialog"); - if(f == null) noContactsDialog = new NoContactsDialog(); - else noContactsDialog = (NoContactsDialog) f; - noContactsDialog.setListener(this); - } - - @Override - public void onResume() { - super.onResume(); - db.addListener(this); - loadHeaders(); - } - - private void loadHeaders() { - clearHeaders(); - dbUiExecutor.execute(new Runnable() { - public void run() { - try { - lifecycleManager.waitForDatabase(); - long now = System.currentTimeMillis(); - for(Contact c : db.getContacts()) { - try { - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(c.getId()); - displayHeaders(c, headers); - } catch(NoSuchContactException e) { - if(LOG.isLoggable(INFO)) - LOG.info("Contact removed"); - } - } - long duration = System.currentTimeMillis() - now; - if(LOG.isLoggable(INFO)) - LOG.info("Full load took " + duration + " ms"); - hideProgressBar(); - } 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 database"); - Thread.currentThread().interrupt(); - } - } - }); - } - - private void clearHeaders() { - runOnUiThread(new Runnable() { - public void run() { - list.setVisibility(GONE); - loading.setVisibility(VISIBLE); - adapter.clear(); - adapter.notifyDataSetChanged(); - } - }); - } - - private void displayHeaders(final Contact c, - final Collection<PrivateMessageHeader> headers) { - runOnUiThread(new Runnable() { - public void run() { - list.setVisibility(VISIBLE); - loading.setVisibility(GONE); - // Remove the old item, if any - ConversationListItem item = findConversation(c.getId()); - if(item != null) adapter.remove(item); - // Add a new item - adapter.add(new ConversationListItem(c, headers)); - adapter.sort(ItemComparator.INSTANCE); - adapter.notifyDataSetChanged(); - selectFirstUnread(); - } - }); - } - - private void hideProgressBar() { - runOnUiThread(new Runnable() { - public void run() { - list.setVisibility(VISIBLE); - loading.setVisibility(GONE); - } - }); - } - - 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 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 onPause() { - super.onPause(); - db.removeListener(this); - } - - public void onClick(View view) { - if(adapter.isEmpty()) { - FragmentManager fm = getSupportFragmentManager(); - noContactsDialog.show(fm, "NoContactsDialog"); - } else { - startActivity(new Intent(this, WritePrivateMessageActivity.class)); - } - } - - public void eventOccurred(DatabaseEvent e) { - if(e instanceof ContactRemovedEvent) { - // Reload the conversation, expecting NoSuchContactException - if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading"); - loadHeaders(((ContactRemovedEvent) e).getContactId()); - } else if(e instanceof MessageExpiredEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); - loadHeaders(); - } else if(e instanceof PrivateMessageAddedEvent) { - if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); - loadHeaders(((PrivateMessageAddedEvent) e).getContactId()); - } - } - - private void loadHeaders(final ContactId c) { - dbUiExecutor.execute(new Runnable() { - public void run() { - try { - lifecycleManager.waitForDatabase(); - long now = System.currentTimeMillis(); - Contact contact = db.getContact(c); - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(c); - long duration = System.currentTimeMillis() - now; - if(LOG.isLoggable(INFO)) - LOG.info("Partial load took " + duration + " ms"); - displayHeaders(contact, headers); - } catch(NoSuchContactException e) { - if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); - removeConversation(c); - } 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 database"); - Thread.currentThread().interrupt(); - } - } - }); - } - - private void removeConversation(final ContactId c) { - runOnUiThread(new Runnable() { - public void run() { - ConversationListItem item = findConversation(c); - if(item != null) { - adapter.remove(item); - selectFirstUnread(); - } - } - }); - } - - public void contactCreationSelected() { - startActivity(new Intent(this, AddContactActivity.class)); - } - - public void contactCreationCancelled() {} - - private static class ItemComparator - implements Comparator<ConversationListItem> { - - private static final ItemComparator INSTANCE = new ItemComparator(); - - 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; - // Break ties by contact name - String aName = a.getContactName(), bName = b.getContactName(); - return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); - } - } -} diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java deleted file mode 100644 index e9ce83be28407f81c6786c9f16045e8fc35bde5a..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.sf.briar.android.messages; - -import static android.widget.LinearLayout.HORIZONTAL; -import static java.text.DateFormat.SHORT; -import static net.sf.briar.android.util.CommonLayoutParams.WRAP_WRAP_1; - -import java.util.ArrayList; - -import net.sf.briar.R; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.text.format.DateUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.LinearLayout; -import android.widget.TextView; - -class ConversationListAdapter extends ArrayAdapter<ConversationListItem> -implements OnItemClickListener { - - 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(); - Resources res = ctx.getResources(); - - LinearLayout layout = new LinearLayout(ctx); - layout.setOrientation(HORIZONTAL); - if(item.getUnreadCount() > 0) - layout.setBackgroundColor(res.getColor(R.color.unread_background)); - - TextView name = new TextView(ctx); - // Give me all the unused width - name.setLayoutParams(WRAP_WRAP_1); - name.setTextSize(18); - name.setMaxLines(1); - name.setPadding(10, 10, 10, 10); - int unread = item.getUnreadCount(); - String contactName = item.getContactName(); - if(unread > 0) name.setText(contactName + " (" + unread + ")"); - else name.setText(contactName); - layout.addView(name); - - if(item.isEmpty()) { - TextView noMessages = new TextView(ctx); - noMessages.setTextSize(14); - noMessages.setPadding(10, 0, 10, 10); - noMessages.setTextColor(res.getColor(R.color.no_messages)); - noMessages.setText(R.string.no_messages); - layout.addView(noMessages); - } else { - TextView date = new TextView(ctx); - date.setTextSize(14); - date.setPadding(0, 10, 10, 10); - long then = item.getTimestamp(), now = System.currentTimeMillis(); - date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT)); - layout.addView(date); - } - - return layout; - } - - public void onItemClick(AdapterView<?> parent, View view, int position, - long id) { - ConversationListItem item = getItem(position); - Intent i = new Intent(getContext(), ConversationActivity.class); - i.putExtra("net.sf.briar.CONTACT_ID", item.getContactId().getInt()); - i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName()); - i.putExtra("net.sf.briar.LOCAL_AUTHOR_ID", - item.getLocalAuthorId().getBytes()); - getContext().startActivity(i); - } -} diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java deleted file mode 100644 index 231560a50af83a6d5123268d25d06ac26cd27e39..0000000000000000000000000000000000000000 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.sf.briar.android.messages; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import net.sf.briar.android.DescendingHeaderComparator; -import net.sf.briar.api.AuthorId; -import net.sf.briar.api.Contact; -import net.sf.briar.api.ContactId; -import net.sf.briar.api.db.PrivateMessageHeader; - -class ConversationListItem { - - private final Contact contact; - private final boolean empty; - private final long timestamp; - private final int unread; - - ConversationListItem(Contact contact, - Collection<PrivateMessageHeader> headers) { - this.contact = contact; - empty = headers.isEmpty(); - if(empty) { - timestamp = 0; - unread = 0; - } else { - List<PrivateMessageHeader> list = - new ArrayList<PrivateMessageHeader>(headers); - Collections.sort(list, DescendingHeaderComparator.INSTANCE); - timestamp = list.get(0).getTimestamp(); - int unread = 0; - for(PrivateMessageHeader h : list) if(!h.isRead()) unread++; - this.unread = unread; - } - } - - ContactId getContactId() { - return contact.getId(); - } - - String getContactName() { - return contact.getAuthor().getName(); - } - - AuthorId getLocalAuthorId() { - return contact.getLocalAuthorId(); - } - - boolean isEmpty() { - return empty; - } - - long getTimestamp() { - return timestamp; - } - - int getUnreadCount() { - return unread; - } -}