diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index fc74fb36e..6020c390a 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -45,9 +45,25 @@
android:label="@string/contact_list_title" >
+
+
+
+
+
+
+
+
@@ -58,16 +74,12 @@
+ android:label="@string/groups_title" >
-
-
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java b/briar-android/src/net/sf/briar/android/GroupNameComparator.java
similarity index 52%
rename from briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java
rename to briar-android/src/net/sf/briar/android/GroupNameComparator.java
index 47fdd6b53..8e397be1b 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupNameComparator.java
+++ b/briar-android/src/net/sf/briar/android/GroupNameComparator.java
@@ -1,12 +1,13 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android;
import java.util.Comparator;
import net.sf.briar.api.messaging.Group;
-class GroupNameComparator implements Comparator {
+public class GroupNameComparator implements Comparator {
- static final GroupNameComparator INSTANCE = new GroupNameComparator();
+ public static final GroupNameComparator INSTANCE =
+ new GroupNameComparator();
public int compare(Group a, Group b) {
return String.CASE_INSENSITIVE_ORDER.compare(a.getName(), b.getName());
diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
index 5596b1cec..f33052720 100644
--- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
+++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java
@@ -14,6 +14,7 @@ import java.util.logging.Logger;
import net.sf.briar.R;
import net.sf.briar.android.BriarService.BriarBinder;
import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.blogs.BlogListActivity;
import net.sf.briar.android.contact.ContactListActivity;
import net.sf.briar.android.groups.GroupListActivity;
import net.sf.briar.android.messages.ConversationListActivity;
@@ -183,12 +184,8 @@ public class HomeScreenActivity extends BriarActivity {
groupsButton.setText(R.string.groups_button);
groupsButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
- Intent i = new Intent(HomeScreenActivity.this,
- GroupListActivity.class);
- i.putExtra("net.sf.briar.RESTRICTED", false);
- i.putExtra("net.sf.briar.TITLE",
- getResources().getString(R.string.groups_title));
- startActivity(i);
+ startActivity(new Intent(HomeScreenActivity.this,
+ GroupListActivity.class));
}
});
buttons.add(groupsButton);
@@ -201,12 +198,8 @@ public class HomeScreenActivity extends BriarActivity {
blogsButton.setText(R.string.blogs_button);
blogsButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
- Intent i = new Intent(HomeScreenActivity.this,
- GroupListActivity.class);
- i.putExtra("net.sf.briar.RESTRICTED", true);
- i.putExtra("net.sf.briar.TITLE",
- getResources().getString(R.string.blogs_title));
- startActivity(i);
+ startActivity(new Intent(HomeScreenActivity.this,
+ BlogListActivity.class));
}
});
buttons.add(blogsButton);
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
new file mode 100644
index 000000000..b30e27640
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java
@@ -0,0 +1,237 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER_HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static net.sf.briar.android.blogs.ReadBlogPostActivity.RESULT_NEXT;
+import static net.sf.briar.android.blogs.ReadBlogPostActivity.RESULT_PREV;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.AscendingHeaderComparator;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.RatingChangedEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.GroupId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class BlogActivity extends BriarActivity implements DatabaseListener,
+OnClickListener, OnItemClickListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(BlogActivity.class.getName());
+
+ private final BriarServiceConnection serviceConnection =
+ new BriarServiceConnection();
+
+ private String groupName = null;
+ private BlogAdapter adapter = null;
+ private ListView list = null;
+
+ // Fields that are accessed from background threads must be volatile
+ @Inject private volatile DatabaseComponent db;
+ @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+ private volatile GroupId groupId = null;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(null);
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+ if(b == null) throw new IllegalStateException();
+ groupId = new GroupId(b);
+ groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+ if(groupName == null) throw new IllegalStateException();
+ setTitle(groupName);
+
+ LinearLayout layout = new LinearLayout(this);
+ layout.setLayoutParams(MATCH_MATCH);
+ layout.setOrientation(VERTICAL);
+ layout.setGravity(CENTER_HORIZONTAL);
+
+ adapter = new BlogAdapter(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);
+
+ layout.addView(new HorizontalBorder(this));
+
+ ImageButton composeButton = new ImageButton(this);
+ composeButton.setBackgroundResource(0);
+ composeButton.setImageResource(R.drawable.content_new_email);
+ composeButton.setOnClickListener(this);
+ layout.addView(composeButton);
+
+ setContentView(layout);
+
+ // Bind to the service so we can wait for it to start
+ bindService(new Intent(BriarService.class.getName()),
+ serviceConnection, 0);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ db.addListener(this);
+ loadHeaders();
+ }
+
+ private void loadHeaders() {
+ dbUiExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ serviceConnection.waitForStartup();
+ long now = System.currentTimeMillis();
+ Collection headers =
+ db.getMessageHeaders(groupId);
+ long duration = System.currentTimeMillis() - now;
+ if(LOG.isLoggable(INFO))
+ LOG.info("Load took " + duration + " ms");
+ displayHeaders(headers);
+ } catch(NoSuchSubscriptionException e) {
+ if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+ finishOnUiThread();
+ } catch(DbException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ } catch(InterruptedException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Interrupted while waiting for service");
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ private void displayHeaders(final Collection headers) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ adapter.clear();
+ for(GroupMessageHeader h : headers) adapter.add(h);
+ adapter.sort(AscendingHeaderComparator.INSTANCE);
+ adapter.notifyDataSetChanged();
+ selectFirstUnread();
+ }
+ });
+ }
+
+ private void selectFirstUnread() {
+ int firstUnread = -1, count = adapter.getCount();
+ for(int i = 0; i < count; i++) {
+ if(!adapter.getItem(i).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 == RESULT_PREV) {
+ int position = request - 1;
+ if(position >= 0 && position < adapter.getCount())
+ displayMessage(position);
+ } else if(result == RESULT_NEXT) {
+ int position = request + 1;
+ if(position >= 0 && position < adapter.getCount())
+ displayMessage(position);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ db.removeListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unbindService(serviceConnection);
+ }
+
+ public void eventOccurred(DatabaseEvent e) {
+ if(e instanceof GroupMessageAddedEvent) {
+ GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
+ if(g.getGroup().getId().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();
+ } else if(e instanceof RatingChangedEvent) {
+ if(LOG.isLoggable(INFO)) LOG.info("Rating changed, reloading");
+ loadHeaders();
+ } else if(e instanceof SubscriptionRemovedEvent) {
+ SubscriptionRemovedEvent s = (SubscriptionRemovedEvent) e;
+ if(s.getGroup().getId().equals(groupId)) {
+ if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+ finishOnUiThread();
+ }
+ }
+ }
+
+ public void onClick(View view) {
+ Intent i = new Intent(this, WriteBlogPostActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+ startActivity(i);
+ }
+
+ public void onItemClick(AdapterView> parent, View view, int position,
+ long id) {
+ displayMessage(position);
+ }
+
+ private void displayMessage(int position) {
+ GroupMessageHeader item = adapter.getItem(position);
+ Intent i = new Intent(this, ReadBlogPostActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+ i.putExtra("net.sf.briar.GROUP_NAME", groupName);
+ i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
+ Author author = item.getAuthor();
+ if(author != null) {
+ i.putExtra("net.sf.briar.AUTHOR_ID", author.getId().getBytes());
+ i.putExtra("net.sf.briar.AUTHOR_NAME", author.getName());
+ i.putExtra("net.sf.briar.RATING", item.getRating().toString());
+ }
+ i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType());
+ i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp());
+ startActivityForResult(i, position);
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java
new file mode 100644
index 000000000..969b520e3
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogAdapter.java
@@ -0,0 +1,114 @@
+package net.sf.briar.android.blogs;
+
+import static android.graphics.Typeface.BOLD;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.View.INVISIBLE;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+import static net.sf.briar.api.messaging.Rating.GOOD;
+import static net.sf.briar.api.messaging.Rating.UNRATED;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Rating;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class BlogAdapter extends ArrayAdapter {
+
+ BlogAdapter(Context ctx) {
+ super(ctx, android.R.layout.simple_expandable_list_item_1,
+ new ArrayList());
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ GroupMessageHeader item = getItem(position);
+ Context ctx = getContext();
+
+ // FIXME: Use a RelativeLayout
+ LinearLayout layout = new LinearLayout(ctx);
+ layout.setOrientation(HORIZONTAL);
+ if(!item.isRead()) {
+ Resources res = ctx.getResources();
+ layout.setBackgroundColor(res.getColor(R.color.unread_background));
+ }
+
+ LinearLayout innerLayout = new LinearLayout(ctx);
+ // Give me all the unused width
+ innerLayout.setLayoutParams(WRAP_WRAP_1);
+ innerLayout.setOrientation(VERTICAL);
+
+ LinearLayout authorLayout = new LinearLayout(ctx);
+ authorLayout.setOrientation(HORIZONTAL);
+ authorLayout.setGravity(CENTER_VERTICAL);
+
+ ImageView thumb = new ImageView(ctx);
+ thumb.setPadding(10, 10, 10, 10);
+ Rating rating = item.getRating();
+ if(rating == GOOD) thumb.setImageResource(R.drawable.rating_good);
+ else thumb.setImageResource(R.drawable.rating_bad);
+ if(rating == UNRATED) thumb.setVisibility(INVISIBLE);
+ authorLayout.addView(thumb);
+
+ 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);
+ Author author = item.getAuthor();
+ Resources res = ctx.getResources();
+ if(author == null) {
+ name.setTextColor(res.getColor(R.color.anonymous_author));
+ name.setText(R.string.anonymous);
+ } else {
+ name.setText(author.getName());
+ }
+ authorLayout.addView(name);
+ innerLayout.addView(authorLayout);
+
+ if(item.getContentType().equals("text/plain")) {
+ TextView subject = new TextView(ctx);
+ subject.setTextSize(14);
+ subject.setMaxLines(2);
+ subject.setPadding(10, 0, 10, 10);
+ if(!item.isRead()) subject.setTypeface(null, BOLD);
+ String s = item.getSubject();
+ subject.setText(s == null ? "" : s);
+ innerLayout.addView(subject);
+ } else {
+ LinearLayout attachmentLayout = new LinearLayout(ctx);
+ attachmentLayout.setOrientation(HORIZONTAL);
+ ImageView attachment = new ImageView(ctx);
+ attachment.setPadding(10, 0, 10, 10);
+ attachment.setImageResource(R.drawable.content_attachment);
+ attachmentLayout.addView(attachment);
+ attachmentLayout.addView(new HorizontalSpace(ctx));
+ innerLayout.addView(attachmentLayout);
+ }
+ layout.addView(innerLayout);
+
+ TextView date = new TextView(ctx);
+ date.setTextSize(14);
+ date.setPadding(0, 10, 10, 10);
+ long then = item.getTimestamp(), now = System.currentTimeMillis();
+ date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+ layout.addView(date);
+
+ return layout;
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
new file mode 100644
index 000000000..b6dfdfdca
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java
@@ -0,0 +1,314 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_HORIZONTAL;
+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 net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarFragmentActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.db.NoSuchSubscriptionException;
+import net.sf.briar.api.db.event.DatabaseEvent;
+import net.sf.briar.api.db.event.DatabaseListener;
+import net.sf.briar.api.db.event.GroupMessageAddedEvent;
+import net.sf.briar.api.db.event.MessageExpiredEvent;
+import net.sf.briar.api.db.event.SubscriptionRemovedEvent;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupId;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+import com.google.inject.Inject;
+
+public class BlogListActivity extends BriarFragmentActivity
+implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener {
+
+ private static final Logger LOG =
+ Logger.getLogger(BlogListActivity.class.getName());
+
+ private final BriarServiceConnection serviceConnection =
+ new BriarServiceConnection();
+
+ private BlogListAdapter adapter = null;
+ private ListView list = null;
+ private ImageButton newBlogButton = null, composeButton = null;
+
+ // Fields that are accessed from background threads must be volatile
+ @Inject private volatile DatabaseComponent db;
+ @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(null);
+ LinearLayout layout = new LinearLayout(this);
+ layout.setLayoutParams(MATCH_MATCH);
+ layout.setOrientation(VERTICAL);
+ layout.setGravity(CENTER_HORIZONTAL);
+
+ adapter = new BlogListAdapter(this);
+ list = new ListView(this);
+ // Give me all the width and all the unused height
+ list.setLayoutParams(MATCH_WRAP_1);
+ list.setAdapter(adapter);
+ list.setOnItemClickListener(adapter);
+ layout.addView(list);
+
+ layout.addView(new HorizontalBorder(this));
+
+ LinearLayout footer = new LinearLayout(this);
+ footer.setLayoutParams(MATCH_WRAP);
+ footer.setOrientation(HORIZONTAL);
+ footer.setGravity(CENTER);
+ footer.addView(new HorizontalSpace(this));
+
+ newBlogButton = new ImageButton(this);
+ newBlogButton.setBackgroundResource(0);
+ newBlogButton.setImageResource(R.drawable.social_new_blog);
+ newBlogButton.setOnClickListener(this);
+ footer.addView(newBlogButton);
+ footer.addView(new HorizontalSpace(this));
+
+ composeButton = new ImageButton(this);
+ composeButton.setBackgroundResource(0);
+ composeButton.setImageResource(R.drawable.content_new_email);
+ composeButton.setOnClickListener(this);
+ footer.addView(composeButton);
+ footer.addView(new HorizontalSpace(this));
+ layout.addView(footer);
+
+ setContentView(layout);
+
+ // Bind to the service so we can wait for it to start
+ bindService(new Intent(BriarService.class.getName()),
+ serviceConnection, 0);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ db.addListener(this);
+ loadHeaders();
+ }
+
+ private void loadHeaders() {
+ clearHeaders();
+ dbUiExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ serviceConnection.waitForStartup();
+ long now = System.currentTimeMillis();
+ Set local = new HashSet();
+ for(Group g : db.getLocalGroups()) local.add(g.getId());
+ for(Group g : db.getSubscriptions()) {
+ if(!g.isRestricted()) continue;
+ boolean postable = local.contains(g.getId());
+ try {
+ Collection headers =
+ db.getMessageHeaders(g.getId());
+ displayHeaders(g, postable, headers);
+ } catch(NoSuchSubscriptionException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Subscription removed");
+ }
+ }
+ long duration = System.currentTimeMillis() - now;
+ if(LOG.isLoggable(INFO))
+ LOG.info("Full load 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 service");
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ private void clearHeaders() {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ adapter.clear();
+ }
+ });
+ }
+
+ private void displayHeaders(final Group g, final boolean postable,
+ final Collection headers) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ // Remove the old item, if any
+ BlogListItem item = findGroup(g.getId());
+ if(item != null) adapter.remove(item);
+ // Add a new item
+ adapter.add(new BlogListItem(g, postable, headers));
+ adapter.sort(GroupComparator.INSTANCE);
+ adapter.notifyDataSetChanged();
+ selectFirstUnread();
+ }
+ });
+ }
+
+ private BlogListItem findGroup(GroupId g) {
+ int count = adapter.getCount();
+ for(int i = 0; i < count; i++) {
+ BlogListItem item = adapter.getItem(i);
+ if(item.getGroupId().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();
+ db.removeListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unbindService(serviceConnection);
+ }
+
+ public void onClick(View view) {
+ if(view == newBlogButton) {
+ startActivity(new Intent(this, CreateBlogActivity.class));
+ } else if(view == composeButton) {
+ if(countPostableGroups() == 0) {
+ NoBlogsDialog dialog = new NoBlogsDialog();
+ dialog.setListener(this);
+ dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
+ } else {
+ startActivity(new Intent(this, WriteBlogPostActivity.class));
+ }
+ }
+ }
+
+ private int countPostableGroups() {
+ int postable = 0, count = adapter.getCount();
+ for(int i = 0; i < count; i++)
+ if(adapter.getItem(i).isPostable()) postable++;
+ return postable;
+ }
+
+ public void eventOccurred(DatabaseEvent e) {
+ if(e instanceof GroupMessageAddedEvent) {
+ Group g = ((GroupMessageAddedEvent) e).getGroup();
+ if(g.isRestricted()) {
+ if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
+ loadHeaders(g);
+ }
+ } else if(e instanceof MessageExpiredEvent) {
+ if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading");
+ loadHeaders();
+ } else if(e instanceof SubscriptionRemovedEvent) {
+ Group g = ((SubscriptionRemovedEvent) e).getGroup();
+ if(g.isRestricted()) {
+ // Reload the group, expecting NoSuchSubscriptionException
+ if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
+ loadHeaders(g);
+ }
+ }
+ }
+
+ private void loadHeaders(final Group g) {
+ dbUiExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ serviceConnection.waitForStartup();
+ long now = System.currentTimeMillis();
+ Collection headers =
+ db.getMessageHeaders(g.getId());
+ boolean postable = db.getLocalGroups().contains(g);
+ long duration = System.currentTimeMillis() - now;
+ if(LOG.isLoggable(INFO))
+ LOG.info("Partial load took " + duration + " ms");
+ displayHeaders(g, postable, headers);
+ } catch(NoSuchSubscriptionException e) {
+ if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
+ removeGroup(g.getId());
+ } catch(DbException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ } catch(InterruptedException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Interrupted while waiting for service");
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ private void removeGroup(final GroupId g) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ BlogListItem item = findGroup(g);
+ if(item != null) {
+ adapter.remove(item);
+ selectFirstUnread();
+ }
+ }
+ });
+ }
+
+ public void createGroupButtonClicked() {
+ startActivity(new Intent(this, CreateBlogActivity.class));
+ }
+
+ public void cancelButtonClicked() {
+ // That's nice dear
+ }
+
+ private static class GroupComparator implements Comparator {
+
+ private static final GroupComparator INSTANCE = new GroupComparator();
+
+ public int compare(BlogListItem a, BlogListItem b) {
+ // The item with the newest message comes first
+ long aTime = a.getTimestamp(), bTime = b.getTimestamp();
+ if(aTime > bTime) return -1;
+ if(aTime < bTime) return 1;
+ // Break ties by group name
+ String aName = a.getGroupName(), bName = b.getGroupName();
+ return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
new file mode 100644
index 000000000..f37c1f04f
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java
@@ -0,0 +1,108 @@
+package net.sf.briar.android.blogs;
+
+import static android.graphics.Typeface.BOLD;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+import static java.text.DateFormat.SHORT;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+
+import java.util.ArrayList;
+
+import net.sf.briar.R;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+class BlogListAdapter extends ArrayAdapter
+implements OnItemClickListener {
+
+ BlogListAdapter(Context ctx) {
+ super(ctx, android.R.layout.simple_expandable_list_item_1,
+ new ArrayList());
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ BlogListItem item = getItem(position);
+ Context ctx = getContext();
+ Resources res = ctx.getResources();
+
+ LinearLayout layout = new LinearLayout(ctx);
+ layout.setOrientation(HORIZONTAL);
+ if(item.getUnreadCount() > 0)
+ layout.setBackgroundColor(res.getColor(R.color.unread_background));
+
+ LinearLayout innerLayout = new LinearLayout(ctx);
+ // Give me all the unused width
+ innerLayout.setLayoutParams(WRAP_WRAP_1);
+ innerLayout.setOrientation(VERTICAL);
+
+ TextView name = new TextView(ctx);
+ name.setTextSize(18);
+ name.setMaxLines(1);
+ name.setPadding(10, 10, 10, 10);
+ int unread = item.getUnreadCount();
+ if(unread > 0) name.setText(item.getGroupName() + " (" + unread + ")");
+ else name.setText(item.getGroupName());
+ innerLayout.addView(name);
+
+ if(item.isEmpty()) {
+ TextView noPosts = new TextView(ctx);
+ noPosts.setTextSize(14);
+ noPosts.setPadding(10, 0, 10, 10);
+ noPosts.setTextColor(res.getColor(R.color.no_posts));
+ noPosts.setText(R.string.no_posts);
+ innerLayout.addView(noPosts);
+ layout.addView(innerLayout);
+ } else {
+ if(item.getContentType().equals("text/plain")) {
+ TextView subject = new TextView(ctx);
+ subject.setTextSize(14);
+ subject.setMaxLines(2);
+ subject.setPadding(10, 0, 10, 10);
+ if(item.getUnreadCount() > 0) subject.setTypeface(null, BOLD);
+ String s = item.getSubject();
+ subject.setText(s == null ? "" : s);
+ innerLayout.addView(subject);
+ } else {
+ LinearLayout attachmentLayout = new LinearLayout(ctx);
+ attachmentLayout.setOrientation(HORIZONTAL);
+ ImageView attachment = new ImageView(ctx);
+ attachment.setPadding(10, 0, 10, 10);
+ attachment.setImageResource(R.drawable.content_attachment);
+ attachmentLayout.addView(attachment);
+ attachmentLayout.addView(new HorizontalSpace(ctx));
+ innerLayout.addView(attachmentLayout);
+ }
+ layout.addView(innerLayout);
+
+ TextView date = new TextView(ctx);
+ date.setTextSize(14);
+ date.setPadding(0, 10, 10, 10);
+ long then = item.getTimestamp(), now = System.currentTimeMillis();
+ date.setText(DateUtils.formatSameDayTime(then, now, SHORT, SHORT));
+ layout.addView(date);
+ }
+
+ return layout;
+ }
+
+ public void onItemClick(AdapterView> parent, View view, int position,
+ long id) {
+ BlogListItem item = getItem(position);
+ Intent i = new Intent(getContext(), BlogActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
+ i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
+ getContext().startActivity(i);
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
new file mode 100644
index 000000000..4d955fcc5
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java
@@ -0,0 +1,85 @@
+package net.sf.briar.android.blogs;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.briar.android.DescendingHeaderComparator;
+import net.sf.briar.api.Author;
+import net.sf.briar.api.db.GroupMessageHeader;
+import net.sf.briar.api.messaging.Group;
+import net.sf.briar.api.messaging.GroupId;
+
+class BlogListItem {
+
+ private final Group group;
+ private final boolean postable, empty;
+ private final String authorName, contentType, subject;
+ private final long timestamp;
+ private final int unread;
+
+ BlogListItem(Group group, boolean postable,
+ Collection headers) {
+ this.group = group;
+ this.postable = postable;
+ empty = headers.isEmpty();
+ if(empty) {
+ authorName = null;
+ contentType = null;
+ subject = null;
+ timestamp = 0;
+ unread = 0;
+ } else {
+ List list =
+ new ArrayList(headers);
+ Collections.sort(list, DescendingHeaderComparator.INSTANCE);
+ GroupMessageHeader newest = list.get(0);
+ Author a = newest.getAuthor();
+ if(a == null) authorName = null;
+ else authorName = a.getName();
+ contentType = newest.getContentType();
+ subject = newest.getSubject();
+ timestamp = newest.getTimestamp();
+ int unread = 0;
+ for(GroupMessageHeader h : list) if(!h.isRead()) unread++;
+ this.unread = unread;
+ }
+ }
+
+ GroupId getGroupId() {
+ return group.getId();
+ }
+
+ String getGroupName() {
+ return group.getName();
+ }
+
+ boolean isPostable() {
+ return postable;
+ }
+
+ boolean isEmpty() {
+ return empty;
+ }
+
+ String getAuthorName() {
+ return authorName;
+ }
+
+ String getContentType() {
+ return contentType;
+ }
+
+ String getSubject() {
+ return subject;
+ }
+
+ long getTimestamp() {
+ return timestamp;
+ }
+
+ int getUnreadCount() {
+ return unread;
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java
rename to briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
index 46f6c9b8f..6d9ce55b7 100644
--- a/briar-android/src/net/sf/briar/android/groups/CreateBlogActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
diff --git a/briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java b/briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
similarity index 96%
rename from briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java
rename to briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
index 34a2f3ffa..8dc22083a 100644
--- a/briar-android/src/net/sf/briar/android/groups/LocalGroupSpinnerAdapter.java
+++ b/briar-android/src/net/sf/briar/android/blogs/LocalGroupSpinnerAdapter.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
import java.util.ArrayList;
diff --git a/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java b/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java
new file mode 100644
index 000000000..15b15cfbe
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/NoBlogsDialog.java
@@ -0,0 +1,43 @@
+package net.sf.briar.android.blogs;
+
+import net.sf.briar.R;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+public class NoBlogsDialog extends DialogFragment {
+
+ private Listener listener = null;
+
+ void setListener(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle state) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.no_blogs);
+ builder.setPositiveButton(R.string.create_button,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ listener.createGroupButtonClicked();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel_button,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ listener.cancelButtonClicked();
+ }
+ });
+ return builder.create();
+ }
+
+ interface Listener {
+
+ void createGroupButtonClicked();
+
+ void cancelButtonClicked();
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
new file mode 100644
index 000000000..bb97cbca5
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/blogs/ReadBlogPostActivity.java
@@ -0,0 +1,375 @@
+package net.sf.briar.android.blogs;
+
+import static android.view.Gravity.CENTER;
+import static android.view.Gravity.CENTER_VERTICAL;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+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 net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP;
+import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
+import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1;
+import static net.sf.briar.api.messaging.Rating.BAD;
+import static net.sf.briar.api.messaging.Rating.GOOD;
+import static net.sf.briar.api.messaging.Rating.UNRATED;
+
+import java.io.UnsupportedEncodingException;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import net.sf.briar.R;
+import net.sf.briar.android.BriarActivity;
+import net.sf.briar.android.BriarService;
+import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.widgets.HorizontalBorder;
+import net.sf.briar.android.widgets.HorizontalSpace;
+import net.sf.briar.api.AuthorId;
+import net.sf.briar.api.android.BundleEncrypter;
+import net.sf.briar.api.android.DatabaseUiExecutor;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.NoSuchMessageException;
+import net.sf.briar.api.messaging.GroupId;
+import net.sf.briar.api.messaging.MessageId;
+import net.sf.briar.api.messaging.Rating;
+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.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.google.inject.Inject;
+
+public class ReadBlogPostActivity extends BriarActivity
+implements OnClickListener {
+
+ static final int RESULT_REPLY = RESULT_FIRST_USER;
+ static final int RESULT_PREV = RESULT_FIRST_USER + 1;
+ static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
+
+ private static final Logger LOG =
+ Logger.getLogger(ReadBlogPostActivity.class.getName());
+
+ private final BriarServiceConnection serviceConnection =
+ new BriarServiceConnection();
+
+ @Inject private BundleEncrypter bundleEncrypter;
+ private GroupId groupId = null;
+ private Rating rating = UNRATED;
+ private boolean read;
+ private ImageView thumb = null;
+ private ImageButton goodButton = null, badButton = null, readButton = null;
+ private ImageButton 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;
+ private volatile MessageId messageId = null;
+ private volatile AuthorId authorId = null;
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(null);
+
+ Intent i = getIntent();
+ byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
+ if(b == null) throw new IllegalStateException();
+ groupId = new GroupId(b);
+ String groupName = i.getStringExtra("net.sf.briar.GROUP_NAME");
+ if(groupName == null) throw new IllegalStateException();
+ setTitle(groupName);
+ b = i.getByteArrayExtra("net.sf.briar.MESSAGE_ID");
+ if(b == null) throw new IllegalStateException();
+ messageId = new MessageId(b);
+ String authorName = null;
+ b = i.getByteArrayExtra("net.sf.briar.AUTHOR_ID");
+ if(b != null) {
+ authorId = new AuthorId(b);
+ authorName = i.getStringExtra("net.sf.briar.AUTHOR_NAME");
+ if(authorName == null) throw new IllegalStateException();
+ String r = i.getStringExtra("net.sf.briar.RATING");
+ if(r != null) rating = Rating.valueOf(r);
+ }
+ String contentType = i.getStringExtra("net.sf.briar.CONTENT_TYPE");
+ if(contentType == null) throw new IllegalStateException();
+ long timestamp = i.getLongExtra("net.sf.briar.TIMESTAMP", -1);
+ if(timestamp == -1) throw new IllegalStateException();
+
+ if(state != null && bundleEncrypter.decrypt(state)) {
+ read = state.getBoolean("net.sf.briar.READ");
+ } else {
+ read = false;
+ setReadInDatabase(true);
+ }
+
+ LinearLayout layout = new LinearLayout(this);
+ layout.setLayoutParams(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);
+
+ thumb = new ImageView(this);
+ thumb.setPadding(0, 10, 10, 10);
+ if(rating == GOOD) thumb.setImageResource(R.drawable.rating_good);
+ else thumb.setImageResource(R.drawable.rating_bad);
+ if(rating == UNRATED) thumb.setVisibility(INVISIBLE);
+ header.addView(thumb);
+
+ TextView author = new TextView(this);
+ // Give me all the unused width
+ author.setLayoutParams(WRAP_WRAP_1);
+ author.setTextSize(18);
+ author.setMaxLines(1);
+ author.setPadding(10, 10, 10, 10);
+ if(authorName == null) {
+ author.setTextColor(res.getColor(R.color.anonymous_author));
+ author.setText(R.string.anonymous);
+ } else {
+ author.setText(authorName);
+ }
+ header.addView(author);
+
+ TextView date = new TextView(this);
+ date.setTextSize(14);
+ date.setPadding(0, 10, 10, 10);
+ long now = System.currentTimeMillis();
+ date.setText(DateUtils.formatSameDayTime(timestamp, now, SHORT, SHORT));
+ header.addView(date);
+ message.addView(header);
+
+ if(contentType.equals("text/plain")) {
+ // Load and display the message body
+ content = new TextView(this);
+ content.setPadding(10, 0, 10, 10);
+ message.addView(content);
+ loadMessageBody();
+ }
+ scrollView.addView(message);
+ layout.addView(scrollView);
+
+ layout.addView(new HorizontalBorder(this));
+
+ LinearLayout footer = new LinearLayout(this);
+ footer.setLayoutParams(MATCH_WRAP);
+ footer.setOrientation(HORIZONTAL);
+ footer.setGravity(CENTER);
+
+ goodButton = new ImageButton(this);
+ goodButton.setBackgroundResource(0);
+ goodButton.setImageResource(R.drawable.rating_good);
+ if(authorName == null) goodButton.setEnabled(false);
+ else goodButton.setOnClickListener(this);
+ footer.addView(goodButton);
+ footer.addView(new HorizontalSpace(this));
+
+ badButton = new ImageButton(this);
+ badButton.setBackgroundResource(0);
+ badButton.setImageResource(R.drawable.rating_bad);
+ badButton.setOnClickListener(this);
+ if(authorName == null) badButton.setEnabled(false);
+ else badButton.setOnClickListener(this);
+ footer.addView(badButton);
+ footer.addView(new HorizontalSpace(this));
+
+ 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_all);
+ replyButton.setOnClickListener(this);
+ footer.addView(replyButton);
+ layout.addView(footer);
+
+ setContentView(layout);
+
+ // Bind to the service so we can wait for it to start
+ bindService(new Intent(BriarService.class.getName()),
+ serviceConnection, 0);
+ }
+
+ private void setReadInDatabase(final boolean read) {
+ dbUiExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ serviceConnection.waitForStartup();
+ 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 service");
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ private void setReadInUi(final boolean read) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ ReadBlogPostActivity.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 {
+ serviceConnection.waitForStartup();
+ 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");
+ finishOnUiThread();
+ } catch(DbException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ } catch(InterruptedException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Interrupted while waiting for service");
+ Thread.currentThread().interrupt();
+ } catch(UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle state) {
+ state.putBoolean("net.sf.briar.READ", read);
+ bundleEncrypter.encrypt(state);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unbindService(serviceConnection);
+ }
+
+ public void onClick(View view) {
+ if(view == goodButton) {
+ if(rating == BAD) setRatingInDatabase(UNRATED);
+ else if(rating == UNRATED) setRatingInDatabase(GOOD);
+ } else if(view == badButton) {
+ if(rating == GOOD) setRatingInDatabase(UNRATED);
+ else if(rating == UNRATED) setRatingInDatabase(BAD);
+ } else 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, WriteBlogPostActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+ i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+ startActivity(i);
+ setResult(RESULT_REPLY);
+ finish();
+ }
+ }
+
+ private void setRatingInDatabase(final Rating r) {
+ dbUiExecutor.execute(new Runnable() {
+ public void run() {
+ try {
+ serviceConnection.waitForStartup();
+ long now = System.currentTimeMillis();
+ db.setRating(authorId, r);
+ long duration = System.currentTimeMillis() - now;
+ if(LOG.isLoggable(INFO))
+ LOG.info("Setting rating took " + duration + " ms");
+ setRatingInUi(r);
+ } catch(DbException e) {
+ if(LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ } catch(InterruptedException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Interrupted while waiting for service");
+ Thread.currentThread().interrupt();
+ }
+ }
+ });
+ }
+
+ private void setRatingInUi(final Rating r) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ rating = r;
+ if(r == GOOD) {
+ thumb.setImageResource(R.drawable.rating_good);
+ thumb.setVisibility(VISIBLE);
+ } else if(r == BAD) {
+ thumb.setImageResource(R.drawable.rating_bad);
+ thumb.setVisibility(VISIBLE);
+ } else {
+ thumb.setVisibility(INVISIBLE);
+ }
+ }
+ });
+ }
+}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java b/briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
similarity index 99%
rename from briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java
rename to briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
index 37c8c79c8..15a807ab4 100644
--- a/briar-android/src/net/sf/briar/android/groups/WriteBlogPostActivity.java
+++ b/briar-android/src/net/sf/briar/android/blogs/WriteBlogPostActivity.java
@@ -1,4 +1,4 @@
-package net.sf.briar.android.groups;
+package net.sf.briar.android.blogs;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
@@ -22,6 +22,7 @@ import net.sf.briar.R;
import net.sf.briar.android.BriarActivity;
import net.sf.briar.android.BriarService;
import net.sf.briar.android.BriarService.BriarServiceConnection;
+import net.sf.briar.android.GroupNameComparator;
import net.sf.briar.android.identity.CreateIdentityActivity;
import net.sf.briar.android.identity.LocalAuthorItem;
import net.sf.briar.android.identity.LocalAuthorItemComparator;
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
index 710eb668a..0cdd42305 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java
@@ -4,8 +4,8 @@ import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.widget.LinearLayout.VERTICAL;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
-import static net.sf.briar.android.groups.ReadGroupMessageActivity.RESULT_NEXT;
-import static net.sf.briar.android.groups.ReadGroupMessageActivity.RESULT_PREV;
+import static net.sf.briar.android.groups.ReadGroupPostActivity.RESULT_NEXT;
+import static net.sf.briar.android.groups.ReadGroupPostActivity.RESULT_PREV;
import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH;
import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
@@ -53,7 +53,6 @@ OnClickListener, OnItemClickListener {
private final BriarServiceConnection serviceConnection =
new BriarServiceConnection();
- private boolean restricted = false;
private String groupName = null;
private GroupAdapter adapter = null;
private ListView list = null;
@@ -68,7 +67,6 @@ OnClickListener, OnItemClickListener {
super.onCreate(null);
Intent i = getIntent();
- restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
@@ -210,15 +208,9 @@ OnClickListener, OnItemClickListener {
}
public void onClick(View view) {
- if(restricted) {
- Intent i = new Intent(this, WriteBlogPostActivity.class);
- i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
- startActivity(i);
- } else {
- Intent i = new Intent(this, WriteGroupPostActivity.class);
- i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
- startActivity(i);
- }
+ Intent i = new Intent(this, WriteGroupPostActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+ startActivity(i);
}
public void onItemClick(AdapterView> parent, View view, int position,
@@ -228,8 +220,7 @@ OnClickListener, OnItemClickListener {
private void displayMessage(int position) {
GroupMessageHeader item = adapter.getItem(position);
- Intent i = new Intent(this, ReadGroupMessageActivity.class);
- i.putExtra("net.sf.briar.RESTRICTED", restricted);
+ Intent i = new Intent(this, ReadGroupPostActivity.class);
i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
i.putExtra("net.sf.briar.GROUP_NAME", groupName);
i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes());
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
index ccbfa93dc..083497f91 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java
@@ -12,8 +12,6 @@ import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_WRAP_1;
import java.util.Collection;
import java.util.Comparator;
-import java.util.HashSet;
-import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -61,7 +59,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
// Fields that are accessed from background threads must be volatile
@Inject private volatile DatabaseComponent db;
@Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor;
- private volatile boolean restricted = false;
@Override
public void onCreate(Bundle state) {
@@ -71,12 +68,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
layout.setOrientation(VERTICAL);
layout.setGravity(CENTER_HORIZONTAL);
- Intent i = getIntent();
- restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
- String title = i.getStringExtra("net.sf.briar.TITLE");
- if(title == null) throw new IllegalStateException();
- setTitle(title);
-
adapter = new GroupListAdapter(this);
list = new ListView(this);
// Give me all the width and all the unused height
@@ -95,9 +86,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
newGroupButton = new ImageButton(this);
newGroupButton.setBackgroundResource(0);
- if(restricted)
- newGroupButton.setImageResource(R.drawable.social_new_blog);
- else newGroupButton.setImageResource(R.drawable.social_new_chat);
+ newGroupButton.setImageResource(R.drawable.social_new_chat);
newGroupButton.setOnClickListener(this);
footer.addView(newGroupButton);
footer.addView(new HorizontalSpace(this));
@@ -131,33 +120,15 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
try {
serviceConnection.waitForStartup();
long now = System.currentTimeMillis();
- Collection subs = db.getSubscriptions();
- if(restricted) {
- Set local = new HashSet();
- for(Group g : db.getLocalGroups()) local.add(g.getId());
- for(Group g : subs) {
- if(!g.isRestricted()) continue;
- boolean postable = local.contains(g.getId());
- try {
- Collection headers =
- db.getMessageHeaders(g.getId());
- displayHeaders(g, postable, headers);
- } catch(NoSuchSubscriptionException e) {
- if(LOG.isLoggable(INFO))
- LOG.info("Subscription removed");
- }
- }
- } else {
- for(Group g : subs) {
- if(g.isRestricted()) continue;
- try {
- Collection headers =
- db.getMessageHeaders(g.getId());
- displayHeaders(g, true, headers);
- } catch(NoSuchSubscriptionException e) {
- if(LOG.isLoggable(INFO))
- LOG.info("Subscription removed");
- }
+ for(Group g : db.getSubscriptions()) {
+ if(g.isRestricted()) continue;
+ try {
+ Collection headers =
+ db.getMessageHeaders(g.getId());
+ displayHeaders(g, headers);
+ } catch(NoSuchSubscriptionException e) {
+ if(LOG.isLoggable(INFO))
+ LOG.info("Subscription removed");
}
}
long duration = System.currentTimeMillis() - now;
@@ -183,7 +154,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
});
}
- private void displayHeaders(final Group g, final boolean postable,
+ private void displayHeaders(final Group g,
final Collection headers) {
runOnUiThread(new Runnable() {
public void run() {
@@ -191,7 +162,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
GroupListItem item = findGroup(g.getId());
if(item != null) adapter.remove(item);
// Add a new item
- adapter.add(new GroupListItem(g, postable, headers));
+ adapter.add(new GroupListItem(g, headers));
adapter.sort(GroupComparator.INSTANCE);
adapter.notifyDataSetChanged();
selectFirstUnread();
@@ -234,34 +205,22 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
public void onClick(View view) {
if(view == newGroupButton) {
- if(restricted)
- startActivity(new Intent(this, CreateBlogActivity.class));
- else startActivity(new Intent(this, CreateGroupActivity.class));
+ startActivity(new Intent(this, CreateGroupActivity.class));
} else if(view == composeButton) {
- if(countPostableGroups() == 0) {
+ if(adapter.isEmpty()) {
NoGroupsDialog dialog = new NoGroupsDialog();
dialog.setListener(this);
- dialog.setRestricted(restricted);
dialog.show(getSupportFragmentManager(), "NoGroupsDialog");
- } else if(restricted) {
- startActivity(new Intent(this, WriteBlogPostActivity.class));
} else {
startActivity(new Intent(this, WriteGroupPostActivity.class));
}
}
}
- private int countPostableGroups() {
- int postable = 0, count = adapter.getCount();
- for(int i = 0; i < count; i++)
- if(adapter.getItem(i).isPostable()) postable++;
- return postable;
- }
-
public void eventOccurred(DatabaseEvent e) {
if(e instanceof GroupMessageAddedEvent) {
Group g = ((GroupMessageAddedEvent) e).getGroup();
- if(g.isRestricted() == restricted) {
+ if(!g.isRestricted()) {
if(LOG.isLoggable(INFO)) LOG.info("Message added, reloading");
loadHeaders(g);
}
@@ -270,7 +229,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
loadHeaders();
} else if(e instanceof SubscriptionRemovedEvent) {
Group g = ((SubscriptionRemovedEvent) e).getGroup();
- if(g.isRestricted() == restricted) {
+ if(!g.isRestricted()) {
// Reload the group, expecting NoSuchSubscriptionException
if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading");
loadHeaders(g);
@@ -284,15 +243,12 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
try {
serviceConnection.waitForStartup();
long now = System.currentTimeMillis();
- boolean postable;
- if(restricted) postable = db.getLocalGroups().contains(g);
- else postable = true;
Collection headers =
db.getMessageHeaders(g.getId());
long duration = System.currentTimeMillis() - now;
if(LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
- displayHeaders(g, postable, headers);
+ displayHeaders(g, headers);
} catch(NoSuchSubscriptionException e) {
if(LOG.isLoggable(INFO)) LOG.info("Subscription removed");
removeGroup(g.getId());
@@ -320,10 +276,8 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener {
});
}
- public void createGroupButtonClicked() {
- if(restricted)
- startActivity(new Intent(this, CreateBlogActivity.class));
- else startActivity(new Intent(this, CreateGroupActivity.class));
+ public void createButtonClicked() {
+ startActivity(new Intent(this, CreateGroupActivity.class));
}
public void cancelButtonClicked() {
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
index 0fd73a29a..0d6478856 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java
@@ -101,7 +101,6 @@ implements OnItemClickListener {
long id) {
GroupListItem item = getItem(position);
Intent i = new Intent(getContext(), GroupActivity.class);
- i.putExtra("net.sf.briar.RESTRICTED", item.isRestricted());
i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes());
i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName());
getContext().startActivity(i);
diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
index abc61370d..0fc071ad3 100644
--- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
+++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java
@@ -14,15 +14,13 @@ import net.sf.briar.api.messaging.GroupId;
class GroupListItem {
private final Group group;
- private final boolean postable, empty;
+ private final boolean empty;
private final String authorName, contentType, subject;
private final long timestamp;
private final int unread;
- GroupListItem(Group group, boolean postable,
- Collection headers) {
+ GroupListItem(Group group, Collection headers) {
this.group = group;
- this.postable = postable;
empty = headers.isEmpty();
if(empty) {
authorName = null;
@@ -55,14 +53,6 @@ class GroupListItem {
return group.getName();
}
- boolean isRestricted() {
- return group.isRestricted();
- }
-
- boolean isPostable() {
- return postable;
- }
-
boolean isEmpty() {
return empty;
}
diff --git a/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java b/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
index e5aab93e1..34613b52c 100644
--- a/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
+++ b/briar-android/src/net/sf/briar/android/groups/NoGroupsDialog.java
@@ -10,24 +10,19 @@ import android.support.v4.app.DialogFragment;
public class NoGroupsDialog extends DialogFragment {
private Listener listener = null;
- private boolean restricted = false;
void setListener(Listener listener) {
this.listener = listener;
}
- void setRestricted(boolean restricted) {
- this.restricted = restricted;
- }
-
@Override
public Dialog onCreateDialog(Bundle state) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setMessage(restricted ? R.string.no_blogs : R.string.no_groups);
+ builder.setMessage(R.string.no_groups);
builder.setPositiveButton(R.string.create_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
- listener.createGroupButtonClicked();
+ listener.createButtonClicked();
}
});
builder.setNegativeButton(R.string.cancel_button,
@@ -41,7 +36,7 @@ public class NoGroupsDialog extends DialogFragment {
interface Listener {
- void createGroupButtonClicked();
+ void createButtonClicked();
void cancelButtonClicked();
}
diff --git a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
similarity index 94%
rename from briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
rename to briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
index cce96dde7..e815281d0 100644
--- a/briar-android/src/net/sf/briar/android/groups/ReadGroupMessageActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/ReadGroupPostActivity.java
@@ -49,7 +49,7 @@ import android.widget.TextView;
import com.google.inject.Inject;
-public class ReadGroupMessageActivity extends BriarActivity
+public class ReadGroupPostActivity extends BriarActivity
implements OnClickListener {
static final int RESULT_REPLY = RESULT_FIRST_USER;
@@ -57,13 +57,12 @@ implements OnClickListener {
static final int RESULT_NEXT = RESULT_FIRST_USER + 2;
private static final Logger LOG =
- Logger.getLogger(ReadGroupMessageActivity.class.getName());
+ Logger.getLogger(ReadGroupPostActivity.class.getName());
private final BriarServiceConnection serviceConnection =
new BriarServiceConnection();
@Inject private BundleEncrypter bundleEncrypter;
- private boolean restricted = false;
private GroupId groupId = null;
private Rating rating = UNRATED;
private boolean read;
@@ -84,7 +83,6 @@ implements OnClickListener {
super.onCreate(null);
Intent i = getIntent();
- restricted = i.getBooleanExtra("net.sf.briar.RESTRICTED", false);
byte[] b = i.getByteArrayExtra("net.sf.briar.GROUP_ID");
if(b == null) throw new IllegalStateException();
groupId = new GroupId(b);
@@ -258,7 +256,7 @@ implements OnClickListener {
private void setReadInUi(final boolean read) {
runOnUiThread(new Runnable() {
public void run() {
- ReadGroupMessageActivity.this.read = read;
+ ReadGroupPostActivity.this.read = read;
if(read) readButton.setImageResource(R.drawable.content_unread);
else readButton.setImageResource(R.drawable.content_read);
}
@@ -326,17 +324,10 @@ implements OnClickListener {
setResult(RESULT_NEXT);
finish();
} else if(view == replyButton) {
- if(restricted) {
- Intent i = new Intent(this, WriteBlogPostActivity.class);
- i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
- i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
- startActivity(i);
- } else {
- Intent i = new Intent(this, WriteGroupPostActivity.class);
- i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
- i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
- startActivity(i);
- }
+ Intent i = new Intent(this, WriteGroupPostActivity.class);
+ i.putExtra("net.sf.briar.GROUP_ID", groupId.getBytes());
+ i.putExtra("net.sf.briar.PARENT_ID", messageId.getBytes());
+ startActivity(i);
setResult(RESULT_REPLY);
finish();
}
diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
index e4f388f0e..9b4621c7e 100644
--- a/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
+++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupPostActivity.java
@@ -24,6 +24,7 @@ import java.util.logging.Logger;
import net.sf.briar.R;
import net.sf.briar.android.BriarActivity;
import net.sf.briar.android.BriarService;
+import net.sf.briar.android.GroupNameComparator;
import net.sf.briar.android.BriarService.BriarServiceConnection;
import net.sf.briar.android.identity.CreateIdentityActivity;
import net.sf.briar.android.identity.LocalAuthorItem;