Use 'forum' instead of 'group'. #174

This commit is contained in:
akwizgran
2015-12-15 11:21:41 +00:00
parent 2621ab011c
commit e7e7bf35ea
27 changed files with 602 additions and 622 deletions

View File

@@ -0,0 +1,163 @@
package org.briarproject.android.forum;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.SubscriptionAddedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
public class AvailableForumsActivity extends BriarActivity
implements EventListener, OnItemClickListener {
private static final Logger LOG =
Logger.getLogger(AvailableForumsActivity.class.getName());
private AvailableForumsAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject private volatile EventBus eventBus;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
adapter = new AvailableForumsAdapter(this);
list = new ListView(this);
list.setLayoutParams(MATCH_MATCH);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(this);
setContentView(loading);
}
@Override
public void onResume() {
super.onResume();
eventBus.addListener(this);
loadForums();
}
private void loadForums() {
runOnDbThread(new Runnable() {
public void run() {
try {
Collection<ForumContacts> available =
new ArrayList<ForumContacts>();
long now = System.currentTimeMillis();
for (Group g : db.getAvailableGroups()) {
try {
GroupId id = g.getId();
Collection<Contact> c = db.getSubscribers(id);
available.add(new ForumContacts(g, c));
} catch (NoSuchSubscriptionException e) {
// Continue
}
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayForums(available);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayForums(final Collection<ForumContacts> available) {
runOnUiThread(new Runnable() {
public void run() {
if (available.isEmpty()) {
LOG.info("No forums available, finishing");
finish();
} else {
setContentView(list);
adapter.clear();
for (ForumContacts f : available)
adapter.add(new AvailableForumsItem(f));
adapter.sort(AvailableForumsItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
}
}
});
}
@Override
public void onPause() {
super.onPause();
eventBus.removeListener(this);
}
public void eventOccurred(Event e) {
if (e instanceof RemoteSubscriptionsUpdatedEvent) {
LOG.info("Remote subscriptions changed, reloading");
loadForums();
} else if (e instanceof SubscriptionAddedEvent) {
LOG.info("Subscription added, reloading");
loadForums();
} else if (e instanceof SubscriptionRemovedEvent) {
LOG.info("Subscription removed, reloading");
loadForums();
}
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
AvailableForumsItem item = adapter.getItem(position);
Collection<ContactId> visible = new ArrayList<ContactId>();
for (Contact c : item.getContacts()) visible.add(c.getId());
addSubscription(item.getGroup(), visible);
String subscribed = getString(R.string.subscribed_toast);
Toast.makeText(this, subscribed, LENGTH_SHORT).show();
}
private void addSubscription(final Group g,
final Collection<ContactId> visible) {
runOnDbThread(new Runnable() {
public void run() {
try {
db.addGroup(g);
db.setVisibility(g.getId(), visible);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
}

View File

@@ -0,0 +1,57 @@
package org.briarproject.android.forum;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.Contact;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import static android.text.TextUtils.TruncateAt.END;
import static android.widget.LinearLayout.VERTICAL;
class AvailableForumsAdapter extends ArrayAdapter<AvailableForumsItem> {
private final int pad;
AvailableForumsAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<AvailableForumsItem>());
pad = LayoutUtils.getPadding(ctx);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
AvailableForumsItem item = getItem(position);
Context ctx = getContext();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(VERTICAL);
TextView name = new TextView(ctx);
name.setTextSize(18);
name.setSingleLine();
name.setEllipsize(END);
name.setPadding(pad, pad, pad, pad);
name.setText(item.getGroup().getName());
layout.addView(name);
TextView status = new TextView(ctx);
status.setPadding(pad, 0, pad, pad);
Collection<String> names = new ArrayList<String>();
for (Contact c : item.getContacts()) names.add(c.getAuthor().getName());
String format = ctx.getString(R.string.shared_by_format);
status.setText(String.format(format, StringUtils.join(names, ", ")));
layout.addView(status);
return layout;
}
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.android.forum;
import org.briarproject.api.Contact;
import org.briarproject.api.messaging.Group;
import java.util.Collection;
class AvailableForumsItem {
private final ForumContacts forumContacts;
AvailableForumsItem(ForumContacts forumContacts) {
this.forumContacts = forumContacts;
}
Group getGroup() {
return forumContacts.getGroup();
}
Collection<Contact> getContacts() {
return forumContacts.getContacts();
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.android.forum;
import java.util.Comparator;
class AvailableForumsItemComparator implements Comparator<AvailableForumsItem> {
static final AvailableForumsItemComparator INSTANCE =
new AvailableForumsItemComparator();
public int compare(AvailableForumsItem a, AvailableForumsItem b) {
if (a == b) return 0;
String aName = a.getGroup().getName();
String bName = b.getGroup().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,171 @@
package org.briarproject.android.forum;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupFactory;
import org.briarproject.util.StringUtils;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.view.Gravity.CENTER;
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 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_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
import static org.briarproject.api.messaging.MessagingConstants.MAX_GROUP_NAME_LENGTH;
public class CreateForumActivity extends BriarActivity
implements OnEditorActionListener, OnClickListener {
private static final Logger LOG =
Logger.getLogger(CreateForumActivity.class.getName());
private EditText nameEntry = null;
private Button createForumButton = null;
private ProgressBar progress = null;
private TextView feedback = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile GroupFactory groupFactory;
@Inject private volatile DatabaseComponent db;
@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);
int pad = LayoutUtils.getPadding(this);
layout.setPadding(pad, pad, pad, pad);
TextView chooseName = new TextView(this);
chooseName.setGravity(CENTER);
chooseName.setTextSize(18);
chooseName.setText(R.string.choose_forum_name);
layout.addView(chooseName);
nameEntry = new EditText(this) {
@Override
protected void onTextChanged(CharSequence text, int start,
int lengthBefore, int lengthAfter) {
enableOrDisableCreateButton();
}
};
nameEntry.setId(1);
nameEntry.setMaxLines(1);
nameEntry.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES);
nameEntry.setOnEditorActionListener(this);
layout.addView(nameEntry);
feedback = new TextView(this);
feedback.setGravity(CENTER);
feedback.setPadding(0, pad, 0, pad);
layout.addView(feedback);
createForumButton = new Button(this);
createForumButton.setLayoutParams(WRAP_WRAP);
createForumButton.setText(R.string.create_forum_button);
createForumButton.setOnClickListener(this);
layout.addView(createForumButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
}
private void enableOrDisableCreateButton() {
if (progress == null) return; // Not created yet
createForumButton.setEnabled(validateName());
}
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
hideSoftKeyboard();
return true;
}
private boolean validateName() {
int length = StringUtils.toUtf8(nameEntry.getText().toString()).length;
if (length > MAX_GROUP_NAME_LENGTH) {
feedback.setText(R.string.name_too_long);
return false;
}
feedback.setText("");
return length > 0;
}
public void onClick(View view) {
if (view == createForumButton) {
hideSoftKeyboard();
if (!validateName()) return;
createForumButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
storeForum(nameEntry.getText().toString());
}
}
private void storeForum(final String name) {
runOnDbThread(new Runnable() {
public void run() {
try {
Group g = groupFactory.createGroup(name);
long now = System.currentTimeMillis();
db.addGroup(g);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Storing forum took " + duration + " ms");
displayForum(g);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
finishOnUiThread();
}
}
});
}
private void displayForum(final Group g) {
runOnUiThread(new Runnable() {
public void run() {
Intent i = new Intent(CreateForumActivity.this,
ForumActivity.class);
i.putExtra("briar.GROUP_ID", g.getId().getBytes());
i.putExtra("briar.FORUM_NAME", g.getName());
startActivity(i);
Toast.makeText(CreateForumActivity.this,
R.string.forum_created_toast, LENGTH_LONG).show();
finish();
}
});
}
}

View File

@@ -0,0 +1,378 @@
package org.briarproject.android.forum;
import android.content.Intent;
import android.content.res.Resources;
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 android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.ElasticHorizontalSpace;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.Author;
import org.briarproject.api.android.AndroidNotificationManager;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.view.Gravity.CENTER;
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.forum.ReadForumPostActivity.RESULT_PREV_NEXT;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ForumActivity extends BriarActivity implements EventListener,
OnClickListener, OnItemClickListener {
private static final int REQUEST_READ = 2;
private static final Logger LOG =
Logger.getLogger(ForumActivity.class.getName());
@Inject private AndroidNotificationManager notificationManager;
private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>();
private TextView empty = null;
private ForumAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private ImageButton composeButton = null, shareButton = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject private volatile EventBus eventBus;
private volatile GroupId groupId = null;
private volatile Group group = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("briar.GROUP_ID");
if (b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String forumName = i.getStringExtra("briar.FORUM_NAME");
if (forumName != null) setTitle(forumName);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
empty = new TextView(this);
empty.setLayoutParams(MATCH_WRAP_1);
empty.setGravity(CENTER);
empty.setTextSize(18);
empty.setText(R.string.no_forum_posts);
empty.setVisibility(GONE);
layout.addView(empty);
adapter = new ForumAdapter(this);
list = new ListView(this);
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
list.setVisibility(GONE);
layout.addView(list);
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setGravity(CENTER);
Resources res = getResources();
footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
footer.addView(new ElasticHorizontalSpace(this));
composeButton = new ImageButton(this);
composeButton.setBackgroundResource(0);
composeButton.setImageResource(R.drawable.content_new_email);
composeButton.setOnClickListener(this);
footer.addView(composeButton);
footer.addView(new ElasticHorizontalSpace(this));
shareButton = new ImageButton(this);
shareButton.setBackgroundResource(0);
shareButton.setImageResource(R.drawable.social_share);
shareButton.setOnClickListener(this);
footer.addView(shareButton);
footer.addView(new ElasticHorizontalSpace(this));
layout.addView(footer);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
eventBus.addListener(this);
loadForum();
loadHeaders();
}
private void loadForum() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
group = db.getGroup(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading group " + duration + " ms");
displayForumName();
} catch (NoSuchSubscriptionException e) {
finishOnUiThread();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayForumName() {
runOnUiThread(new Runnable() {
public void run() {
setTitle(group.getName());
}
});
}
private void loadHeaders() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getMessageHeaders(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayHeaders(headers);
} catch (NoSuchSubscriptionException e) {
finishOnUiThread();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayHeaders(final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
loading.setVisibility(GONE);
adapter.clear();
if (headers.isEmpty()) {
empty.setVisibility(VISIBLE);
list.setVisibility(GONE);
} else {
empty.setVisibility(GONE);
list.setVisibility(VISIBLE);
for (MessageHeader h : headers) {
ForumItem item = new ForumItem(h);
byte[] body = bodyCache.get(h.getId());
if (body == null) loadMessageBody(h);
else item.setBody(body);
adapter.add(item);
}
adapter.sort(ForumItemComparator.INSTANCE);
// Scroll to the bottom
list.setSelection(adapter.getCount() - 1);
}
adapter.notifyDataSetChanged();
}
});
}
private void loadMessageBody(final MessageHeader h) {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
byte[] body = db.getMessageBody(h.getId());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading message took " + duration + " ms");
displayMessage(h.getId(), body);
} catch (NoSuchMessageException e) {
// The item will be removed when we get the event
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayMessage(final MessageId m, final byte[] body) {
runOnUiThread(new Runnable() {
public void run() {
bodyCache.put(m, body);
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
ForumItem item = adapter.getItem(i);
if (item.getHeader().getId().equals(m)) {
item.setBody(body);
adapter.notifyDataSetChanged();
// Scroll to the bottom
list.setSelection(count - 1);
return;
}
}
}
});
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
int position = data.getIntExtra("briar.POSITION", -1);
if (position >= 0 && position < adapter.getCount())
displayMessage(position);
}
}
@Override
public void onPause() {
super.onPause();
eventBus.removeListener(this);
if (isFinishing()) markMessagesRead();
}
private void markMessagesRead() {
notificationManager.clearForumPostNotification(groupId);
List<MessageId> unread = new ArrayList<MessageId>();
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
MessageHeader h = adapter.getItem(i).getHeader();
if (!h.isRead()) unread.add(h.getId());
}
if (unread.isEmpty()) return;
if (LOG.isLoggable(INFO))
LOG.info("Marking " + unread.size() + " messages read");
markMessagesRead(Collections.unmodifiableList(unread));
}
private void markMessagesRead(final Collection<MessageId> unread) {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
for (MessageId m : unread) db.setReadFlag(m, true);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Marking read took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) {
if (((MessageAddedEvent) e).getGroup().getId().equals(groupId)) {
LOG.info("Message added, reloading");
loadHeaders();
}
} else if (e instanceof MessageExpiredEvent) {
LOG.info("Message expired, reloading");
loadHeaders();
} else if (e instanceof SubscriptionRemovedEvent) {
SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
if (s.getGroup().getId().equals(groupId)) {
LOG.info("Subscription removed");
finishOnUiThread();
}
}
}
public void onClick(View view) {
if (view == composeButton) {
Intent i = new Intent(this, WriteForumPostActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.FORUM_NAME", group.getName());
i.putExtra("briar.MIN_TIMESTAMP", getMinTimestampForNewMessage());
startActivity(i);
} else if (view == shareButton) {
Intent i = new Intent(this, ShareForumActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.FORUM_NAME", group.getName());
startActivity(i);
}
}
private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message
long timestamp = 0;
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
long t = adapter.getItem(i).getHeader().getTimestamp();
if (t > timestamp) timestamp = t;
}
return timestamp + 1;
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
displayMessage(position);
}
private void displayMessage(int position) {
MessageHeader item = adapter.getItem(position).getHeader();
Intent i = new Intent(this, ReadForumPostActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.FORUM_NAME", group.getName());
i.putExtra("briar.MESSAGE_ID", item.getId().getBytes());
Author author = item.getAuthor();
if (author != null) i.putExtra("briar.AUTHOR_NAME", author.getName());
i.putExtra("briar.AUTHOR_STATUS", item.getAuthorStatus().name());
i.putExtra("briar.CONTENT_TYPE", item.getContentType());
i.putExtra("briar.TIMESTAMP", item.getTimestamp());
i.putExtra("briar.MIN_TIMESTAMP", getMinTimestampForNewMessage());
i.putExtra("briar.POSITION", position);
startActivityForResult(i, REQUEST_READ);
}
}

View File

@@ -0,0 +1,88 @@
package org.briarproject.android.forum;
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.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.AuthorView;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.Author;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
class ForumAdapter extends ArrayAdapter<ForumItem> {
private final int pad;
ForumAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ForumItem>());
pad = LayoutUtils.getPadding(ctx);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ForumItem item = getItem(position);
MessageHeader header = item.getHeader();
Context ctx = getContext();
Resources res = ctx.getResources();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
if (!header.isRead())
layout.setBackgroundColor(res.getColor(R.color.unread_background));
LinearLayout headerLayout = new LinearLayout(ctx);
headerLayout.setOrientation(HORIZONTAL);
headerLayout.setGravity(CENTER_VERTICAL);
AuthorView authorView = new AuthorView(ctx);
authorView.setLayoutParams(WRAP_WRAP_1);
Author author = header.getAuthor();
if (author == null) authorView.init(null, header.getAuthorStatus());
else authorView.init(author.getName(), header.getAuthorStatus());
headerLayout.addView(authorView);
TextView date = new TextView(ctx);
date.setPadding(0, pad, pad, pad);
long timestamp = header.getTimestamp();
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
headerLayout.addView(date);
layout.addView(headerLayout);
if (item.getBody() == null) {
TextView ellipsis = new TextView(ctx);
ellipsis.setPadding(pad, 0, pad, pad);
ellipsis.setText("\u2026");
layout.addView(ellipsis);
} else if (header.getContentType().equals("text/plain")) {
TextView text = new TextView(ctx);
text.setPadding(pad, 0, pad, pad);
text.setText(StringUtils.fromUtf8(item.getBody()));
layout.addView(text);
} else {
ImageButton attachment = new ImageButton(ctx);
attachment.setPadding(pad, 0, pad, pad);
attachment.setImageResource(R.drawable.content_attachment);
layout.addView(attachment);
}
return layout;
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.android.forum;
import org.briarproject.api.Contact;
import org.briarproject.api.messaging.Group;
import java.util.Collection;
class ForumContacts {
private final Group group;
private final Collection<Contact> contacts;
ForumContacts(Group group, Collection<Contact> contacts) {
this.group = group;
this.contacts = contacts;
}
Group getGroup() {
return group;
}
Collection<Contact> getContacts() {
return contacts;
}
}

View File

@@ -0,0 +1,27 @@
package org.briarproject.android.forum;
import org.briarproject.api.db.MessageHeader;
// This class is not thread-safe
class ForumItem {
private final MessageHeader header;
private byte[] body;
ForumItem(MessageHeader header) {
this.header = header;
body = null;
}
MessageHeader getHeader() {
return header;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
}

View File

@@ -0,0 +1,17 @@
package org.briarproject.android.forum;
import java.util.Comparator;
class ForumItemComparator implements Comparator<ForumItem> {
static final ForumItemComparator INSTANCE = new ForumItemComparator();
public int compare(ForumItem a, ForumItem 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,391 @@
package org.briarproject.android.forum;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.android.util.ListLoadingProgressBar;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.db.NoSuchSubscriptionException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.MessageAddedEvent;
import org.briarproject.api.event.MessageExpiredEvent;
import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
import org.briarproject.api.event.SubscriptionAddedEvent;
import org.briarproject.api.event.SubscriptionRemovedEvent;
import org.briarproject.api.messaging.Group;
import org.briarproject.api.messaging.GroupId;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.view.Gravity.CENTER;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Menu.NONE;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.LinearLayout.HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_SHORT;
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;
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
public class ForumListActivity extends BriarActivity
implements EventListener, OnClickListener, OnItemClickListener,
OnCreateContextMenuListener {
private static final int MENU_ITEM_UNSUBSCRIBE = 1;
private static final Logger LOG =
Logger.getLogger(ForumListActivity.class.getName());
private final Map<GroupId, GroupId> groupIds =
new ConcurrentHashMap<GroupId, GroupId>();
private TextView empty = null;
private ForumListAdapter adapter = null;
private ListView list = null;
private ListLoadingProgressBar loading = null;
private TextView available = null;
private ImageButton newForumButton = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject private volatile EventBus eventBus;
@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);
int pad = LayoutUtils.getPadding(this);
empty = new TextView(this);
empty.setLayoutParams(MATCH_WRAP_1);
empty.setGravity(CENTER);
empty.setTextSize(18);
empty.setText(R.string.no_forums);
empty.setVisibility(GONE);
layout.addView(empty);
adapter = new ForumListAdapter(this);
list = new ListView(this);
list.setLayoutParams(MATCH_WRAP_1);
list.setAdapter(adapter);
list.setOnItemClickListener(this);
list.setOnCreateContextMenuListener(this);
list.setVisibility(GONE);
layout.addView(list);
// Show a progress bar while the list is loading
loading = new ListLoadingProgressBar(this);
layout.addView(loading);
available = new TextView(this);
available.setLayoutParams(MATCH_WRAP);
available.setGravity(CENTER);
available.setTextSize(18);
available.setPadding(pad, pad, pad, pad);
Resources res = getResources();
int background = res.getColor(R.color.forums_available_background);
available.setBackgroundColor(background);
available.setOnClickListener(this);
available.setVisibility(GONE);
layout.addView(available);
layout.addView(new HorizontalBorder(this));
LinearLayout footer = new LinearLayout(this);
footer.setLayoutParams(MATCH_WRAP);
footer.setOrientation(HORIZONTAL);
footer.setGravity(CENTER);
footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
newForumButton = new ImageButton(this);
newForumButton.setBackgroundResource(0);
newForumButton.setImageResource(R.drawable.social_new_chat);
newForumButton.setOnClickListener(this);
footer.addView(newForumButton);
layout.addView(footer);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
eventBus.addListener(this);
loadHeaders();
}
private void loadHeaders() {
clearHeaders();
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
for (Group g : db.getGroups()) {
try {
displayHeaders(g, db.getMessageHeaders(g.getId()));
} catch (NoSuchSubscriptionException e) {
// Continue
}
}
int available = db.getAvailableGroups().size();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Full load took " + duration + " ms");
displayAvailable(available);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void clearHeaders() {
runOnUiThread(new Runnable() {
public void run() {
groupIds.clear();
empty.setVisibility(GONE);
list.setVisibility(GONE);
available.setVisibility(GONE);
loading.setVisibility(VISIBLE);
adapter.clear();
adapter.notifyDataSetChanged();
}
});
}
private void displayHeaders(final Group g,
final Collection<MessageHeader> headers) {
runOnUiThread(new Runnable() {
public void run() {
GroupId id = g.getId();
groupIds.put(id, id);
list.setVisibility(VISIBLE);
loading.setVisibility(GONE);
// Remove the old item, if any
ForumListItem item = findForum(id);
if (item != null) adapter.remove(item);
// Add a new item
adapter.add(new ForumListItem(g, headers));
adapter.sort(ForumListItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
}
});
}
private void displayAvailable(final int availableCount) {
runOnUiThread(new Runnable() {
public void run() {
if (adapter.isEmpty()) empty.setVisibility(VISIBLE);
loading.setVisibility(GONE);
if (availableCount == 0) {
available.setVisibility(GONE);
} else {
available.setVisibility(VISIBLE);
available.setText(getResources().getQuantityString(
R.plurals.forums_shared, availableCount,
availableCount));
}
}
});
}
private ForumListItem findForum(GroupId g) {
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
ForumListItem item = adapter.getItem(i);
if (item.getGroup().getId().equals(g)) return item;
}
return null; // Not found
}
private void selectFirstUnread() {
int firstUnread = -1, count = adapter.getCount();
for (int i = 0; i < count; i++) {
if (adapter.getItem(i).getUnreadCount() > 0) {
firstUnread = i;
break;
}
}
if (firstUnread == -1) list.setSelection(count - 1);
else list.setSelection(firstUnread);
}
@Override
public void onPause() {
super.onPause();
eventBus.removeListener(this);
}
public void eventOccurred(Event e) {
if (e instanceof MessageAddedEvent) {
Group g = ((MessageAddedEvent) e).getGroup();
if (groupIds.containsKey(g.getId())) {
LOG.info("Message added, reloading");
loadHeaders(g);
}
} else if (e instanceof MessageExpiredEvent) {
LOG.info("Message expired, reloading");
loadHeaders();
} else if (e instanceof RemoteSubscriptionsUpdatedEvent) {
LOG.info("Remote subscriptions changed, reloading");
loadAvailable();
} else if (e instanceof SubscriptionAddedEvent) {
LOG.info("Group added, reloading");
loadHeaders();
} else if (e instanceof SubscriptionRemovedEvent) {
Group g = ((SubscriptionRemovedEvent) e).getGroup();
if (groupIds.containsKey(g.getId())) {
LOG.info("Group removed, reloading");
loadHeaders();
}
}
}
private void loadHeaders(final Group g) {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<MessageHeader> headers =
db.getMessageHeaders(g.getId());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
displayHeaders(g, headers);
} catch (NoSuchSubscriptionException e) {
removeForum(g.getId());
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void removeForum(final GroupId g) {
runOnUiThread(new Runnable() {
public void run() {
ForumListItem item = findForum(g);
if (item != null) {
groupIds.remove(g);
adapter.remove(item);
adapter.notifyDataSetChanged();
if (adapter.isEmpty()) {
empty.setVisibility(VISIBLE);
list.setVisibility(GONE);
} else {
selectFirstUnread();
}
}
}
});
}
private void loadAvailable() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
int available = db.getAvailableGroups().size();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading available took " + duration + " ms");
displayAvailable(available);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
public void onClick(View view) {
if (view == available) {
startActivity(new Intent(this, AvailableForumsActivity.class));
} else if (view == newForumButton) {
startActivity(new Intent(this, CreateForumActivity.class));
}
}
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
Intent i = new Intent(this, ForumActivity.class);
Group g = adapter.getItem(position).getGroup();
i.putExtra("briar.GROUP_ID", g.getId().getBytes());
i.putExtra("briar.FORUM_NAME", g.getName());
startActivity(i);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view,
ContextMenu.ContextMenuInfo info) {
String delete = getString(R.string.unsubscribe);
menu.add(NONE, MENU_ITEM_UNSUBSCRIBE, NONE, delete);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem) {
if (menuItem.getItemId() == MENU_ITEM_UNSUBSCRIBE) {
ContextMenuInfo info = menuItem.getMenuInfo();
int position = ((AdapterContextMenuInfo) info).position;
ForumListItem item = adapter.getItem(position);
removeSubscription(item.getGroup());
String unsubscribed = getString(R.string.unsubscribed_toast);
Toast.makeText(this, unsubscribed, LENGTH_SHORT).show();
}
return true;
}
private void removeSubscription(final Group g) {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
db.removeGroup(g);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Removing group took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
}

View File

@@ -0,0 +1,70 @@
package org.briarproject.android.forum;
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;
import org.briarproject.R;
import org.briarproject.android.util.LayoutUtils;
import java.util.ArrayList;
import static android.text.TextUtils.TruncateAt.END;
import static android.widget.LinearLayout.HORIZONTAL;
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
class ForumListAdapter extends ArrayAdapter<ForumListItem> {
private final int pad;
ForumListAdapter(Context ctx) {
super(ctx, android.R.layout.simple_expandable_list_item_1,
new ArrayList<ForumListItem>());
pad = LayoutUtils.getPadding(ctx);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ForumListItem item = getItem(position);
Context ctx = getContext();
Resources res = ctx.getResources();
LinearLayout layout = new LinearLayout(ctx);
layout.setOrientation(HORIZONTAL);
int unread = item.getUnreadCount();
if (unread > 0)
layout.setBackgroundColor(res.getColor(R.color.unread_background));
TextView name = new TextView(ctx);
name.setLayoutParams(WRAP_WRAP_1);
name.setTextSize(18);
name.setSingleLine();
name.setEllipsize(END);
name.setPadding(pad, pad, pad, pad);
String forumName = item.getGroup().getName();
if (unread > 0) name.setText(forumName + " (" + unread + ")");
else name.setText(forumName);
layout.addView(name);
if (item.isEmpty()) {
TextView noPosts = new TextView(ctx);
noPosts.setPadding(pad, 0, pad, pad);
noPosts.setTextColor(res.getColor(R.color.no_posts));
noPosts.setText(R.string.no_forum_posts);
layout.addView(noPosts);
} else {
TextView date = new TextView(ctx);
date.setPadding(pad, 0, pad, pad);
long timestamp = item.getTimestamp();
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
layout.addView(date);
}
return layout;
}
}

View File

@@ -0,0 +1,52 @@
package org.briarproject.android.forum;
import org.briarproject.api.db.MessageHeader;
import org.briarproject.api.messaging.Group;
import java.util.Collection;
class ForumListItem {
private final Group group;
private final boolean empty;
private final long timestamp;
private final int unread;
ForumListItem(Group group, Collection<MessageHeader> headers) {
this.group = group;
empty = headers.isEmpty();
if (empty) {
timestamp = 0;
unread = 0;
} else {
MessageHeader newest = null;
long timestamp = -1;
int unread = 0;
for (MessageHeader h : headers) {
if (h.getTimestamp() > timestamp) {
timestamp = h.getTimestamp();
newest = h;
}
if (!h.isRead()) unread++;
}
this.timestamp = newest.getTimestamp();
this.unread = unread;
}
}
Group getGroup() {
return group;
}
boolean isEmpty() {
return empty;
}
long getTimestamp() {
return timestamp;
}
int getUnreadCount() {
return unread;
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.android.forum;
import java.util.Comparator;
class ForumListItemComparator implements Comparator<ForumListItem> {
static final ForumListItemComparator INSTANCE =
new ForumListItemComparator();
public int compare(ForumListItem a, ForumListItem b) {
if (a == b) return 0;
// The item with the newest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by group name
String aName = a.getGroup().getName();
String bName = b.getGroup().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.android.forum;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import org.briarproject.R;
public class NoContactsDialog {
private Listener listener = null;
public void setListener(Listener listener) {
this.listener = listener;
}
public Dialog build(Context ctx) {
if (listener == null) throw new IllegalStateException();
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setMessage(R.string.no_contacts_prompt);
builder.setPositiveButton(R.string.add_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactCreationSelected();
}
});
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
listener.contactCreationCancelled();
}
});
return builder.create();
}
public interface Listener {
void contactCreationSelected();
void contactCreationCancelled();
}
}

View File

@@ -0,0 +1,234 @@
package org.briarproject.android.forum;
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 org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.util.AuthorView;
import org.briarproject.android.util.ElasticHorizontalSpace;
import org.briarproject.android.util.HorizontalBorder;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.Author;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchMessageException;
import org.briarproject.api.messaging.GroupId;
import org.briarproject.api.messaging.MessageId;
import org.briarproject.util.StringUtils;
import java.util.logging.Logger;
import javax.inject.Inject;
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.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;
public class ReadForumPostActivity extends BriarActivity
implements OnClickListener {
static final int RESULT_REPLY = RESULT_FIRST_USER;
static final int RESULT_PREV_NEXT = RESULT_FIRST_USER + 1;
private static final Logger LOG =
Logger.getLogger(ReadForumPostActivity.class.getName());
private GroupId groupId = null;
private String forumName = null;
private long minTimestamp = -1;
private ImageButton prevButton = null, nextButton = null;
private ImageButton replyButton = null;
private TextView content = null;
private int position = -1;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
private volatile MessageId messageId = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("briar.GROUP_ID");
if (b == null) throw new IllegalStateException();
groupId = new GroupId(b);
forumName = i.getStringExtra("briar.FORUM_NAME");
if (forumName == null) throw new IllegalStateException();
setTitle(forumName);
b = i.getByteArrayExtra("briar.MESSAGE_ID");
if (b == null) throw new IllegalStateException();
messageId = new MessageId(b);
String contentType = i.getStringExtra("briar.CONTENT_TYPE");
if (contentType == null) throw new IllegalStateException();
long timestamp = i.getLongExtra("briar.TIMESTAMP", -1);
if (timestamp == -1) throw new IllegalStateException();
minTimestamp = i.getLongExtra("briar.MIN_TIMESTAMP", -1);
if (minTimestamp == -1) throw new IllegalStateException();
position = i.getIntExtra("briar.POSITION", -1);
if (position == -1) throw new IllegalStateException();
String authorName = i.getStringExtra("briar.AUTHOR_NAME");
String s = i.getStringExtra("briar.AUTHOR_STATUS");
if (s == null) throw new IllegalStateException();
Author.Status authorStatus = Author.Status.valueOf(s);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
ScrollView scrollView = new ScrollView(this);
scrollView.setLayoutParams(MATCH_WRAP_1);
LinearLayout message = new LinearLayout(this);
message.setOrientation(VERTICAL);
LinearLayout header = new LinearLayout(this);
header.setLayoutParams(MATCH_WRAP);
header.setOrientation(HORIZONTAL);
header.setGravity(CENTER_VERTICAL);
AuthorView author = new AuthorView(this);
author.setLayoutParams(WRAP_WRAP_1);
author.init(authorName, authorStatus);
header.addView(author);
int pad = LayoutUtils.getPadding(this);
TextView date = new TextView(this);
date.setPadding(0, pad, pad, pad);
date.setText(DateUtils.getRelativeTimeSpanString(this, timestamp));
header.addView(date);
message.addView(header);
if (contentType.equals("text/plain")) {
// Load and display the message body
content = new TextView(this);
content.setPadding(pad, 0, pad, pad);
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);
Resources res = getResources();
footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
prevButton = new ImageButton(this);
prevButton.setBackgroundResource(0);
prevButton.setImageResource(R.drawable.navigation_previous_item);
prevButton.setOnClickListener(this);
footer.addView(prevButton);
footer.addView(new ElasticHorizontalSpace(this));
nextButton = new ImageButton(this);
nextButton.setBackgroundResource(0);
nextButton.setImageResource(R.drawable.navigation_next_item);
nextButton.setOnClickListener(this);
footer.addView(nextButton);
footer.addView(new ElasticHorizontalSpace(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);
}
@Override
public void onPause() {
super.onPause();
if (isFinishing()) markMessageRead();
}
private void markMessageRead() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
db.setReadFlag(messageId, true);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Marking read took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void loadMessageBody() {
runOnDbThread(new Runnable() {
public void run() {
try {
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");
displayMessageBody(StringUtils.fromUtf8(body));
} catch (NoSuchMessageException e) {
finishOnUiThread();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayMessageBody(final String body) {
runOnUiThread(new Runnable() {
public void run() {
content.setText(body);
}
});
}
public void onClick(View view) {
if (view == prevButton) {
Intent i = new Intent();
i.putExtra("briar.POSITION", position - 1);
setResult(RESULT_PREV_NEXT, i);
finish();
} else if (view == nextButton) {
Intent i = new Intent();
i.putExtra("briar.POSITION", position + 1);
setResult(RESULT_PREV_NEXT, i);
finish();
} else if (view == replyButton) {
Intent i = new Intent(this, WriteForumPostActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
i.putExtra("briar.FORUM_NAME", forumName);
i.putExtra("briar.PARENT_ID", messageId.getBytes());
i.putExtra("briar.MIN_TIMESTAMP", minTimestamp);
startActivity(i);
setResult(RESULT_REPLY);
finish();
}
}
}

View File

@@ -0,0 +1,206 @@
package org.briarproject.android.forum;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.contact.SelectContactsDialog;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.Contact;
import org.briarproject.api.ContactId;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.messaging.GroupId;
import java.util.Collection;
import java.util.Collections;
import java.util.logging.Logger;
import javax.inject.Inject;
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.WRAP_WRAP;
public class ShareForumActivity extends BriarActivity
implements OnClickListener, NoContactsDialog.Listener,
SelectContactsDialog.Listener {
private static final Logger LOG =
Logger.getLogger(ShareForumActivity.class.getName());
private RadioGroup radioGroup = null;
private RadioButton shareWithAll = null, shareWithSome = null;
private Button shareButton = null;
private ProgressBar progress = null;
private boolean changed = false;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
private volatile GroupId groupId = null;
private volatile Collection<Contact> contacts = null;
private volatile Collection<ContactId> selected = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("briar.GROUP_ID");
if (b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String forumName = i.getStringExtra("briar.FORUM_NAME");
if (forumName == null) throw new IllegalStateException();
setTitle(forumName);
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_MATCH);
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
int pad = LayoutUtils.getPadding(this);
layout.setPadding(pad, pad, pad, pad);
radioGroup = new RadioGroup(this);
radioGroup.setOrientation(VERTICAL);
radioGroup.setPadding(0, 0, 0, pad);
shareWithAll = new RadioButton(this);
shareWithAll.setId(2);
shareWithAll.setText(R.string.forum_share_with_all);
shareWithAll.setOnClickListener(this);
radioGroup.addView(shareWithAll);
shareWithSome = new RadioButton(this);
shareWithSome.setId(3);
shareWithSome.setText(R.string.forum_share_with_some);
shareWithSome.setOnClickListener(this);
radioGroup.addView(shareWithSome);
layout.addView(radioGroup);
shareButton = new Button(this);
shareButton.setLayoutParams(WRAP_WRAP);
shareButton.setText(R.string.share_button);
shareButton.setOnClickListener(this);
layout.addView(shareButton);
progress = new ProgressBar(this);
progress.setLayoutParams(WRAP_WRAP);
progress.setIndeterminate(true);
progress.setVisibility(GONE);
layout.addView(progress);
setContentView(layout);
}
public void onClick(View view) {
if (view == shareWithAll) {
changed = true;
} else if (view == shareWithSome) {
changed = true;
if (contacts == null) loadVisibility();
else displayVisibility();
} else if (view == shareButton) {
if (changed) {
share();
} else {
finish();
}
}
}
private void share() {
// Replace the button with a progress bar
shareButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
// Update the group in a background thread
storeVisibility(shareWithAll.isChecked());
}
private void loadVisibility() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
contacts = db.getContacts();
selected = db.getVisibility(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayVisibility();
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayVisibility() {
runOnUiThread(new Runnable() {
public void run() {
if (contacts.isEmpty()) {
NoContactsDialog builder = new NoContactsDialog();
builder.setListener(ShareForumActivity.this);
builder.build(ShareForumActivity.this).show();
} else {
SelectContactsDialog builder = new SelectContactsDialog();
builder.setListener(ShareForumActivity.this);
builder.setContacts(contacts);
builder.setSelected(selected);
builder.build(ShareForumActivity.this).show();
}
}
});
}
private void storeVisibility(final boolean all) {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
db.setVisibleToAll(groupId, all);
if (!all) db.setVisibility(groupId, selected);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Update took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
finishOnUiThread();
}
});
}
public void contactCreationSelected() {
startActivity(new Intent(this, AddContactActivity.class));
}
public void contactCreationCancelled() {
radioGroup.clearCheck();
}
public void contactsSelected(Collection<ContactId> selected) {
this.selected = Collections.unmodifiableCollection(selected);
share();
}
public void contactSelectionCancelled() {
radioGroup.clearCheck();
}
}

View File

@@ -0,0 +1,306 @@
package org.briarproject.android.forum;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
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.RelativeLayout;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.identity.LocalAuthorItem;
import org.briarproject.android.identity.LocalAuthorItemComparator;
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
import org.briarproject.android.util.CommonLayoutParams;
import org.briarproject.android.util.LayoutUtils;
import org.briarproject.api.AuthorId;
import org.briarproject.api.LocalAuthor;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.crypto.KeyParser;
import org.briarproject.api.crypto.PrivateKey;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
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 org.briarproject.util.StringUtils;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import static android.widget.LinearLayout.VERTICAL;
import static android.widget.RelativeLayout.ALIGN_PARENT_LEFT;
import static android.widget.RelativeLayout.ALIGN_PARENT_RIGHT;
import static android.widget.RelativeLayout.CENTER_VERTICAL;
import static android.widget.RelativeLayout.LEFT_OF;
import static android.widget.RelativeLayout.RIGHT_OF;
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;
public class WriteForumPostActivity extends BriarActivity
implements OnItemSelectedListener, OnClickListener {
private static final int REQUEST_CREATE_IDENTITY = 2;
private static final Logger LOG =
Logger.getLogger(WriteForumPostActivity.class.getName());
@Inject @CryptoExecutor private Executor cryptoExecutor;
private LocalAuthorSpinnerAdapter adapter = null;
private Spinner spinner = null;
private ImageButton sendButton = null;
private EditText content = null;
private AuthorId localAuthorId = null;
private GroupId groupId = null;
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject private volatile CryptoComponent crypto;
@Inject private volatile MessageFactory messageFactory;
private volatile MessageId parentId = null;
private volatile long minTimestamp = -1;
private volatile LocalAuthor localAuthor = null;
private volatile Group group = null;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
Intent i = getIntent();
byte[] b = i.getByteArrayExtra("briar.GROUP_ID");
if (b == null) throw new IllegalStateException();
groupId = new GroupId(b);
String forumName = i.getStringExtra("briar.FORUM_NAME");
if (forumName == null) throw new IllegalStateException();
setTitle(forumName);
minTimestamp = i.getLongExtra("briar.MIN_TIMESTAMP", -1);
if (minTimestamp == -1) throw new IllegalStateException();
b = i.getByteArrayExtra("briar.PARENT_ID");
if (b != null) parentId = new MessageId(b);
if (state != null) {
b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
if (b != null) localAuthorId = new AuthorId(b);
}
LinearLayout layout = new LinearLayout(this);
layout.setLayoutParams(MATCH_WRAP);
layout.setOrientation(VERTICAL);
int pad = LayoutUtils.getPadding(this);
layout.setPadding(pad, 0, pad, pad);
RelativeLayout header = new RelativeLayout(this);
TextView from = new TextView(this);
from.setId(1);
from.setTextSize(18);
from.setText(R.string.from);
RelativeLayout.LayoutParams left = CommonLayoutParams.relative();
left.addRule(ALIGN_PARENT_LEFT);
left.addRule(CENTER_VERTICAL);
header.addView(from, left);
adapter = new LocalAuthorSpinnerAdapter(this, true);
spinner = new Spinner(this);
spinner.setId(2);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(this);
RelativeLayout.LayoutParams between = CommonLayoutParams.relative();
between.addRule(CENTER_VERTICAL);
between.addRule(RIGHT_OF, 1);
between.addRule(LEFT_OF, 3);
header.addView(spinner, between);
sendButton = new ImageButton(this);
sendButton.setId(3);
sendButton.setBackgroundResource(0);
sendButton.setImageResource(R.drawable.social_send_now);
sendButton.setEnabled(false); // Enabled after loading the group
sendButton.setOnClickListener(this);
RelativeLayout.LayoutParams right = CommonLayoutParams.relative();
right.addRule(ALIGN_PARENT_RIGHT);
right.addRule(CENTER_VERTICAL);
header.addView(sendButton, right);
layout.addView(header);
content = new EditText(this);
content.setId(4);
int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
| TYPE_TEXT_FLAG_CAP_SENTENCES;
content.setInputType(inputType);
content.setHint(R.string.forum_post_hint);
layout.addView(content);
setContentView(layout);
}
@Override
public void onResume() {
super.onResume();
loadAuthorsAndGroup();
}
private void loadAuthorsAndGroup() {
runOnDbThread(new Runnable() {
public void run() {
try {
long now = System.currentTimeMillis();
Collection<LocalAuthor> localAuthors = db.getLocalAuthors();
group = db.getGroup(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Load took " + duration + " ms");
displayAuthorsAndGroup(localAuthors);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayAuthorsAndGroup(
final Collection<LocalAuthor> localAuthors) {
runOnUiThread(new Runnable() {
public void run() {
if (localAuthors.isEmpty()) throw new IllegalStateException();
adapter.clear();
for (LocalAuthor a : localAuthors)
adapter.add(new LocalAuthorItem(a));
adapter.sort(LocalAuthorItemComparator.INSTANCE);
adapter.notifyDataSetChanged();
int count = adapter.getCount();
for (int i = 0; i < count; i++) {
LocalAuthorItem item = adapter.getItem(i);
if (item == LocalAuthorItem.ANONYMOUS) continue;
if (item == LocalAuthorItem.NEW) continue;
if (item.getLocalAuthor().getId().equals(localAuthorId)) {
localAuthor = item.getLocalAuthor();
spinner.setSelection(i);
break;
}
}
setTitle(group.getName());
sendButton.setEnabled(true);
}
});
}
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
if (localAuthorId != null) {
byte[] b = localAuthorId.getBytes();
state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
}
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
if (b == null) throw new IllegalStateException();
localAuthorId = new AuthorId(b);
loadAuthorsAndGroup();
}
}
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
LocalAuthorItem item = adapter.getItem(position);
if (item == LocalAuthorItem.ANONYMOUS) {
localAuthor = null;
localAuthorId = null;
} else if (item == LocalAuthorItem.NEW) {
localAuthor = null;
localAuthorId = null;
Intent i = new Intent(this, CreateIdentityActivity.class);
startActivityForResult(i, REQUEST_CREATE_IDENTITY);
} else {
localAuthor = item.getLocalAuthor();
localAuthorId = localAuthor.getId();
}
}
public void onNothingSelected(AdapterView<?> parent) {
localAuthor = null;
localAuthorId = null;
}
public void onClick(View view) {
if (group == null) throw new IllegalStateException();
String message = content.getText().toString();
if (message.equals("")) return;
createMessage(StringUtils.toUtf8(message));
Toast.makeText(this, R.string.post_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 newest post
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, minTimestamp);
Message m;
try {
if (localAuthor == null) {
m = messageFactory.createAnonymousMessage(parentId,
group, "text/plain", timestamp, body);
} else {
KeyParser keyParser = crypto.getSignatureKeyParser();
byte[] b = localAuthor.getPrivateKey();
PrivateKey authorKey = keyParser.parsePrivateKey(b);
m = messageFactory.createPseudonymousMessage(parentId,
group, localAuthor, authorKey, "text/plain",
timestamp, body);
}
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
storeMessage(m);
}
});
}
private void storeMessage(final Message m) {
runOnDbThread(new Runnable() {
public void run() {
try {
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);
}
}
});
}
}