diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index fc74fb36e3abd6cca1a40735956a4a311489b9f0..6020c390ae7b5cfb696f052f4b240fb24ef1124e 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -45,9 +45,25 @@ android:label="@string/contact_list_title" > </activity> <activity - android:name=".android.groups.CreateBlogActivity" + android:name=".android.blogs.BlogActivity" + android:label="@string/app_name" > + </activity> + <activity + android:name=".android.blogs.BlogListActivity" + android:label="@string/blogs_title" > + </activity> + <activity + android:name=".android.blogs.CreateBlogActivity" android:label="@string/create_blog_title" > </activity> + <activity + android:name=".android.blogs.ReadBlogPostActivity" + android:label="@string/app_name" > + </activity> + <activity + android:name=".android.blogs.WriteBlogPostActivity" + android:label="@string/compose_blog_title" > + </activity> <activity android:name=".android.groups.CreateGroupActivity" android:label="@string/create_group_title" > @@ -58,16 +74,12 @@ </activity> <activity android:name=".android.groups.GroupListActivity" - android:label="@string/app_name" > + android:label="@string/groups_title" > </activity> <activity - android:name=".android.groups.ReadGroupMessageActivity" + android:name=".android.groups.ReadGroupPostActivity" android:label="@string/app_name" > </activity> - <activity - android:name=".android.groups.WriteBlogPostActivity" - android:label="@string/compose_blog_title" > - </activity> <activity android:name=".android.groups.WriteGroupPostActivity" android:label="@string/compose_group_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 47fdd6b538c7075e70d647e2d12f3ae569cc0769..8e397be1b11430d429e837f26c5343c2044216e7 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<Group> { +public class GroupNameComparator implements Comparator<Group> { - 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 5596b1cecd2b9484a468f4af0c97558a1d7bb9a5..f33052720509bf02f224ede0e71f26f5385b37e8 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 0000000000000000000000000000000000000000..b30e2764020ced7dbdbb1dc301cdd7471462128f --- /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<GroupMessageHeader> 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<GroupMessageHeader> 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 0000000000000000000000000000000000000000..969b520e3687b2fba546a6563878e807ed27c4ec --- /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<GroupMessageHeader> { + + BlogAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<GroupMessageHeader>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + GroupMessageHeader item = getItem(position); + Context ctx = getContext(); + + // FIXME: Use a RelativeLayout + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(HORIZONTAL); + 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 0000000000000000000000000000000000000000..b6dfdfdcabbeb29e286a0ef8a2c722391497a2a8 --- /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<GroupId> local = new HashSet<GroupId>(); + 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<GroupMessageHeader> 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<GroupMessageHeader> 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<GroupMessageHeader> 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<BlogListItem> { + + 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 0000000000000000000000000000000000000000..f37c1f04f20edcf3ca3aa8d9a6ae91f5f56d0396 --- /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<BlogListItem> +implements OnItemClickListener { + + BlogListAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<BlogListItem>()); + } + + @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 0000000000000000000000000000000000000000..4d955fcc53c41e32e247d3d086242c89a97b3133 --- /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<GroupMessageHeader> headers) { + this.group = group; + this.postable = postable; + empty = headers.isEmpty(); + if(empty) { + authorName = null; + contentType = null; + subject = null; + timestamp = 0; + unread = 0; + } else { + List<GroupMessageHeader> list = + new ArrayList<GroupMessageHeader>(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 46f6c9b8fe0d0743b6d826ff186cd97b390dd0cf..6d9ce55b7b98533db58b886abac57b28faec7870 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 34a2f3ffa94c7f1224b423fc6aab4d012ee326e6..8dc22083a4bfb2b9e8625730bf5591cc515a6c94 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 0000000000000000000000000000000000000000..15b15cfbeae485c8b574e4b52bdfe474aba5bc28 --- /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 0000000000000000000000000000000000000000..bb97cbca536547bb44c9dc093b2986a07e3442bf --- /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 37c8c79c8897b90fc9d1f60614441a2e380a844e..15a807ab437f79ce42abb809c95304e8e2cdcbd5 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 710eb668a6882d256eeff3ca3b2ddae859c17530..0cdd423055dccde1550d623c11fe7be6d7770ece 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 ccbfa93dcd53f4f30765c3b861a532a467eb5a56..083497f91f7849d551e02b62bc8b5436ba0d7f07 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<Group> subs = db.getSubscriptions(); - if(restricted) { - Set<GroupId> local = new HashSet<GroupId>(); - 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<GroupMessageHeader> 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<GroupMessageHeader> 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<GroupMessageHeader> 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<GroupMessageHeader> 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<GroupMessageHeader> 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 0fd73a29a8a549a575435a3ebb35b70abe99e631..0d6478856f2a7009c764f745e631d59a89e0cc1c 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 abc61370d85fe8b353cd4a7ac8e5b72e01b780d8..0fc071ad398c4a964c71a64ce3e331c8dbba56c0 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<GroupMessageHeader> headers) { + GroupListItem(Group group, Collection<GroupMessageHeader> 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 e5aab93e17816c6a296139d1aa153cd20aeca1f8..34613b52c9a95144f115621896c01c6c31f6e8b3 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 cce96dde745376183b08025fa968cfdc20dd7a48..e815281d0d3a2ff25ac5dce45864f065a51e3577 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 e4f388f0ec53305455c77ecc9ce645cd99a5f993..9b4621c7ea60983580d1da8f8aedf561ae3ebfcb 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;