diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 0b0771da6e3cb758e532496672e95d226cf837cd..e402626affe6154e434dfd3d5c4bd8ce01cefbef 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -40,6 +40,22 @@ android:name=".android.contact.ContactListActivity" android:label="@string/contact_list_title" > </activity> + <activity + android:name=".android.groups.GroupActivity" + android:label="@string/groups_title" > + </activity> + <activity + android:name=".android.groups.GroupListActivity" + android:label="@string/groups_title" > + </activity> + <activity + android:name=".android.groups.ReadGroupMessageActivity" + android:label="@string/groups_title" > + </activity> + <activity + android:name=".android.groups.WriteGroupMessageActivity" + android:label="@string/compose_group_title" > + </activity> <activity android:name=".android.invitation.AddContactActivity" android:label="@string/add_contact_title" > @@ -53,12 +69,12 @@ android:label="@string/messages_title" > </activity> <activity - android:name=".android.messages.ReadMessageActivity" + android:name="net.sf.briar.android.messages.ReadPrivateMessageActivity" android:label="@string/messages_title" > </activity> <activity - android:name=".android.messages.WriteMessageActivity" - android:label="@string/compose_title" > + android:name="net.sf.briar.android.messages.WritePrivateMessageActivity" + android:label="@string/compose_message_title" > </activity> </application> </manifest> diff --git a/briar-android/res/drawable-hdpi/social_new_chat.png b/briar-android/res/drawable-hdpi/social_new_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..a6a42eeb6779836698a0e2c213955518d2284373 Binary files /dev/null and b/briar-android/res/drawable-hdpi/social_new_chat.png differ diff --git a/briar-android/res/drawable-hdpi/social_reply_all.png b/briar-android/res/drawable-hdpi/social_reply_all.png new file mode 100644 index 0000000000000000000000000000000000000000..377f6286ce9d88bcb8e7e4bc2b331d583f5cd6c3 Binary files /dev/null and b/briar-android/res/drawable-hdpi/social_reply_all.png differ diff --git a/briar-android/res/drawable-mdpi/social_new_chat.png b/briar-android/res/drawable-mdpi/social_new_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..e78580b8ec8bd72df2953c1cb27afbe201205552 Binary files /dev/null and b/briar-android/res/drawable-mdpi/social_new_chat.png differ diff --git a/briar-android/res/drawable-mdpi/social_reply_all.png b/briar-android/res/drawable-mdpi/social_reply_all.png new file mode 100644 index 0000000000000000000000000000000000000000..86334552edec456966f3b5bd4b9e00b73a180bd7 Binary files /dev/null and b/briar-android/res/drawable-mdpi/social_reply_all.png differ diff --git a/briar-android/res/drawable-xhdpi/social_new_chat.png b/briar-android/res/drawable-xhdpi/social_new_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5d9049252fe043a5b55b80cc8b5b8b8af37b3a Binary files /dev/null and b/briar-android/res/drawable-xhdpi/social_new_chat.png differ diff --git a/briar-android/res/drawable-xhdpi/social_reply_all.png b/briar-android/res/drawable-xhdpi/social_reply_all.png new file mode 100644 index 0000000000000000000000000000000000000000..f10a492c0229009a9b358cbd8ccae910ba21cdab Binary files /dev/null and b/briar-android/res/drawable-xhdpi/social_reply_all.png differ diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml index a55aadfa90727f7aedb1be06d8a627e684a43259..866b33e5869f3fc6a2201322d71a9cf620a7502f 100644 --- a/briar-android/res/values/color.xml +++ b/briar-android/res/values/color.xml @@ -1,4 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="HorizontalBorder">#CCCCCC</color> + <color name="horizontal_border">#CCCCCC</color> + <color name="anonymous_author">#999999</color> + <color name="pseudonymous_author">#000000</color> </resources> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 92486abf3bd87f4e6a8982c4ceb6909747448989..14258e04719804e036265ed5c8b05d5a2e558144 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -11,14 +11,13 @@ <string name="quit_button">Quit</string> <string name="contact_list_title">Contacts</string> <string name="contact_connected">Connected</string> - <string name="contact_last_connected">Last connected <br /> %1$s</string> - <string name="search_button">Search</string> + <string name="format_contact_last_connected">Last connected <br /> %1$s</string> <string name="add_contact_title">Add a Contact</string> <string name="same_network">Briar can add contacts via Wi-Fi or Bluetooth. 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> <string name="wifi_disabled">Wi-Fi is OFF</string> <string name="wifi_disconnected">Wi-Fi is DISCONNECTED</string> - <string name="wifi_connected">Wi-Fi is CONNECTED to %1$s</string> + <string name="format_wifi_connected">Wi-Fi is CONNECTED to %1$s</string> <string name="bluetooth_not_available">Bluetooth is not available on this device</string> <string name="bluetooth_disabled">Bluetooth is OFF</string> <string name="bluetooth_not_discoverable">Bluetooth is NOT DISCOVERABLE</string> @@ -26,7 +25,7 @@ <string name="continue_button">Continue</string> <string name="your_invitation_code">Your invitation code is</string> <string name="enter_invitation_code">Please enter your contact\'s invitation code:</string> - <string name="connecting_wifi">Connecting via %1$s\u2026</string> + <string name="format_connecting_wifi">Connecting via %1$s\u2026</string> <string name="connecting_bluetooth">Connecting via Bluetooth\u2026</string> <string name="connection_failed">Connection failed</string> <string name="check_same_network">Please check that you are both using the same network.</string> @@ -41,7 +40,11 @@ <string name="enter_nickname">Please enter a nickname for this contact:</string> <string name="done_button">Done</string> <string name="messages_title">Messages</string> - <string name="message_from">From: %1$s</string> - <string name="compose_title">New Message</string> - <string name="message_to">To:</string> + <string name="format_from">From: %1$s</string> + <string name="format_to">To: %1$s</string> + <string name="compose_message_title">New Message</string> + <string name="to">To:</string> + <string name="groups_title">Groups</string> + <string name="anonymous">(Anonymous)</string> + <string name="compose_group_title">New Post</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 090ceca84df471d1e5cf04c4e977bb575ed0205a..02cea07efb922402829581a0e8f09f40b23bfc2e 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.groups.GroupListActivity; import net.sf.briar.android.messages.ConversationListActivity; import net.sf.briar.android.widgets.CommonLayoutParams; import android.content.Intent; @@ -88,7 +89,8 @@ public class HomeScreenActivity extends BriarActivity { groupsButton.setText(R.string.groups_button); groupsButton.setOnClickListener(new OnClickListener() { public void onClick(View view) { - // FIXME: Hook this button up to an activity + startActivity(new Intent(HomeScreenActivity.this, + GroupListActivity.class)); } }); buttons.add(groupsButton); diff --git a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java index 6495bf631051e67b2e2f1d23f5e77da4844c8735..8e9207c6fc1731c4f0ef1e288d2c358885c1b13f 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactComparator.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactComparator.java @@ -7,7 +7,7 @@ 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.contact.getName(), - b.contact.getName()); + 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/ContactListAdapter.java b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java index 39cb91c6c1a97215f7e3aba48e7a1e5d73fbcdf0..bb10e399cf2f6de279036871782b87118bfdbdb6 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListAdapter.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import net.sf.briar.R; import net.sf.briar.android.widgets.CommonLayoutParams; import android.content.Context; -import android.content.res.Resources; import android.text.Html; import android.text.format.DateUtils; import android.view.View; @@ -46,8 +45,9 @@ implements OnItemClickListener { // Give me all the unused width name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); name.setTextSize(18); + name.setMaxLines(1); name.setPadding(0, 10, 10, 10); - name.setText(item.getName()); + name.setText(item.getContactName()); layout.addView(name); TextView connected = new TextView(ctx); @@ -56,8 +56,8 @@ implements OnItemClickListener { if(item.isConnected()) { connected.setText(R.string.contact_connected); } else { - Resources res = ctx.getResources(); - String format = res.getString(R.string.contact_last_connected); + String format = ctx.getResources().getString( + R.string.format_contact_last_connected); long then = item.getLastConnected(); CharSequence ago = DateUtils.getRelativeTimeSpanString(then); connected.setText(Html.fromHtml(String.format(format, ago))); 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 cba805e863df13b0d69393146c8559d9b6c836d2..0b030f2f96a4dad832dc1de2b73ea2e6649fba3f 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListItem.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListItem.java @@ -6,7 +6,7 @@ import net.sf.briar.api.ContactId; // This class is not thread-safe class ContactListItem { - final Contact contact; + private final Contact contact; private boolean connected; ContactListItem(Contact contact, boolean connected) { @@ -18,7 +18,7 @@ class ContactListItem { return contact.getId(); } - String getName() { + String getContactName() { return contact.getName(); } diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed287306acf942eaf298ed4440fb4441f9806ee --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java @@ -0,0 +1,226 @@ +package net.sf.briar.android.groups; + +import static android.view.Gravity.CENTER_HORIZONTAL; +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.concurrent.Executor; +import java.util.logging.Logger; + +import net.sf.briar.R; +import net.sf.briar.android.AscendingHeaderComparator; +import net.sf.briar.android.BriarActivity; +import net.sf.briar.android.BriarService; +import net.sf.briar.android.BriarService.BriarServiceConnection; +import net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalBorder; +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.GroupMessageHeader; +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.SubscriptionAddedEvent; +import net.sf.briar.api.db.event.SubscriptionRemovedEvent; +import net.sf.briar.api.messaging.Author; +import net.sf.briar.api.messaging.GroupId; +import android.content.Intent; +import android.os.Bundle; +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; + +import com.google.inject.Inject; + +public class GroupActivity extends BriarActivity implements DatabaseListener, +OnClickListener, OnItemClickListener { + + private static final Logger LOG = + Logger.getLogger(GroupActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + new BriarServiceConnection(); + + @Inject private DatabaseComponent db; + @Inject @DatabaseExecutor private Executor dbExecutor; + + private GroupId groupId = null; + private String groupName = null; + private GroupAdapter adapter = null; + private ListView list = null; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + + Intent i = getIntent(); + byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); + if(id == null) throw new IllegalStateException(); + groupId = new GroupId(id); + groupName = i.getStringExtra("net.sf.briar.GROUP_NAME"); + if(groupName == null) throw new IllegalStateException(); + setTitle(groupName); + + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH); + layout.setOrientation(VERTICAL); + layout.setGravity(CENTER_HORIZONTAL); + + adapter = new GroupAdapter(this); + list = new ListView(this); + // Give me all the width and all the unused height + list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); + list.setAdapter(adapter); + list.setOnItemClickListener(this); + layout.addView(list); + + 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); + + // Listen for messages and groups 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); + } + + @Override + public void onResume() { + super.onResume(); + reloadMessageHeaders(); + } + + private void reloadMessageHeaders() { + final DatabaseComponent db = this.db; + final GroupId groupId = this.groupId; + 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 + Collection<GroupMessageHeader> headers = + db.getMessageHeaders(groupId); + if(LOG.isLoggable(INFO)) + LOG.info("Loaded " + headers.size() + " headers"); + // Update the conversation + updateConversation(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 updateConversation( + 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; + adapter.clear(); + for(GroupMessageHeader h : sort) { + if(firstUnread == -1 && !h.isRead()) + firstUnread = adapter.getCount(); + adapter.add(h); + } + if(firstUnread == -1) list.setSelection(adapter.getCount() - 1); + else list.setSelection(firstUnread); + } + }); + } + + @Override + public void onDestroy() { + super.onDestroy(); + db.removeListener(this); + unbindService(serviceConnection); + } + + public void eventOccurred(DatabaseEvent e) { + 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(); + } else if(e instanceof SubscriptionAddedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); + reloadMessageHeaders(); + } else if(e instanceof SubscriptionRemovedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + reloadMessageHeaders(); + } + } + + 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); + } + + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + showMessage(position); + } + + private void showMessage(int 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); + i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes()); + Author author = item.getAuthor(); + if(author == null) { + i.putExtra("net.sf.briar.ANONYMOUS", true); + } else { + i.putExtra("net.sf.briar.ANONYMOUS", false); + i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes()); + i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName()); + } + i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType()); + i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp()); + i.putExtra("net.sf.briar.FIRST", position == 0); + 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 new file mode 100644 index 0000000000000000000000000000000000000000..17915c8d45abe00087b8e98784b6a36b26c4e0a6 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupAdapter.java @@ -0,0 +1,92 @@ +package net.sf.briar.android.groups; + +import static android.graphics.Typeface.BOLD; +import static android.view.Gravity.CENTER_VERTICAL; +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 net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalSpace; +import net.sf.briar.api.db.GroupMessageHeader; +import net.sf.briar.api.messaging.Author; +import android.content.Context; +import android.content.res.Resources; +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.TextView; + +class GroupAdapter extends ArrayAdapter<GroupMessageHeader> { + + GroupAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<GroupMessageHeader>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + GroupMessageHeader item = getItem(position); + Context ctx = getContext(); + // FIXME: Use a RelativeLayout + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(HORIZONTAL); + layout.setGravity(CENTER_VERTICAL); + + LinearLayout innerLayout = new LinearLayout(ctx); + // Give me all the unused width + innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); + innerLayout.setOrientation(VERTICAL); + + Author author = item.getAuthor(); + + TextView name = new TextView(ctx); + name.setTextSize(18); + name.setMaxLines(1); + name.setPadding(10, 10, 10, 10); + Resources res = ctx.getResources(); + if(author == null) { + name.setTextColor(res.getColor(R.color.anonymous_author)); + name.setText(R.string.anonymous); + } else { + name.setTextColor(res.getColor(R.color.pseudonymous_author)); + name.setText(author.getName()); + } + innerLayout.addView(name); + + if(item.getContentType().equals("text/plain")) { + TextView subject = new TextView(ctx); + subject.setTextSize(14); + subject.setMaxLines(2); + subject.setPadding(10, 0, 10, 10); + if(!item.isRead()) subject.setTypeface(null, BOLD); + subject.setText(item.getSubject()); + innerLayout.addView(subject); + } else { + LinearLayout innerInnerLayout = new LinearLayout(ctx); + innerInnerLayout.setOrientation(HORIZONTAL); + ImageView attachment = new ImageView(ctx); + attachment.setPadding(10, 0, 10, 10); + attachment.setImageResource(R.drawable.content_attachment); + innerInnerLayout.addView(attachment); + innerInnerLayout.addView(new HorizontalSpace(ctx)); + innerLayout.addView(innerInnerLayout); + } + layout.addView(innerLayout); + + 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; + } +} diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..5c0c3beecf8e5b9c99e5ee571c86e6d9ea816035 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -0,0 +1,302 @@ +package net.sf.briar.android.groups; + +import static android.view.Gravity.CENTER_HORIZONTAL; +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.security.KeyPair; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +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.BriarServiceConnection; +import net.sf.briar.android.DescendingHeaderComparator; +import net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalBorder; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.CryptoComponent; +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.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.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.Message; +import net.sf.briar.api.messaging.MessageFactory; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ListView; + +import com.google.inject.Inject; + +public class GroupListActivity extends BriarActivity +implements OnClickListener, DatabaseListener { + + private static final Logger LOG = + Logger.getLogger(GroupListActivity.class.getName()); + + 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; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH); + layout.setOrientation(VERTICAL); + layout.setGravity(CENTER_HORIZONTAL); + + adapter = new GroupListAdapter(this); + ListView list = new ListView(this); + // Give me all the width and all the unused height + list.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); + list.setAdapter(adapter); + list.setOnItemClickListener(adapter); + layout.addView(list); + + layout.addView(new HorizontalBorder(this)); + + ImageButton newGroupButton = new ImageButton(this); + newGroupButton.setBackgroundResource(0); + newGroupButton.setImageResource(R.drawable.social_new_chat); + newGroupButton.setOnClickListener(this); + layout.addView(newGroupButton); + + setContentView(layout); + + // Listen for messages and groups 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); + + // Add some fake messages to the database in a background thread + insertFakeMessages(); + } + + // 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 { + // Wait for the service to be bound and started + serviceConnection.waitForStartup(); + // If there are no groups in the DB, create some fake ones + Collection<Group> groups = db.getSubscriptions(); + if(!groups.isEmpty()) return; + if(LOG.isLoggable(INFO)) + LOG.info("Inserting fake groups and messages"); + // We'll also need a contact to receive messages from + ContactId contactId = db.addContact("Dave"); + // Finally, we'll need some authors for the messages + KeyPair keyPair = crypto.generateSignatureKeyPair(); + byte[] publicKey = keyPair.getPublic().getEncoded(); + PrivateKey privateKey = keyPair.getPrivate(); + Author author = authorFactory.createAuthor("Batman", + publicKey); + Author author1 = authorFactory.createAuthor("Duckman", + publicKey); + // Insert some fake groups and make them visible + Group group = groupFactory.createGroup("DisneyLeaks"); + db.subscribe(group); + db.setVisibility(group.getId(), Arrays.asList(contactId)); + Group group1 = groupFactory.createGroup("Godwin's Lore"); + db.subscribe(group1); + db.setVisibility(group1.getId(), Arrays.asList(contactId)); + // Insert some text messages to the groups + for(int i = 0; i < 20; i++) { + String body; + if(i % 3 == 0) { + body = "Message " + i + " is short."; + } else { + body = "Message " + i + " is long enough to wrap" + + " onto a second line on some screens."; + } + Group g = i % 2 == 0 ? group : group1; + Message m; + if(i % 5 == 0) { + m = messageFactory.createAnonymousMessage(null, g, + "text/plain", body.getBytes("UTF-8")); + } else if(i % 5 == 2) { + m = messageFactory.createPseudonymousMessage(null, + g, author, privateKey, "text/plain", + body.getBytes("UTF-8")); + } else { + m = messageFactory.createPseudonymousMessage(null, + g, author1, privateKey, "text/plain", + body.getBytes("UTF-8")); + } + if(Math.random() < 0.5) db.addLocalGroupMessage(m); + else db.receiveMessage(contactId, m); + db.setReadFlag(m.getId(), i % 4 == 0); + } + // Insert a non-text message + Message m = messageFactory.createAnonymousMessage(null, + group, "image/jpeg", new byte[1000]); + db.receiveMessage(contactId, m); + // Insert a long text message + StringBuilder s = new StringBuilder(); + for(int i = 0; i < 100; i++) + s.append("This is a very tedious message. "); + String body = s.toString(); + m = messageFactory.createAnonymousMessage(m.getId(), + group1, "text/plain", body.getBytes("UTF-8")); + db.addLocalGroupMessage(m); + } 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 onResume() { + super.onResume(); + reloadGroupList(); + } + + private void reloadGroupList() { + final DatabaseComponent db = this.db; + dbExecutor.execute(new Runnable() { + public void run() { + try { + // Wait for the service to be bound and started + serviceConnection.waitForStartup(); + // Load the groups and message headers from the DB + if(LOG.isLoggable(INFO)) LOG.info("Loading groups"); + Collection<Group> groups = db.getSubscriptions(); + if(LOG.isLoggable(INFO)) + LOG.info("Loaded " + groups.size() + " groups"); + List<GroupListItem> items = new ArrayList<GroupListItem>(); + for(Group g : groups) { + // Filter out restricted groups + if(g.getPublicKey() != null) continue; + Collection<GroupMessageHeader> headers; + try { + headers = db.getMessageHeaders(g.getId()); + } catch(NoSuchSubscriptionException e) { + // We'll reload the list when we get the event + continue; + } + 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)); + } 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 GroupListItem createItem(Group group, + Collection<GroupMessageHeader> headers) { + List<GroupMessageHeader> sort = + new ArrayList<GroupMessageHeader>(headers); + Collections.sort(sort, DescendingHeaderComparator.INSTANCE); + return new GroupListItem(group, sort); + } + + private void updateGroupList(final Collection<GroupListItem> items) { + runOnUiThread(new Runnable() { + public void run() { + adapter.clear(); + for(GroupListItem i : items) adapter.add(i); + adapter.sort(GroupComparator.INSTANCE); + } + }); + } + + @Override + public void onDestroy() { + super.onDestroy(); + db.removeListener(this); + unbindService(serviceConnection); + } + + public void onClick(View view) { + startActivity(new Intent(this, WriteGroupMessageActivity.class)); + } + + public void eventOccurred(DatabaseEvent e) { + if(e instanceof MessageAddedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading"); + reloadGroupList(); + } 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(); + } else if(e instanceof SubscriptionRemovedEvent) { + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + reloadGroupList(); + } + } + + private static class GroupComparator implements Comparator<GroupListItem> { + + private static final GroupComparator INSTANCE = new GroupComparator(); + + public int compare(GroupListItem a, GroupListItem b) { + return String.CASE_INSENSITIVE_ORDER.compare(a.getGroupName(), + b.getGroupName()); + } + } +} \ No newline at end of file diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..102a93a06ca5a65a45afa400c29f7bb0c7ae965b --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java @@ -0,0 +1,85 @@ +package net.sf.briar.android.groups; + +import static android.graphics.Typeface.BOLD; +import static android.view.Gravity.CENTER_VERTICAL; +import static android.view.Gravity.LEFT; +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.android.widgets.CommonLayoutParams; +import net.sf.briar.util.StringUtils; +import android.content.Context; +import android.content.Intent; +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 GroupListAdapter extends ArrayAdapter<GroupListItem> +implements OnItemClickListener { + + GroupListAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<GroupListItem>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + GroupListItem item = getItem(position); + Context ctx = getContext(); + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(HORIZONTAL); + layout.setGravity(CENTER_VERTICAL); + + LinearLayout innerLayout = new LinearLayout(ctx); + // Give me all the unused width + innerLayout.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); + innerLayout.setOrientation(VERTICAL); + innerLayout.setGravity(LEFT); + + TextView name = new TextView(ctx); + name.setTextSize(18); + name.setMaxLines(1); + name.setPadding(10, 10, 10, 10); + int unread = item.getUnreadCount(); + if(unread > 0) name.setText(item.getGroupName() + " (" + unread + ")"); + else name.setText(item.getGroupName()); + innerLayout.addView(name); + + if(!StringUtils.isNullOrEmpty(item.getSubject())) { + TextView subject = new TextView(ctx); + subject.setTextSize(14); + subject.setMaxLines(2); + subject.setPadding(10, 0, 10, 10); + if(unread > 0) subject.setTypeface(null, BOLD); + subject.setText(item.getSubject()); + innerLayout.addView(subject); + } + layout.addView(innerLayout); + + 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) { + GroupListItem item = getItem(position); + Intent i = new Intent(getContext(), GroupActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName()); + getContext().startActivity(i); + } +} diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java new file mode 100644 index 0000000000000000000000000000000000000000..38a4513c21cb24679762f1373a093e72e00feab5 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java @@ -0,0 +1,57 @@ +package net.sf.briar.android.groups; + +import java.util.Collections; +import java.util.List; + +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; + +class GroupListItem { + + private final Group group; + private final String author, subject; + private final long timestamp; + private final int unread; + + GroupListItem(Group group, List<GroupMessageHeader> headers) { + if(headers.isEmpty()) throw new IllegalArgumentException(); + this.group = group; + Collections.sort(headers, DescendingHeaderComparator.INSTANCE); + GroupMessageHeader newest = headers.get(0); + Author a = newest.getAuthor(); + if(a == null) author = null; + else author = a.getName(); + subject = newest.getSubject(); + timestamp = newest.getTimestamp(); + int unread = 0; + for(GroupMessageHeader h : headers) if(!h.isRead()) unread++; + this.unread = unread; + } + + GroupId getGroupId() { + return group.getId(); + } + + String getGroupName() { + return group.getName(); + } + + String getAuthorName() { + return author; + } + + String getSubject() { + return subject; + } + + long getTimestamp() { + return timestamp; + } + + int getUnreadCount() { + return unread; + } +} diff --git a/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..2c16ebfaf89cb84af0d66b55fcfa04211c3eb9dc --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/GroupNameSpinnerAdapter.java @@ -0,0 +1,36 @@ +package net.sf.briar.android.groups; + +import java.util.ArrayList; + +import net.sf.briar.api.messaging.Group; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +class GroupNameSpinnerAdapter extends ArrayAdapter<Group> +implements SpinnerAdapter { + + GroupNameSpinnerAdapter(Context context) { + super(context, android.R.layout.simple_spinner_item, + new ArrayList<Group>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView name = new TextView(getContext()); + name.setTextSize(18); + name.setMaxLines(1); + name.setPadding(10, 10, 10, 10); + name.setText(getItem(position).getName()); + return name; + } + + @Override + public View getDropDownView(int position, View convertView, + ViewGroup parent) { + return getView(position, convertView, parent); + } +} diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..d5750019ba58a3137df5a79cc18ddbb119621a9a --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java @@ -0,0 +1,288 @@ +package net.sf.briar.android.groups; + +import static android.view.Gravity.CENTER; +import static android.view.Gravity.CENTER_VERTICAL; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; +import static java.text.DateFormat.SHORT; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +import java.io.UnsupportedEncodingException; +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.BriarServiceConnection; +import net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalBorder; +import net.sf.briar.android.widgets.HorizontalSpace; +import net.sf.briar.api.android.BundleEncrypter; +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.messaging.AuthorId; +import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.MessageId; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.google.inject.Inject; + +public class ReadGroupMessageActivity extends BriarActivity +implements OnClickListener { + + static final int RESULT_REPLY = RESULT_FIRST_USER; + static final int RESULT_PREV = RESULT_FIRST_USER + 1; + static final int RESULT_NEXT = RESULT_FIRST_USER + 2; + + private static final Logger LOG = + Logger.getLogger(ReadGroupMessageActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + 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 boolean read; + private ImageButton readButton = null, prevButton = null, nextButton = null; + private ImageButton replyButton = null; + private TextView content = null; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + + Intent i = getIntent(); + byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); + if(id == null) throw new IllegalStateException(); + groupId = new GroupId(id); + String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME"); + if(groupName == null) throw new IllegalStateException(); + setTitle(groupName); + id = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID"); + if(id == null) throw new IllegalStateException(); + messageId = new MessageId(id); + boolean anonymous = i.getBooleanExtra("net.sf.briar.ANONYMOUS", false); + if(!anonymous) { + id = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID"); + if(id == null) throw new IllegalStateException(); + authorId = new AuthorId(id); + authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME"); + if(authorName == null) throw new IllegalStateException(); + } + String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE"); + if(contentType == null) throw new IllegalStateException(); + long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1); + if(timestamp == -1) throw new IllegalStateException(); + boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false); + boolean last = i.getBooleanExtra("net.sf.briar.LAST", false); + + if(state != null && bundleEncrypter.decrypt(state)) { + read = state.getBoolean("net.sf.briar.READ"); + } else { + read = false; + setReadInDatabase(true); + } + + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + layout.setOrientation(VERTICAL); + + ScrollView scrollView = new ScrollView(this); + // Give me all the width and all the unused height + scrollView.setLayoutParams(CommonLayoutParams.MATCH_WRAP_1); + + LinearLayout message = new LinearLayout(this); + message.setOrientation(VERTICAL); + + LinearLayout header = new LinearLayout(this); + header.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + header.setOrientation(HORIZONTAL); + header.setGravity(CENTER_VERTICAL); + + TextView author = new TextView(this); + // Give me all the unused width + author.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); + author.setTextSize(18); + author.setMaxLines(1); + author.setPadding(10, 10, 10, 10); + Resources res = getResources(); + if(authorName == null) { + author.setTextColor(res.getColor(R.color.anonymous_author)); + author.setText(R.string.anonymous); + } else { + author.setTextColor(res.getColor(R.color.pseudonymous_author)); + author.setText(authorName); + } + header.addView(author); + + TextView date = new TextView(this); + date.setTextSize(14); + date.setPadding(0, 10, 10, 10); + long now = System.currentTimeMillis(); + date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT)); + header.addView(date); + message.addView(header); + + if(contentType.equals("text/plain")) { + // Load and display the message body + content = new TextView(this); + content.setPadding(10, 0, 10, 10); + message.addView(content); + loadMessageBody(); + } + scrollView.addView(message); + layout.addView(scrollView); + + layout.addView(new HorizontalBorder(this)); + + LinearLayout footer = new LinearLayout(this); + footer.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + footer.setOrientation(HORIZONTAL); + footer.setGravity(CENTER); + + readButton = new ImageButton(this); + readButton.setBackgroundResource(0); + if(read) readButton.setImageResource(R.drawable.content_unread); + else readButton.setImageResource(R.drawable.content_read); + readButton.setOnClickListener(this); + footer.addView(readButton); + footer.addView(new HorizontalSpace(this)); + + prevButton = new ImageButton(this); + prevButton.setBackgroundResource(0); + prevButton.setImageResource(R.drawable.navigation_previous_item); + prevButton.setOnClickListener(this); + prevButton.setEnabled(!first); + footer.addView(prevButton); + footer.addView(new HorizontalSpace(this)); + + nextButton = new ImageButton(this); + nextButton.setBackgroundResource(0); + nextButton.setImageResource(R.drawable.navigation_next_item); + nextButton.setOnClickListener(this); + nextButton.setEnabled(!last); + footer.addView(nextButton); + footer.addView(new HorizontalSpace(this)); + + replyButton = new ImageButton(this); + replyButton.setBackgroundResource(0); + replyButton.setImageResource(R.drawable.social_reply_all); + replyButton.setOnClickListener(this); + footer.addView(replyButton); + layout.addView(footer); + + setContentView(layout); + + // Bind to the service so we can wait for the DB to be opened + bindService(new Intent(BriarService.class.getName()), + serviceConnection, 0); + } + + private void setReadInDatabase(final boolean read) { + final DatabaseComponent db = this.db; + final MessageId messageId = this.messageId; + dbExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + db.setReadFlag(messageId, read); + setReadInUi(read); + } 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 setReadInUi(final boolean read) { + runOnUiThread(new Runnable() { + public void run() { + ReadGroupMessageActivity.this.read = read; + if(read) readButton.setImageResource(R.drawable.content_unread); + else readButton.setImageResource(R.drawable.content_read); + } + }); + } + + private void loadMessageBody() { + final DatabaseComponent db = this.db; + final MessageId messageId = this.messageId; + dbExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + byte[] body = db.getMessageBody(messageId); + final String text = new String(body, "UTF-8"); + runOnUiThread(new Runnable() { + public void run() { + content.setText(text); + } + }); + } 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(); + } catch(UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + }); + } + + @Override + public void onSaveInstanceState(Bundle state) { + state.putBoolean("net.sf.briar.READ", read); + bundleEncrypter.encrypt(state); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unbindService(serviceConnection); + } + + public void onClick(View view) { + if(view == readButton) { + setReadInDatabase(!read); + } else if(view == prevButton) { + setResult(RESULT_PREV); + finish(); + } else if(view == nextButton) { + setResult(RESULT_NEXT); + finish(); + } else if(view == replyButton) { + Intent i = new Intent(this, WriteGroupMessageActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes()); + i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes()); + startActivity(i); + setResult(RESULT_REPLY); + finish(); + } + } +} diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..7a1a93c06f3ac3cb5ad32e72d014c289a2877988 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java @@ -0,0 +1,217 @@ +package net.sf.briar.android.groups; + +import static android.view.Gravity.CENTER_VERTICAL; +import static android.widget.LinearLayout.HORIZONTAL; +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.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.util.Collection; +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.BriarServiceConnection; +import net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalSpace; +import net.sf.briar.api.android.BundleEncrypter; +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.messaging.Group; +import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.Message; +import net.sf.briar.api.messaging.MessageFactory; +import net.sf.briar.api.messaging.MessageId; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import com.google.inject.Inject; + +public class WriteGroupMessageActivity extends BriarActivity +implements OnClickListener, OnItemSelectedListener { + + private static final Logger LOG = + Logger.getLogger(WriteGroupMessageActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + 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; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + + Intent i = getIntent(); + byte[] id = i.getByteArrayExtra("net.sf.briar.GROUP_ID"); + if(id != null) groupId = new GroupId(id); + id = i.getByteArrayExtra("net.sf.briar.PARENT_ID"); + if(id != null) parentId = new MessageId(id); + + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + layout.setOrientation(VERTICAL); + + LinearLayout actionBar = new LinearLayout(this); + actionBar.setLayoutParams(CommonLayoutParams.MATCH_WRAP); + actionBar.setOrientation(HORIZONTAL); + actionBar.setGravity(CENTER_VERTICAL); + + TextView to = new TextView(this); + to.setTextSize(18); + to.setPadding(10, 10, 10, 10); + to.setText(R.string.to); + actionBar.addView(to); + + adapter = new GroupNameSpinnerAdapter(this); + spinner = new Spinner(this); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(this); + loadContactNames(); + actionBar.addView(spinner); + + actionBar.addView(new HorizontalSpace(this)); + + sendButton = new ImageButton(this); + sendButton.setBackgroundResource(0); + sendButton.setImageResource(R.drawable.social_send_now); + sendButton.setEnabled(false); + sendButton.setOnClickListener(this); + actionBar.addView(sendButton); + layout.addView(actionBar); + + content = new EditText(this); + content.setPadding(10, 10, 10, 10); + if(state != null && bundleEncrypter.decrypt(state)) { + Parcelable p = state.getParcelable("net.sf.briar.CONTENT"); + if(p != null) content.onRestoreInstanceState(p); + } + layout.addView(content); + + setContentView(layout); + + // Bind to the service so we can wait for the DB to be opened + bindService(new Intent(BriarService.class.getName()), + serviceConnection, 0); + } + + private void loadContactNames() { + final DatabaseComponent db = this.db; + 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); + } + } + }); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + } + }); + } + + @Override + public void onSaveInstanceState(Bundle state) { + Parcelable p = content.onSaveInstanceState(); + state.putParcelable("net.sf.briar.CONTENT", p); + bundleEncrypter.encrypt(state); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unbindService(serviceConnection); + } + + public void onClick(View view) { + if(group == null) throw new IllegalStateException(); + try { + storeMessage(content.getText().toString().getBytes("UTF-8")); + } catch(UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + finish(); + } + + 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 { + serviceConnection.waitForStartup(); + Message m = messageFactory.createAnonymousMessage(parentId, + group, "text/plain", body); + db.addLocalGroupMessage(m); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(GeneralSecurityException e) { + throw new RuntimeException(e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + }); + } + + public void onItemSelected(AdapterView<?> parent, View view, int position, + long id) { + group = adapter.getItem(position); + groupId = group.getId(); + sendButton.setEnabled(true); + } + + public void onNothingSelected(AdapterView<?> parent) { + group = null; + groupId = null; + sendButton.setEnabled(false); + } +} diff --git a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java index 90be870c03b0b52cfc1695aee2659c5cfcd96ab8..9daf3287f5e1c0b85eafbee965442a2f57569c4f 100644 --- a/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java +++ b/briar-android/src/net/sf/briar/android/invitation/ConnectionView.java @@ -4,7 +4,6 @@ import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_HORIZONTAL; import net.sf.briar.R; import android.content.Context; -import android.content.res.Resources; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -45,9 +44,9 @@ public class ConnectionView extends AddContactView { innerLayout.addView(progress); TextView connecting = new TextView(ctx); - Resources res = getResources(); - String connectingVia = res.getString(R.string.connecting_wifi); - connecting.setText(String.format(connectingVia, networkName)); + String format = getResources().getString( + R.string.format_connecting_wifi); + connecting.setText(String.format(format, networkName)); innerLayout.addView(connecting); addView(innerLayout); diff --git a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java index 1305acc56607cb4db88a4d198bc179099f1ffcef..8cb63950fe9dbcc84268aabeb8768fb7512cd602 100644 --- a/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java +++ b/briar-android/src/net/sf/briar/android/invitation/WifiWidget.java @@ -7,7 +7,6 @@ import net.sf.briar.R; import net.sf.briar.android.widgets.CommonLayoutParams; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.view.View; @@ -70,9 +69,9 @@ public class WifiWidget extends LinearLayout implements OnClickListener { ok.setImageResource(R.drawable.navigation_accept); ok.setPadding(10, 10, 10, 10); addView(ok); - Resources res = getResources(); - String connected = res.getString(R.string.wifi_connected); - status.setText(String.format(connected, networkName)); + String format = getResources().getString( + R.string.format_wifi_connected); + status.setText(String.format(format, networkName)); addView(status); ImageButton settings = new ImageButton(ctx); settings.setImageResource(R.drawable.action_settings); diff --git a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java index 168d7aa81ee78d694e55999c3cd80df44597b5a9..18c4d00e7da665af939054442d5b261cd63906b7 100644 --- a/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java +++ b/briar-android/src/net/sf/briar/android/messages/ContactNameSpinnerAdapter.java @@ -22,6 +22,7 @@ implements SpinnerAdapter { public View getView(int position, View convertView, ViewGroup parent) { TextView name = new TextView(getContext()); name.setTextSize(18); + name.setMaxLines(1); name.setPadding(10, 10, 10, 10); name.setText(getItem(position).getName()); return name; 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 60d3869012118d56ea94b15e01cb3791dbb598b5..b382e920ee2553ed4477be64aab9c25065a2ef3f 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java @@ -67,6 +67,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { contactId = new ContactId(id); contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME"); if(contactName == null) throw new IllegalStateException(); + setTitle(contactName); LinearLayout layout = new LinearLayout(this); layout.setLayoutParams(CommonLayoutParams.MATCH_MATCH); @@ -104,23 +105,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { reloadMessageHeaders(); } - @Override - public void onDestroy() { - super.onDestroy(); - db.removeListener(this); - unbindService(serviceConnection); - } - - public void eventOccurred(DatabaseEvent e) { - 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(); - } - } - private void reloadMessageHeaders() { final DatabaseComponent db = this.db; final ContactId contactId = this.contactId; @@ -168,8 +152,38 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { }); } + @Override + public void onActivityResult(int request, int result, Intent data) { + if(result == ReadPrivateMessageActivity.RESULT_PREV) { + int position = request - 1; + if(position >= 0 && position < adapter.getCount()) + showMessage(position); + } else if(result == ReadPrivateMessageActivity.RESULT_NEXT) { + int position = request + 1; + if(position >= 0 && position < adapter.getCount()) + showMessage(position); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + db.removeListener(this); + unbindService(serviceConnection); + } + + public void eventOccurred(DatabaseEvent e) { + 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(); + } + } + public void onClick(View view) { - Intent i = new Intent(this, WriteMessageActivity.class); + Intent i = new Intent(this, WritePrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); startActivity(i); } @@ -181,28 +195,15 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { private void showMessage(int position) { PrivateMessageHeader item = adapter.getItem(position); - Intent i = new Intent(this, ReadMessageActivity.class); + Intent i = new Intent(this, ReadPrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); i.putExtra("net.sf.briar.CONTACT_NAME", contactName); i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes()); i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType()); i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp()); + i.putExtra("net.sf.briar.INCOMING", item.isIncoming()); i.putExtra("net.sf.briar.FIRST", position == 0); i.putExtra("net.sf.briar.LAST", position == adapter.getCount() - 1); - i.putExtra("net.sf.briar.STARRED", item.isStarred()); startActivityForResult(i, position); } - - @Override - public void onActivityResult(int request, int result, Intent data) { - if(result == ReadMessageActivity.RESULT_PREV) { - int position = request - 1; - if(position >= 0 && position < adapter.getCount()) - showMessage(position); - } else if(result == ReadMessageActivity.RESULT_NEXT) { - int position = request + 1; - if(position >= 0 && position < adapter.getCount()) - showMessage(position); - } - } } diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java index 056935ad82664426016e213e9636c8ed0863245e..40e9626dbc1ad1fc3033b1877d55c894cc90b842 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import net.sf.briar.R; import net.sf.briar.android.widgets.CommonLayoutParams; +import net.sf.briar.android.widgets.HorizontalSpace; import net.sf.briar.api.db.PrivateMessageHeader; import android.content.Context; import android.text.format.DateUtils; @@ -34,23 +35,24 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> { layout.setOrientation(HORIZONTAL); layout.setGravity(CENTER_VERTICAL); - if(!item.getContentType().equals("text/plain")) { + if(item.getContentType().equals("text/plain")) { + TextView subject = new TextView(ctx); + // Give me all the unused width + subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); + subject.setTextSize(14); + subject.setMaxLines(2); + subject.setPadding(10, 10, 10, 10); + if(!item.isRead()) subject.setTypeface(null, BOLD); + subject.setText(item.getSubject()); + layout.addView(subject); + } else { ImageView attachment = new ImageView(ctx); attachment.setPadding(10, 10, 10, 10); attachment.setImageResource(R.drawable.content_attachment); layout.addView(attachment); + layout.addView(new HorizontalSpace(ctx)); } - TextView subject = new TextView(ctx); - // Give me all the unused width - subject.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); - subject.setTextSize(14); - subject.setMaxLines(2); - subject.setPadding(10, 10, 10, 10); - if(!item.isRead()) subject.setTypeface(null, BOLD); - subject.setText(item.getSubject()); - layout.addView(subject); - TextView date = new TextView(ctx); date.setTextSize(14); date.setPadding(0, 10, 10, 10); 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 561805748359773da5c4469758f759255503f290..5eafcf3367303550aabe2763b095eb8e5155d700 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java @@ -238,7 +238,7 @@ implements OnClickListener, DatabaseListener { } public void onClick(View view) { - startActivity(new Intent(this, WriteMessageActivity.class)); + startActivity(new Intent(this, WritePrivateMessageActivity.class)); } public void eventOccurred(DatabaseEvent e) { diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java index 542a14d52e173b8c7d4b8fe7d97916167dca44b8..19066131bc3c28a1807feaef81941492cbf6f19a 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListAdapter.java @@ -46,10 +46,12 @@ implements OnItemClickListener { TextView name = new TextView(ctx); name.setTextSize(18); + name.setMaxLines(1); name.setPadding(10, 10, 10, 10); int unread = item.getUnreadCount(); - if(unread > 0) name.setText(item.getName() + " (" + unread + ")"); - else name.setText(item.getName()); + String contactName = item.getContactName(); + if(unread > 0) name.setText(contactName + " (" + unread + ")"); + else name.setText(contactName); innerLayout.addView(name); if(!StringUtils.isNullOrEmpty(item.getSubject())) { @@ -78,7 +80,7 @@ implements OnItemClickListener { 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.getName()); + i.putExtra("net.sf.briar.CONTACT_NAME", item.getContactName()); 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 index b168f7a9bce3f5b0a494efefc04185dfdee5b473..eec30a91c80947d7cd4142db16e009f7c4c50991 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListItem.java @@ -30,7 +30,7 @@ class ConversationListItem { return contact.getId(); } - String getName() { + String getContactName() { return contact.getName(); } diff --git a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java similarity index 90% rename from briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java rename to briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java index 04185e78f8bc985dd141960f6c822fb3cbc2ae36..afe0944578c549bc4b61bba270f150c8c6db5a6b 100644 --- a/briar-android/src/net/sf/briar/android/messages/ReadMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ReadPrivateMessageActivity.java @@ -37,7 +37,7 @@ import android.widget.TextView; import com.google.inject.Inject; -public class ReadMessageActivity extends BriarActivity +public class ReadPrivateMessageActivity extends BriarActivity implements OnClickListener { static final int RESULT_REPLY = RESULT_FIRST_USER; @@ -45,7 +45,7 @@ implements OnClickListener { static final int RESULT_NEXT = RESULT_FIRST_USER + 2; private static final Logger LOG = - Logger.getLogger(ReadMessageActivity.class.getName()); + Logger.getLogger(ReadPrivateMessageActivity.class.getName()); private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); @@ -55,11 +55,9 @@ implements OnClickListener { @Inject @DatabaseExecutor private Executor dbExecutor; private ContactId contactId = null; - private String contactName = null; private MessageId messageId = null; - private boolean first, last, starred, read; - private ImageButton readButton = null; - private ImageButton prevButton = null, nextButton = null; + private boolean read; + private ImageButton readButton = null, prevButton = null, nextButton = null; private ImageButton replyButton = null; private TextView content = null; @@ -71,8 +69,9 @@ implements OnClickListener { int cid = i.getIntExtra("net.sf.briar.CONTACT_ID", -1); if(cid == -1) throw new IllegalStateException(); contactId = new ContactId(cid); - contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME"); + String contactName = i.getStringExtra("net.sf.briar.CONTACT_NAME"); if(contactName == null) throw new IllegalStateException(); + setTitle(contactName); byte[] mid = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID"); if(mid == null) throw new IllegalStateException(); messageId = new MessageId(mid); @@ -80,14 +79,13 @@ implements OnClickListener { if(contentType == null) throw new IllegalStateException(); long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1); if(timestamp == -1) throw new IllegalStateException(); - first = i.getBooleanExtra("net.sf.briar.FIRST", false); - last = i.getBooleanExtra("net.sf.briar.LAST", false); + boolean incoming = i.getBooleanExtra("net.sf.briar.INCOMING", false); + boolean first = i.getBooleanExtra("net.sf.briar.FIRST", false); + boolean last = i.getBooleanExtra("net.sf.briar.LAST", false); if(state != null && bundleEncrypter.decrypt(state)) { - starred = state.getBoolean("net.sf.briar.STARRED"); read = state.getBoolean("net.sf.briar.READ"); } else { - starred = i.getBooleanExtra("net.sf.briar.STARRED", false); read = false; setReadInDatabase(true); } @@ -112,8 +110,11 @@ implements OnClickListener { // Give me all the unused width name.setLayoutParams(CommonLayoutParams.WRAP_WRAP_1); name.setTextSize(18); + name.setMaxLines(1); name.setPadding(10, 10, 10, 10); - String format = getResources().getString(R.string.message_from); + String format; + if(incoming) format = getResources().getString(R.string.format_from); + else format = getResources().getString(R.string.format_to); name.setText(String.format(format, contactName)); header.addView(name); @@ -204,7 +205,7 @@ implements OnClickListener { private void setReadInUi(final boolean read) { runOnUiThread(new Runnable() { public void run() { - ReadMessageActivity.this.read = read; + ReadPrivateMessageActivity.this.read = read; if(read) readButton.setImageResource(R.drawable.content_unread); else readButton.setImageResource(R.drawable.content_read); } @@ -241,7 +242,6 @@ implements OnClickListener { @Override public void onSaveInstanceState(Bundle state) { - state.putBoolean("net.sf.briar.STARRED", starred); state.putBoolean("net.sf.briar.READ", read); bundleEncrypter.encrypt(state); } @@ -262,7 +262,7 @@ implements OnClickListener { setResult(RESULT_NEXT); finish(); } else if(view == replyButton) { - Intent i = new Intent(this, WriteMessageActivity.class); + Intent i = new Intent(this, WritePrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes()); startActivity(i); diff --git a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java similarity index 89% rename from briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java rename to briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java index 4673d8180e8b3d7d76ab6d3fe85f684d311b5bed..ced62326ac5f737bc66264c187b63a5f85d0ed69 100644 --- a/briar-android/src/net/sf/briar/android/messages/WriteMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/WritePrivateMessageActivity.java @@ -7,6 +7,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.Collection; import java.util.concurrent.Executor; @@ -42,11 +43,11 @@ import android.widget.TextView; import com.google.inject.Inject; -public class WriteMessageActivity extends BriarActivity +public class WritePrivateMessageActivity extends BriarActivity implements OnClickListener, OnItemSelectedListener { private static final Logger LOG = - Logger.getLogger(WriteMessageActivity.class.getName()); + Logger.getLogger(WritePrivateMessageActivity.class.getName()); private final BriarServiceConnection serviceConnection = new BriarServiceConnection(); @@ -85,7 +86,7 @@ implements OnClickListener, OnItemSelectedListener { TextView to = new TextView(this); to.setTextSize(18); to.setPadding(10, 10, 10, 10); - to.setText(R.string.message_to); + to.setText(R.string.to); actionBar.addView(to); adapter = new ContactNameSpinnerAdapter(this); @@ -163,32 +164,36 @@ implements OnClickListener, OnItemSelectedListener { public void onClick(View view) { if(contactId == null) throw new IllegalStateException(); try { - byte[] body = content.getText().toString().getBytes("UTF-8"); - storeMessage(messageFactory.createPrivateMessage(parentId, - "text/plain", body)); - } catch(IOException e) { - throw new RuntimeException(e); - } catch(GeneralSecurityException e) { + storeMessage(content.getText().toString().getBytes("UTF-8")); + } catch(UnsupportedEncodingException e) { throw new RuntimeException(e); } finish(); } - private void storeMessage(final Message m) { + 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 { serviceConnection.waitForStartup(); + Message m = messageFactory.createPrivateMessage(parentId, + "text/plain", body); db.addLocalPrivateMessage(m, contactId); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } catch(GeneralSecurityException e) { + throw new RuntimeException(e); } catch(InterruptedException e) { if(LOG.isLoggable(INFO)) LOG.info("Interrupted while waiting for service"); Thread.currentThread().interrupt(); + } catch(IOException e) { + throw new RuntimeException(e); } } }); diff --git a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java index 958092f40c729faf84cf64cfab28485512fd47ea..c708f335ca1753e51a29f66148d9580eecb037c2 100644 --- a/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java +++ b/briar-android/src/net/sf/briar/android/widgets/HorizontalBorder.java @@ -13,6 +13,6 @@ public class HorizontalBorder extends View { public HorizontalBorder(Context ctx) { super(ctx); setLayoutParams(new LayoutParams(MATCH_PARENT, LINE_WIDTH)); - setBackgroundColor(getResources().getColor(R.color.HorizontalBorder)); + setBackgroundColor(getResources().getColor(R.color.horizontal_border)); } }