Changed the root package from net.sf.briar to org.briarproject.

This commit is contained in:
akwizgran
2014-01-08 16:18:30 +00:00
parent dce70f487c
commit 832476412c
427 changed files with 2507 additions and 2507 deletions

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.contact;
import org.briarproject.api.Contact;
public class ContactItem {
public static final ContactItem NEW = new ContactItem(null);
private final Contact contact;
public ContactItem(Contact contact) {
this.contact = contact;
}
public Contact getContact() {
return contact;
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.android.contact;
import java.util.Comparator;
public class ContactItemComparator implements Comparator<ContactItem> {
public static final ContactItemComparator INSTANCE =
new ContactItemComparator();
public int compare(ContactItem a, ContactItem b) {
if(a == b) return 0;
if(a == ContactItem.NEW) return 1;
if(b == ContactItem.NEW) return -1;
String aName = a.getContact().getAuthor().getName();
String bName = b.getContact().getAuthor().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,320 @@
package org.briarproject.android.contact;
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 org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.AuthorId;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactAddedEvent;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.transport.ConnectionListener;
import org.briarproject.api.transport.ConnectionRegistry;
import roboguice.activity.RoboActivity;
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;
public class ContactListActivity extends RoboActivity
implements OnClickListener, OnItemClickListener, EventListener,
ConnectionListener {
private static final Logger LOG =
Logger.getLogger(ContactListActivity.class.getName());
@Inject private ConnectionRegistry connectionRegistry;
private ContactListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton addContactButton = 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 ContactListAdapter(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(this);
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));
addContactButton = new ImageButton(this);
addContactButton.setBackgroundResource(0);
addContactButton.setImageResource(R.drawable.social_add_person);
addContactButton.setOnClickListener(this);
layout.addView(addContactButton);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
connectionRegistry.addListener(this);
loadContacts();
}
private void loadContacts() {
clearContacts();
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Map<ContactId, Long> times = db.getLastConnected();
for(Contact c : db.getContacts()) {
Long lastConnected = times.get(c.getId());
if(lastConnected == null) continue;
try {
GroupId inbox = db.getInboxGroupId(c.getId());
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(c.getId());
displayContact(c, lastConnected, inbox, 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 clearContacts() {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(GONE);
loading.setVisibility(VISIBLE);
adapter.clear();
adapter.notifyDataSetChanged();
}
});
}
private void displayContact(final Contact c, final long lastConnected,
final GroupId inbox, final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
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,
inbox, 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.getContact().getId().equals(c)) return item;
}
return null; // Not found
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
connectionRegistry.removeListener(this);
}
public void onClick(View view) {
startActivity(new Intent(this, AddContactActivity.class));
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
ContactListItem item = adapter.getItem(position);
ContactId contactId = item.getContact().getId();
String contactName = item.getContact().getAuthor().getName();
GroupId inbox = item.getInboxGroupId();
AuthorId localAuthorId = item.getContact().getLocalAuthorId();
Intent i = new Intent(this, ConversationActivity.class);
i.putExtra("org.briarproject.CONTACT_ID", contactId.getInt());
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", inbox.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
startActivity(i);
}
public void eventOccurred(Event e) {
if(e instanceof ContactAddedEvent) {
loadContacts();
} else if(e instanceof ContactRemovedEvent) {
// Reload the conversation, expecting NoSuchContactException
if(LOG.isLoggable(INFO)) LOG.info("Contact removed, reloading");
reloadContact(((ContactRemovedEvent) e).getContactId());
} else if(e instanceof MessageAddedEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
ContactId source = ((MessageAddedEvent) e).getContactId();
if(source == null) loadContacts();
else reloadContact(source);
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadContacts();
}
}
private void reloadContact(final ContactId c) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(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<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item != null) item.setHeaders(headers);
}
});
}
private void removeItem(final ContactId c) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item != null) {
adapter.remove(item);
adapter.notifyDataSetChanged();
}
}
});
}
public void contactConnected(ContactId c) {
setConnected(c, true);
}
public void contactDisconnected(ContactId c) {
setConnected(c, false);
}
private void setConnected(final ContactId c, final boolean connected) {
runOnUiThread(new Runnable() {
public void run() {
ContactListItem item = findItem(c);
if(item == null) return;
if(LOG.isLoggable(INFO)) LOG.info("Updating connection time");
item.setConnected(connected);
item.setLastConnected(System.currentTimeMillis());
list.invalidateViews();
}
});
}
private static class ItemComparator implements Comparator<ContactListItem> {
private static final ItemComparator INSTANCE = new ItemComparator();
public int compare(ContactListItem a, ContactListItem b) {
String aName = a.getContact().getAuthor().getName();
String bName = b.getContact().getAuthor().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}
}

View File

@@ -0,0 +1,73 @@
package org.briarproject.android.contact;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import android.content.Context;
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.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
class ContactListAdapter extends ArrayAdapter<ContactListItem> {
ContactListAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ContactListItem>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ContactListItem item = getItem(position);
Context ctx = getContext();
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);
if(item.isConnected())
bulb.setImageResource(R.drawable.contact_connected);
else bulb.setImageResource(R.drawable.contact_disconnected);
layout.addView(bulb);
TextView name = new TextView(ctx);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(0, 10, 10, 10);
int unread = item.getUnreadCount();
String contactName = item.getContact().getAuthor().getName();
if(unread > 0) name.setText(contactName + " (" + unread + ")");
else name.setText(contactName);
layout.addView(name);
TextView connected = new TextView(ctx);
connected.setTextSize(14);
connected.setPadding(0, 10, 10, 10);
if(item.isConnected()) {
connected.setText(R.string.contact_connected);
} else {
String format = res.getString(R.string.format_last_connected);
long then = item.getLastConnected();
CharSequence ago = DateUtils.getRelativeTimeSpanString(then);
connected.setText(Html.fromHtml(String.format(format, ago)));
}
layout.addView(connected);
return layout;
}
}

View File

@@ -0,0 +1,76 @@
package org.briarproject.android.contact;
import java.util.Collection;
import org.briarproject.api.Contact;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.messaging.GroupId;
// This class is not thread-safe
class ContactListItem {
private final Contact contact;
private final GroupId inbox;
private boolean connected;
private long lastConnected;
private boolean empty;
private long timestamp;
private int unread;
ContactListItem(Contact contact, boolean connected, long lastConnected,
GroupId inbox, Collection<MessageHeader> headers) {
this.contact = contact;
this.inbox = inbox;
this.connected = connected;
this.lastConnected = lastConnected;
setHeaders(headers);
}
void setHeaders(Collection<MessageHeader> headers) {
empty = headers.isEmpty();
timestamp = 0;
unread = 0;
if(!empty) {
for(MessageHeader h : headers) {
if(h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
if(!h.isRead()) unread++;
}
}
}
Contact getContact() {
return contact;
}
GroupId getInboxGroupId() {
return inbox;
}
long getLastConnected() {
return lastConnected;
}
void setLastConnected(long lastConnected) {
this.lastConnected = lastConnected;
}
boolean isConnected() {
return connected;
}
void setConnected(boolean connected) {
this.connected = connected;
}
boolean isEmpty() {
return empty;
}
long getTimestamp() {
return timestamp;
}
int getUnreadCount() {
return unread;
}
}

View File

@@ -0,0 +1,248 @@
package org.briarproject.android.contact;
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 org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.AuthorId;
import org.briarproject.api.ContactId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import roboguice.activity.RoboActivity;
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;
public class ConversationActivity extends RoboActivity
implements EventListener, OnClickListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName());
private String contactName = null;
private ConversationAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton composeButton = 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;
private volatile ContactId contactId = null;
private volatile GroupId groupId = null;
private volatile AuthorId localAuthorId = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
int id = i.getIntExtra("org.briarproject.CONTACT_ID", -1);
if(id == -1) throw new IllegalStateException();
contactId = new ContactId(id);
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
setTitle(contactName);
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
adapter = new ConversationAdapter(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(this);
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));
composeButton = new ImageButton(this);
composeButton.setBackgroundResource(0);
composeButton.setImageResource(R.drawable.content_new_email);
composeButton.setEnabled(false); // Enabled after loading the headers
composeButton.setOnClickListener(this);
layout.addView(composeButton);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
db.addListener(this);
loadHeaders();
}
private void loadHeaders() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getInboxMessageHeaders(contactId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayHeaders(headers);
} catch(NoSuchContactException e) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} 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 displayHeaders(final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
composeButton.setEnabled(true);
adapter.clear();
for(MessageHeader h : headers)
adapter.add(new ConversationItem(h));
adapter.sort(ConversationItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
});
}
private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount();
for(int i = 0; i < count; i++) {
if(!adapter.getItem(i).getHeader().isRead()) {
firstUnread = i;
break;
}
}
if(firstUnread == -1) list.setSelection(count - 1);
else list.setSelection(firstUnread);
}
@Override
public void onActivityResult(int request, int result, Intent data) {
if(result == ReadPrivateMessageActivity.RESULT_PREV) {
int position = request - 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
} else if(result == ReadPrivateMessageActivity.RESULT_NEXT) {
int position = request + 1;
if(position >= 0 && position < adapter.getCount())
displayMessage(position);
}
}
@Override
public void onPause() {
super.onPause();
db.removeListener(this);
}
public void eventOccurred(Event e) {
if(e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if(c.getContactId().equals(contactId)) {
if(LOG.isLoggable(INFO)) LOG.info("Contact removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
}
} else if(e instanceof MessageAddedEvent) {
GroupId g = ((MessageAddedEvent) e).getGroup().getId();
if(g.equals(groupId)) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
loadHeaders();
}
} else if(e instanceof MessageExpiredEvent) {
if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
loadHeaders();
}
}
public void onClick(View view) {
Intent i = new Intent(this, WritePrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
startActivity(i);
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
displayMessage(position);
}
private void displayMessage(int position) {
MessageHeader header = adapter.getItem(position).getHeader();
Intent i = new Intent(this, ReadPrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_ID", contactId.getInt());
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID", localAuthorId.getBytes());
i.putExtra("org.briarproject.AUTHOR_NAME", header.getAuthor().getName());
i.putExtra("org.briarproject.MESSAGE_ID", header.getId().getBytes());
i.putExtra("org.briarproject.CONTENT_TYPE", header.getContentType());
i.putExtra("org.briarproject.TIMESTAMP", header.getTimestamp());
startActivityForResult(i, position);
}
}

View File

@@ -0,0 +1,57 @@
package org.briarproject.android.contact;
import static android.widget.LinearLayout.HORIZONTAL;
import static java.text.DateFormat.SHORT;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.util.ArrayList;
import org.briarproject.R;
import org.briarproject.api.db.MessageHeader;
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.LinearLayout;
import android.widget.TextView;
class ConversationAdapter extends ArrayAdapter<ConversationItem> {
ConversationAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ConversationItem>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MessageHeader header = getItem(position).getHeader();
Context ctx = getContext();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
if(!header.isRead()) {
Resources res = ctx.getResources();
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);
name.setText(header.getAuthor().getName());
layout.addView(name);
TextView date = new TextView(ctx);
date.setTextSize(14);
date.setPadding(0, 10, 10, 10);
long then = header.getTimestamp(), now = System.currentTimeMillis();
date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
layout.addView(date);
return layout;
}
}

View File

@@ -0,0 +1,37 @@
package org.briarproject.android.contact;
import org.briarproject.api.db.MessageHeader;
// This class is not thread-safe
class ConversationItem {
private final MessageHeader header;
private boolean expanded;
private byte[] body;
ConversationItem(MessageHeader header) {
this.header = header;
expanded = false;
body = null;
}
MessageHeader getHeader() {
return header;
}
boolean isExpanded() {
return expanded;
}
void setExpanded(boolean expanded) {
this.expanded = expanded;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.contact;
import java.util.Comparator;
public class ConversationItemComparator
implements Comparator<ConversationItem> {
public static final ConversationItemComparator INSTANCE =
new ConversationItemComparator();
public int compare(ConversationItem a, ConversationItem b) {
// The oldest message comes first
long aTime = a.getHeader().getTimestamp();
long bTime = b.getHeader().getTimestamp();
if(aTime < bTime) return -1;
if(aTime > bTime) return 1;
return 0;
}
}

View File

@@ -0,0 +1,281 @@
package org.briarproject.android.contact;
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 static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.AuthorId;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
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;
public class ReadPrivateMessageActivity extends RoboActivity
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(ReadPrivateMessageActivity.class.getName());
private String contactName = null;
private AuthorId localAuthorId = null;
private boolean read = false;
private ImageButton readButton = null, prevButton = null, nextButton = null;
private ImageButton replyButton = null;
private TextView content = 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;
private volatile MessageId messageId = null;
private volatile GroupId groupId = null;
private volatile long timestamp = -1;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
setTitle(contactName);
byte[] b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
String authorName = i.getStringExtra("org.briarproject.AUTHOR_NAME");
if(authorName == null) throw new IllegalStateException();
b = i.getByteArrayExtra("org.briarproject.MESSAGE_ID");
if(b == null) throw new IllegalStateException();
messageId = new MessageId(b);
b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String contentType = i.getStringExtra("org.briarproject.CONTENT_TYPE");
if(contentType == null) throw new IllegalStateException();
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
if(timestamp == -1) throw new IllegalStateException();
if(state == null) {
read = false;
setReadInDatabase(true);
} else {
read = state.getBoolean("org.briarproject.READ");
}
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
ScrollView scrollView = new ScrollView(this);
// Give me all the width and all the unused height
scrollView.setLayoutParams(MATCH_WRAP_1);
LinearLayout message = new LinearLayout(this);
message.setOrientation(VERTICAL);
Resources res = getResources();
message.setBackgroundColor(res.getColor(R.color.content_background));
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
TextView name = new TextView(this);
// Give me all the unused width
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setMaxLines(1);
name.setPadding(10, 10, 10, 10);
name.setText(authorName);
header.addView(name);
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(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);
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);
footer.addView(nextButton);
footer.addView(new HorizontalSpace(this));
replyButton = new ImageButton(this);
replyButton.setBackgroundResource(0);
replyButton.setImageResource(R.drawable.social_reply);
replyButton.setOnClickListener(this);
footer.addView(replyButton);
layout.addView(footer);
setContentView(layout);
}
private void setReadInDatabase(final boolean read) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.setReadFlag(messageId, read);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Setting flag took " + duration + " ms");
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 database");
Thread.currentThread().interrupt();
}
}
});
}
private void setReadInUi(final boolean read) {
runOnUiThread(new Runnable() {
public void run() {
ReadPrivateMessageActivity.this.read = read;
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
}
});
}
private void loadMessageBody() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
byte[] body = db.getMessageBody(messageId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Loading message took " + duration + " ms");
final String text = new String(body, "UTF-8");
runOnUiThread(new Runnable() {
public void run() {
content.setText(text);
}
});
} catch(NoSuchMessageException e) {
if(LOG.isLoggable(INFO)) LOG.info("Message removed");
runOnUiThread(new Runnable() {
public void run() {
finish();
}
});
} 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();
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
});
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putBoolean("org.briarproject.READ", read);
}
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, WritePrivateMessageActivity.class);
i.putExtra("org.briarproject.CONTACT_NAME", contactName);
i.putExtra("org.briarproject.GROUP_ID", groupId.getBytes());
i.putExtra("org.briarproject.LOCAL_AUTHOR_ID",
localAuthorId.getBytes());
i.putExtra("org.briarproject.PARENT_ID", messageId.getBytes());
i.putExtra("org.briarproject.TIMESTAMP", timestamp);
startActivity(i);
setResult(RESULT_REPLY);
finish();
}
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.android.contact;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.briarproject.R;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
public class SelectContactsDialog extends DialogFragment
implements DialogInterface.OnMultiChoiceClickListener {
private final Set<ContactId> selected = new HashSet<ContactId>();
private Listener listener = null;
private Contact[] contacts = null;
public void setListener(Listener listener) {
this.listener = listener;
}
public void setContacts(Collection<Contact> contacts) {
this.contacts = contacts.toArray(new Contact[contacts.size()]);
}
@Override
public Dialog onCreateDialog(Bundle state) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
String[] names = new String[contacts.length];
for(int i = 0; i < contacts.length; i++)
names[i] = contacts[i].getAuthor().getName();
builder.setMultiChoiceItems(names, new boolean[contacts.length], this);
builder.setPositiveButton(R.string.done_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactsSelected(selected);
}
});
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactSelectionCancelled();
}
});
return builder.create();
}
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if(isChecked) selected.add(contacts[which].getId());
else selected.remove(contacts[which].getId());
}
public interface Listener {
void contactsSelected(Collection<ContactId> selected);
void contactSelectionCancelled();
}
}

View File

@@ -0,0 +1,230 @@
package org.briarproject.android.contact;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import org.briarproject.R;
import org.briarproject.android.util.HorizontalSpace;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.android.DatabaseUiExecutor;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.Message;
import org.briarproject.api.messaging.MessageFactory;
import org.briarproject.api.messaging.MessageId;
import roboguice.activity.RoboActivity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class WritePrivateMessageActivity extends RoboActivity
implements OnClickListener {
private static final Logger LOG =
Logger.getLogger(WritePrivateMessageActivity.class.getName());
private TextView from = null, to = null;
private ImageButton sendButton = null;
private EditText content = 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;
@Inject @CryptoExecutor private volatile Executor cryptoExecutor;
@Inject private volatile MessageFactory messageFactory;
private volatile String contactName = null;
private volatile GroupId groupId = null;
private volatile AuthorId localAuthorId = null;
private volatile MessageId parentId = null;
private volatile long timestamp = -1;
private volatile LocalAuthor localAuthor = null;
private volatile Group group = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
contactName = i.getStringExtra("org.briarproject.CONTACT_NAME");
if(contactName == null) throw new IllegalStateException();
byte[] b = i.getByteArrayExtra("org.briarproject.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
b = i.getByteArrayExtra("org.briarproject.LOCAL_AUTHOR_ID");
if(b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
b = i.getByteArrayExtra("org.briarproject.PARENT_ID");
if(b != null) parentId = new MessageId(b);
timestamp = i.getLongExtra("org.briarproject.TIMESTAMP", -1);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
from = new TextView(this);
from.setTextSize(18);
from.setPadding(10, 10, 10, 10);
from.setText(R.string.from);
header.addView(from);
header.addView(new HorizontalSpace(this));
sendButton = new ImageButton(this);
sendButton.setBackgroundResource(0);
sendButton.setImageResource(R.drawable.social_send_now);
sendButton.setEnabled(false); // Enabled after loading the group
sendButton.setOnClickListener(this);
header.addView(sendButton);
layout.addView(header);
to = new TextView(this);
to.setTextSize(18);
to.setPadding(10, 0, 10, 10);
String format = getResources().getString(R.string.format_to);
to.setText(String.format(format, contactName));
layout.addView(to);
content = new EditText(this);
content.setId(1);
int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
| TYPE_TEXT_FLAG_CAP_SENTENCES;
content.setInputType(inputType);
layout.addView(content);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
loadAuthorAndGroup();
}
private void loadAuthorAndGroup() {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
localAuthor = db.getLocalAuthor(localAuthorId);
group = db.getGroup(groupId);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayLocalAuthor();
} catch(NoSuchContactException e) {
finish();
} catch(NoSuchSubscriptionException e) {
finish();
} catch(DbException e) {
if(LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch(InterruptedException e) {
LOG.info("Interrupted while waiting for database");
Thread.currentThread().interrupt();
}
}
});
}
private void displayLocalAuthor() {
runOnUiThread(new Runnable() {
public void run() {
Resources res = getResources();
String format = res.getString(R.string.format_from);
String name = localAuthor.getName();
from.setText(String.format(format, name));
sendButton.setEnabled(true);
}
});
}
public void onClick(View view) {
if(localAuthor == null) throw new IllegalStateException();
try {
createMessage(content.getText().toString().getBytes("UTF-8"));
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
Toast.makeText(this, R.string.message_sent_toast, LENGTH_LONG).show();
finish();
}
private void createMessage(final byte[] body) {
cryptoExecutor.execute(new Runnable() {
public void run() {
// Don't use an earlier timestamp than the parent
long time = System.currentTimeMillis();
time = Math.max(time, timestamp + 1);
Message m;
try {
m = messageFactory.createAnonymousMessage(parentId, group,
"text/plain", time, body);
} catch(GeneralSecurityException e) {
throw new RuntimeException(e);
} catch(IOException e) {
throw new RuntimeException(e);
}
storeMessage(m);
}
});
}
private void storeMessage(final Message m) {
dbUiExecutor.execute(new Runnable() {
public void run() {
try {
lifecycleManager.waitForDatabase();
long now = System.currentTimeMillis();
db.addLocalMessage(m);
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Storing message took " + duration + " ms");
} 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();
}
}
});
}
}