diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index c2bedce0776881464f6b5e45fc3fb0f46683a398..2f5e0d87b53bc775d9c0cb23bd9fdc4491a0da2b 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -52,17 +52,29 @@ android:name=".android.blogs.BlogListActivity" android:label="@string/blogs_title" > </activity> + <activity + android:name=".android.blogs.ConfigureBlogActivity" + android:label="@string/app_name" > + </activity> <activity android:name=".android.blogs.CreateBlogActivity" android:label="@string/create_blog_title" > </activity> + <activity + android:name=".android.blogs.ManageBlogsActivity" + android:label="@string/manage_subscriptions_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" > + android:label="@string/new_post_title" > + </activity> + <activity + android:name=".android.groups.ConfigureGroupActivity" + android:label="@string/app_name" > </activity> <activity android:name=".android.groups.CreateGroupActivity" @@ -77,12 +89,16 @@ android:label="@string/groups_title" > </activity> <activity + android:name=".android.groups.ManageGroupsActivity" + android:label="@string/manage_subscriptions_title" > + </activity> + <activity android:name=".android.groups.ReadGroupPostActivity" android:label="@string/app_name" > </activity> <activity android:name=".android.groups.WriteGroupPostActivity" - android:label="@string/compose_group_title" > + android:label="@string/new_post_title" > </activity> <activity android:name=".android.identity.CreateIdentityActivity" @@ -106,7 +122,7 @@ </activity> <activity android:name=".android.messages.WritePrivateMessageActivity" - android:label="@string/compose_message_title" > + android:label="@string/new_message_title" > </activity> </application> </manifest> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 8219599e76de09e2b5cf308bfcec0a3d327710e2..632d4b8f38f2bbb0170934df54cfd9168a1d76e9 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -9,7 +9,7 @@ <string name="confirm_password">Confirm your password:</string> <string name="format_min_password">Password must be at least %1$d characters long.</string> <string name="enter_password">Enter your password:</string> - <string name="try_again">Wrong password, try again</string> + <string name="try_again">Wrong password, try again:</string> <string name="expiry_warning">This software has expired.\nPlease install a newer version.</string> <string name="contact_list_button">Contacts</string> <string name="messages_button">Messages</string> @@ -50,28 +50,40 @@ <string name="messages_title">Messages</string> <string name="no_messages">(No messages)</string> <string name="format_from">From: %1$s</string> - <string name="format_to">To: %1$s</string> - <string name="compose_message_title">New Message</string> + <string name="new_message_title">New Message</string> <string name="from">From:</string> <string name="to">To:</string> <string name="anonymous">Anonymous</string> <string name="new_contact_item">New contact\u2026</string> <string name="groups_title">Groups</string> - <string name="no_posts">(No posts)</string> + <plurals name="groups_available"> + <item quantity="one">%1$d group available from contacts</item> + <item quantity="two">$1$d groups available from contacts</item> + </plurals> + <string name="no_posts">No posts</string> + <string name="subscribe_to_this_group">Subscribe to this group</string> <string name="create_group_title">New Group</string> <string name="choose_group_name">Choose a name for your group:</string> <string name="group_visible_to_all">Share this group with all contacts</string> <string name="group_visible_to_some">Share this group with chosen contacts</string> - <string name="compose_group_title">New Post</string> + <string name="new_post_title">New Post</string> <string name="new_group_item">New group\u2026</string> <string name="blogs_title">Blogs</string> + <plurals name="blogs_available"> + <item quantity="one">%1$d blog available from contacts</item> + <item quantity="two">$1$d blogs available from contacts</item> + </plurals> + <string name="manage_subscriptions_title">Manage Subscriptions</string> + <string name="subscribed_all">Subscribed, shared with all contacts</string> + <string name="subscribed_some">Subscribed, shared with chosen contacts</string> + <string name="not_subscribed">Not subscribed</string> + <string name="subscribe_to_this_blog">Subscribe to this blog</string> <string name="create_blog_title">New Blog</string> <string name="choose_blog_name">Choose a name for your blog:</string> <string name="blog_visible_to_all">Share this blog with all contacts</string> <string name="blog_visible_to_some">Share this blog with chosen contacts</string> <string name="not_your_blog">Only the creator of this blog can write posts</string> <string name="ok_button">OK</string> - <string name="compose_blog_title">New Post</string> <string name="new_blog_item">New blog\u2026</string> <string name="create_nickname_item">New nickname\u2026</string> <string name="create_identity_title">Create an Identity</string> diff --git a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java index 65f916118249ff68e9ea760896012af3b0e88d75..443e3b57c64c0af510a9621fb49d070de74209f2 100644 --- a/briar-android/src/net/sf/briar/android/HomeScreenActivity.java +++ b/briar-android/src/net/sf/briar/android/HomeScreenActivity.java @@ -74,7 +74,7 @@ public class HomeScreenActivity extends BriarActivity { @Inject @DatabaseUiExecutor private Executor dbUiExecutor = null; @Inject @CryptoExecutor private Executor cryptoExecutor = null; private boolean bound = false; - private TextView tryAgain = null; + private TextView enterPassword = null; private Button continueButton = null; private ProgressBar progress = null; @@ -194,7 +194,7 @@ public class HomeScreenActivity extends BriarActivity { layout.setOrientation(VERTICAL); layout.setGravity(CENTER_HORIZONTAL); - TextView enterPassword = new TextView(this); + enterPassword = new TextView(this); enterPassword.setGravity(CENTER); enterPassword.setTextSize(18); enterPassword.setPadding(10, 10, 10, 10); @@ -214,14 +214,6 @@ public class HomeScreenActivity extends BriarActivity { }); layout.addView(passwordEntry); - tryAgain = new TextView(this); - tryAgain.setGravity(CENTER); - tryAgain.setTextSize(14); - tryAgain.setPadding(10, 10, 10, 10); - tryAgain.setText(R.string.try_again); - tryAgain.setVisibility(GONE); - layout.addView(tryAgain); - continueButton = new Button(this); continueButton.setLayoutParams(WRAP_WRAP); continueButton.setText(R.string.continue_button); @@ -241,7 +233,7 @@ public class HomeScreenActivity extends BriarActivity { } private void validatePassword(final byte[] encrypted, Editable e) { - if(tryAgain == null || continueButton == null || progress == null) + if(enterPassword == null || continueButton == null || progress == null) return; // Hide the soft keyboard Object o = getSystemService(INPUT_METHOD_SERVICE); @@ -270,7 +262,7 @@ public class HomeScreenActivity extends BriarActivity { private void tryAgain() { runOnUiThread(new Runnable() { public void run() { - tryAgain.setVisibility(VISIBLE); + enterPassword.setText(R.string.try_again); continueButton.setVisibility(VISIBLE); progress.setVisibility(GONE); } diff --git a/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java b/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..cddca6333345a8a5c1a9fcd13dc0005893096066 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/ManageGroupsAdapter.java @@ -0,0 +1,66 @@ +package net.sf.briar.android; + +import static android.view.View.INVISIBLE; +import static android.widget.LinearLayout.HORIZONTAL; +import static android.widget.LinearLayout.VERTICAL; + +import java.util.ArrayList; + +import net.sf.briar.R; +import net.sf.briar.api.messaging.GroupStatus; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; + +public class ManageGroupsAdapter extends ArrayAdapter<GroupStatus> +implements ListAdapter { + + public ManageGroupsAdapter(Context ctx) { + super(ctx, android.R.layout.simple_expandable_list_item_1, + new ArrayList<GroupStatus>()); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final GroupStatus item = getItem(position); + Context ctx = getContext(); + + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(HORIZONTAL); + + ImageView subscribed = new ImageView(ctx); + subscribed.setPadding(5, 5, 5, 5); + subscribed.setImageResource(R.drawable.navigation_accept); + if(!item.isSubscribed()) subscribed.setVisibility(INVISIBLE); + layout.addView(subscribed); + + LinearLayout innerLayout = new LinearLayout(ctx); + innerLayout.setOrientation(VERTICAL); + + TextView name = new TextView(ctx); + name.setTextSize(18); + name.setMaxLines(1); + name.setPadding(0, 10, 10, 10); + name.setText(item.getGroup().getName()); + innerLayout.addView(name); + + TextView status = new TextView(ctx); + status.setTextSize(14); + status.setPadding(0, 0, 10, 10); + if(item.isSubscribed()) { + if(item.isVisibleToAll()) status.setText(R.string.subscribed_all); + else status.setText(R.string.subscribed_some); + } else { + status.setText(R.string.not_subscribed); + } + innerLayout.addView(status); + layout.addView(innerLayout); + + 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 index 2beedf041873bbaa322e65980ed95b7cb170bfc3..c7a8f1d0c6a912db32a3b6c25deaf338c22e4f81 100644 --- a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java +++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java @@ -6,6 +6,7 @@ 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.blogs.BlogListItem.MANAGE; 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; @@ -32,13 +33,18 @@ 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.RemoteSubscriptionsUpdatedEvent; +import net.sf.briar.api.db.event.SubscriptionAddedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; 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; @@ -46,7 +52,8 @@ import android.widget.ListView; import com.google.inject.Inject; public class BlogListActivity extends BriarFragmentActivity -implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { +implements DatabaseListener, OnClickListener, NoBlogsDialog.Listener, +OnItemClickListener { private static final Logger LOG = Logger.getLogger(BlogListActivity.class.getName()); @@ -57,6 +64,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { private BlogListAdapter adapter = null; private ListView list = null; private ImageButton newBlogButton = null, composeButton = null; + private ImageButton manageBlogsButton = null; // Fields that are accessed from background threads must be volatile @Inject private volatile DatabaseComponent db; @@ -75,7 +83,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { // Give me all the width and all the unused height list.setLayoutParams(MATCH_WRAP_1); list.setAdapter(adapter); - list.setOnItemClickListener(adapter); + list.setOnItemClickListener(this); layout.addView(list); layout.addView(new HorizontalBorder(this)); @@ -99,6 +107,13 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { composeButton.setOnClickListener(this); footer.addView(composeButton); footer.addView(new HorizontalSpace(this)); + + manageBlogsButton = new ImageButton(this); + manageBlogsButton.setBackgroundResource(0); + manageBlogsButton.setImageResource(R.drawable.action_settings); + manageBlogsButton.setOnClickListener(this); + footer.addView(manageBlogsButton); + footer.addView(new HorizontalSpace(this)); layout.addView(footer); setContentView(layout); @@ -124,21 +139,28 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { long now = System.currentTimeMillis(); Set<GroupId> local = new HashSet<GroupId>(); for(Group g : db.getLocalGroups()) local.add(g.getId()); - for(Group g : db.getSubscriptions()) { + int available = 0; + for(GroupStatus s : db.getAvailableGroups()) { + Group g = s.getGroup(); if(!g.isRestricted()) continue; - boolean postable = local.contains(g.getId()); - try { - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(g.getId()); - displayHeaders(g, postable, headers); - } catch(NoSuchSubscriptionException e) { - if(LOG.isLoggable(INFO)) - LOG.info("Subscription removed"); + if(s.isSubscribed()) { + boolean postable = local.contains(g.getId()); + try { + Collection<GroupMessageHeader> headers = + db.getGroupMessageHeaders(g.getId()); + displayHeaders(g, postable, headers); + } catch(NoSuchSubscriptionException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Subscription removed"); + } + } else { + available++; } } long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Full load took " + duration + " ms"); + displayAvailable(available); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -155,6 +177,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { runOnUiThread(new Runnable() { public void run() { adapter.clear(); + adapter.notifyDataSetChanged(); } }); } @@ -168,18 +191,28 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { if(item != null) adapter.remove(item); // Add a new item adapter.add(new BlogListItem(g, postable, headers)); - adapter.sort(GroupComparator.INSTANCE); + adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); selectFirstUnread(); } }); } + private void displayAvailable(final int available) { + runOnUiThread(new Runnable() { + public void run() { + adapter.setAvailable(available); + adapter.notifyDataSetChanged(); + } + }); + } + 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; + if(item == MANAGE) continue; + if(item.getGroup().getId().equals(g)) return item; } return null; // Not found } @@ -187,7 +220,9 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { private void selectFirstUnread() { int firstUnread = -1, count = adapter.getCount(); for(int i = 0; i < count; i++) { - if(adapter.getItem(i).getUnreadCount() > 0) { + BlogListItem item = adapter.getItem(i); + if(item == MANAGE) continue; + if(item.getUnreadCount() > 0) { firstUnread = i; break; } @@ -208,27 +243,6 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { 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(), "NoBlogsDialog"); - } 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(); @@ -239,6 +253,16 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); loadHeaders(); + } else if(e instanceof RemoteSubscriptionsUpdatedEvent) { + if(LOG.isLoggable(INFO)) + LOG.info("Remote subscriptions changed, reloading"); + loadAvailable(); + } else if(e instanceof SubscriptionAddedEvent) { + Group g = ((SubscriptionAddedEvent) e).getGroup(); + if(g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); + loadHeaders(); + } } else if(e instanceof SubscriptionRemovedEvent) { Group g = ((SubscriptionRemovedEvent) e).getGroup(); if(g.isRestricted()) { @@ -290,23 +314,96 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { }); } + private void loadAvailable() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + int available = 0; + long now = System.currentTimeMillis(); + for(GroupStatus s : db.getAvailableGroups()) { + if(s.getGroup().isRestricted() && !s.isSubscribed()) + available++; + } + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Loading available took " + duration + " ms"); + displayAvailable(available); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + } + }); + } + + 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(), "NoBlogsDialog"); + } else { + startActivity(new Intent(this, WriteBlogPostActivity.class)); + } + } else if(view == manageBlogsButton) { + startActivity(new Intent(this, ManageBlogsActivity.class)); + } + } + + private int countPostableGroups() { + int postable = 0, count = adapter.getCount(); + for(int i = 0; i < count; i++) { + BlogListItem item = adapter.getItem(i); + if(item == MANAGE) continue; + if(item.isPostable()) postable++; + } + return postable; + } + public void blogCreationSelected() { startActivity(new Intent(this, CreateBlogActivity.class)); } public void blogCreationCancelled() {} - private static class GroupComparator implements Comparator<BlogListItem> { + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + BlogListItem item = adapter.getItem(position); + if(item == MANAGE) { + startActivity(new Intent(this, ManageBlogsActivity.class)); + } else { + Intent i = new Intent(this, BlogActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", + item.getGroup().getId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName()); + i.putExtra("net.sf.briar.POSTABLE", item.isPostable()); + startActivity(i); + } + } + + private static class ItemComparator implements Comparator<BlogListItem> { - private static final GroupComparator INSTANCE = new GroupComparator(); + private static final ItemComparator INSTANCE = new ItemComparator(); public int compare(BlogListItem a, BlogListItem b) { + if(a == b) return 0; + // The manage blogs item comes last + if(a == MANAGE) return 1; + if(b == MANAGE) return -1; // 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(); + String aName = a.getGroup().getName(); + String bName = b.getGroup().getName(); return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); } } diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java index 2ec0fec77511101249f34d8fbbbdf8b6bc8781e5..4721572612c94d4b23174e7f2c9cda9788db4aae 100644 --- a/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java +++ b/briar-android/src/net/sf/briar/android/blogs/BlogListAdapter.java @@ -1,42 +1,79 @@ package net.sf.briar.android.blogs; import static android.graphics.Typeface.BOLD; +import static android.view.Gravity.CENTER; import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.VERTICAL; import static java.text.DateFormat.SHORT; +import static net.sf.briar.android.blogs.BlogListItem.MANAGE; import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; 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.BaseAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -class BlogListAdapter extends ArrayAdapter<BlogListItem> -implements OnItemClickListener { +class BlogListAdapter extends BaseAdapter { + + private final Context ctx; + private final List<BlogListItem> list = new ArrayList<BlogListItem>(); + private int available = 0; BlogListAdapter(Context ctx) { - super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList<BlogListItem>()); + this.ctx = ctx; + } + + public void setAvailable(int available) { + this.available = available; + } + + public void add(BlogListItem item) { + list.add(item); + } + + public void clear() { + list.clear(); + } + + public int getCount() { + return available == 0 ? list.size() : list.size() + 1; + } + + public BlogListItem getItem(int position) { + return position == list.size() ? MANAGE : list.get(position); + } + + public long getItemId(int position) { + return android.R.layout.simple_expandable_list_item_1; } - @Override public View getView(int position, View convertView, ViewGroup parent) { BlogListItem item = getItem(position); - Context ctx = getContext(); Resources res = ctx.getResources(); + if(item == MANAGE) { + TextView manage = new TextView(ctx); + manage.setGravity(CENTER); + manage.setTextSize(18); + manage.setPadding(10, 10, 10, 10); + String format = res.getQuantityString(R.plurals.blogs_available, + available); + manage.setText(String.format(format, available)); + return manage; + } + LinearLayout layout = new LinearLayout(ctx); layout.setOrientation(HORIZONTAL); if(item.getUnreadCount() > 0) @@ -52,8 +89,9 @@ implements OnItemClickListener { 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()); + String groupName = item.getGroup().getName(); + if(unread > 0) name.setText(groupName + " (" + unread + ")"); + else name.setText(groupName); innerLayout.addView(name); if(item.isEmpty()) { @@ -97,13 +135,16 @@ implements OnItemClickListener { 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()); - i.putExtra("net.sf.briar.POSTABLE", item.isPostable()); - getContext().startActivity(i); + @Override + public boolean isEmpty() { + return false; + } + + public void remove(BlogListItem item) { + list.remove(item); + } + + public void sort(Comparator<BlogListItem> comparator) { + Collections.sort(list, comparator); } } diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java index 4d955fcc53c41e32e247d3d086242c89a97b3133..72f3b1b8d5b2c6f7390ca49183ab942913cf7df2 100644 --- a/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java +++ b/briar-android/src/net/sf/briar/android/blogs/BlogListItem.java @@ -9,10 +9,12 @@ 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 { + static final BlogListItem MANAGE = new BlogListItem(null, false, + Collections.<GroupMessageHeader>emptyList()); + private final Group group; private final boolean postable, empty; private final String authorName, contentType, subject; @@ -47,12 +49,8 @@ class BlogListItem { } } - GroupId getGroupId() { - return group.getId(); - } - - String getGroupName() { - return group.getName(); + Group getGroup() { + return group; } boolean isPostable() { diff --git a/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..f004a77e3de4afda952ad2496cc77457f06af26e --- /dev/null +++ b/briar-android/src/net/sf/briar/android/blogs/ConfigureBlogActivity.java @@ -0,0 +1,239 @@ +package net.sf.briar.android.blogs; + +import static android.view.Gravity.CENTER_HORIZONTAL; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.LinearLayout.VERTICAL; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH; +import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP; + +import java.util.Collection; +import java.util.Collections; +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.contact.SelectContactsDialog; +import net.sf.briar.android.invitation.AddContactActivity; +import net.sf.briar.android.messages.NoContactsDialog; +import net.sf.briar.api.Contact; +import net.sf.briar.api.ContactId; +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.messaging.GroupId; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import com.google.inject.Inject; + +public class ConfigureBlogActivity extends BriarFragmentActivity +implements OnClickListener, NoContactsDialog.Listener, +SelectContactsDialog.Listener { + + private static final Logger LOG = + Logger.getLogger(ConfigureBlogActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + new BriarServiceConnection(); + + private boolean wasSubscribed = false; + private CheckBox subscribeCheckBox = null; + private RadioGroup radioGroup = null; + private RadioButton visibleToAll = null, visibleToSome = null; + private Button doneButton = null; + private ProgressBar progress = 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; + private volatile Collection<ContactId> selected = Collections.emptyList(); + + @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 IllegalArgumentException(); + setTitle(groupName); + wasSubscribed = i.getBooleanExtra("net.sf.briar.SUBSCRIBED", false); + boolean all = i.getBooleanExtra("net.sf.briar.VISIBLE_TO_ALL", false); + + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(MATCH_MATCH); + layout.setOrientation(VERTICAL); + layout.setGravity(CENTER_HORIZONTAL); + + subscribeCheckBox = new CheckBox(this); + subscribeCheckBox.setText(R.string.subscribe_to_this_blog); + subscribeCheckBox.setChecked(wasSubscribed); + subscribeCheckBox.setOnClickListener(this); + layout.addView(subscribeCheckBox); + + radioGroup = new RadioGroup(this); + radioGroup.setOrientation(VERTICAL); + radioGroup.setEnabled(wasSubscribed); + + visibleToAll = new RadioButton(this); + visibleToAll.setId(1); + visibleToAll.setText(R.string.blog_visible_to_all); + visibleToAll.setOnClickListener(this); + radioGroup.addView(visibleToAll); + + visibleToSome = new RadioButton(this); + visibleToSome.setId(2); + visibleToSome.setText(R.string.blog_visible_to_some); + visibleToSome.setOnClickListener(this); + radioGroup.addView(visibleToSome); + + if(all) radioGroup.check(1); + else radioGroup.check(2); + layout.addView(radioGroup); + + doneButton = new Button(this); + doneButton.setLayoutParams(WRAP_WRAP); + doneButton.setText(R.string.done_button); + doneButton.setOnClickListener(this); + layout.addView(doneButton); + + progress = new ProgressBar(this); + progress.setLayoutParams(WRAP_WRAP); + progress.setIndeterminate(true); + progress.setVisibility(GONE); + layout.addView(progress); + + 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 onDestroy() { + super.onDestroy(); + unbindService(serviceConnection); + } + + public void onClick(View view) { + if(view == subscribeCheckBox) { + radioGroup.setEnabled(subscribeCheckBox.isChecked()); + } else if(view == visibleToSome) { + loadContacts(); + } else if(view == doneButton) { + boolean subscribe = subscribeCheckBox.isChecked(); + boolean all = visibleToAll.isChecked(); + Collection<ContactId> visible = + Collections.unmodifiableCollection(selected); + // Replace the button with a progress bar + doneButton.setVisibility(GONE); + progress.setVisibility(VISIBLE); + // Update the blog in a background thread + if(subscribe || wasSubscribed) + updateGroup(subscribe, wasSubscribed, all, visible); + } + } + + private void loadContacts() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + Collection<Contact> contacts = db.getContacts(); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + displayContacts(contacts); + } 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 displayContacts(final Collection<Contact> contacts) { + runOnUiThread(new Runnable() { + public void run() { + if(contacts.isEmpty()) { + NoContactsDialog dialog = new NoContactsDialog(); + dialog.setListener(ConfigureBlogActivity.this); + dialog.show(getSupportFragmentManager(), + "NoContactsDialog"); + } else { + SelectContactsDialog dialog = new SelectContactsDialog(); + dialog.setListener(ConfigureBlogActivity.this); + dialog.setContacts(contacts); + dialog.show(getSupportFragmentManager(), + "SelectContactsDialog"); + } + } + }); + } + + private void updateGroup(final boolean subscribe, + final boolean wasSubscribed, final boolean all, + final Collection<ContactId> visible) { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + if(subscribe) { + if(!wasSubscribed) db.subscribe(db.getGroup(groupId)); + db.setVisibleToAll(groupId, all); + if(!all) db.setVisibility(groupId, visible); + } else if(wasSubscribed) { + db.unsubscribe(db.getGroup(groupId)); + } + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Update took " + duration + " ms"); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + finishOnUiThread(); + } + }); + } + + public void contactCreationSelected() { + startActivity(new Intent(this, AddContactActivity.class)); + } + + public void contactCreationCancelled() {} + + public void contactsSelected(Collection<ContactId> selected) { + this.selected = selected; + } + + public void contactSelectionCancelled() {} +} diff --git a/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java index c3025a62d42f07fb56217f59bf93df0520d25c2b..35cbe2f6a4841bf860f8c8db3b46ee4c84b14f21 100644 --- a/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java +++ b/briar-android/src/net/sf/briar/android/blogs/CreateBlogActivity.java @@ -255,11 +255,7 @@ SelectContactsDialog.Listener { LOG.info("Interrupted while waiting for service"); Thread.currentThread().interrupt(); } - runOnUiThread(new Runnable() { - public void run() { - finish(); - } - }); + finishOnUiThread(); } }); } diff --git a/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java b/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..f18ba0ee314c8d05a054f9db4914890f32f502dc --- /dev/null +++ b/briar-android/src/net/sf/briar/android/blogs/ManageBlogsActivity.java @@ -0,0 +1,168 @@ +package net.sf.briar.android.blogs; + +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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import net.sf.briar.android.BriarFragmentActivity; +import net.sf.briar.android.BriarService; +import net.sf.briar.android.ManageGroupsAdapter; +import net.sf.briar.android.BriarService.BriarServiceConnection; +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.event.DatabaseEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; +import net.sf.briar.api.db.event.SubscriptionAddedEvent; +import net.sf.briar.api.db.event.SubscriptionRemovedEvent; +import net.sf.briar.api.messaging.Group; +import net.sf.briar.api.messaging.GroupStatus; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +import com.google.inject.Inject; + +public class ManageBlogsActivity extends BriarFragmentActivity +implements DatabaseListener, OnItemClickListener { + + private static final Logger LOG = + Logger.getLogger(ManageBlogsActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + new BriarServiceConnection(); + + private ManageGroupsAdapter 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; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + + adapter = new ManageGroupsAdapter(this); + list = new ListView(this); + list.setLayoutParams(MATCH_MATCH); + list.setAdapter(adapter); + list.setOnItemClickListener(this); + setContentView(list); + + // 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); + loadAvailableGroups(); + } + + private void loadAvailableGroups() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + List<GroupStatus> available = new ArrayList<GroupStatus>(); + for(GroupStatus s : db.getAvailableGroups()) + if(s.getGroup().isRestricted()) available.add(s); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + available = Collections.unmodifiableList(available); + displayAvailableGroups(available); + } 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 displayAvailableGroups( + final Collection<GroupStatus> available) { + runOnUiThread(new Runnable() { + public void run() { + adapter.clear(); + for(GroupStatus g : available) adapter.add(g); + adapter.sort(ItemComparator.INSTANCE); + adapter.notifyDataSetChanged(); + } + }); + } + + @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 RemoteSubscriptionsUpdatedEvent) { + if(LOG.isLoggable(INFO)) + LOG.info("Remote subscriptions changed, reloading"); + loadAvailableGroups(); + } else if(e instanceof SubscriptionAddedEvent) { + Group g = ((SubscriptionAddedEvent) e).getGroup(); + if(g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); + loadAvailableGroups(); + } + } else if(e instanceof SubscriptionRemovedEvent) { + Group g = ((SubscriptionRemovedEvent) e).getGroup(); + if(g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + loadAvailableGroups(); + } + } + } + + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + GroupStatus item = adapter.getItem(position); + Intent i = new Intent(this, ConfigureBlogActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", item.getGroup().getId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName()); + i.putExtra("net.sf.briar.SUBSCRIBED", item.isSubscribed()); + i.putExtra("net.sf.briar.VISIBLE_TO_ALL", item.isVisibleToAll()); + startActivity(i); + } + + private static class ItemComparator implements Comparator<GroupStatus> { + + private static final ItemComparator INSTANCE = new ItemComparator(); + + public int compare(GroupStatus a, GroupStatus b) { + String aName = a.getGroup().getName(); + String bName = b.getGroup().getName(); + return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); + } + } +} diff --git a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java index ef7866fb94d2652f2360b9084dde7809e57e059d..e52d0d4bf06d900c8d030c5c6acff171c34c2b76 100644 --- a/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java +++ b/briar-android/src/net/sf/briar/android/contact/ContactListActivity.java @@ -143,7 +143,7 @@ implements OnClickListener, DatabaseListener, ConnectionListener { if(last != null) adapter.add(new ContactListItem(c, now, last)); } - adapter.sort(ContactComparator.INSTANCE); + adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); } }); @@ -198,10 +198,9 @@ implements OnClickListener, DatabaseListener, ConnectionListener { }); } - private static class ContactComparator - implements Comparator<ContactListItem> { + private static class ItemComparator implements Comparator<ContactListItem> { - static final ContactComparator INSTANCE = new ContactComparator(); + private static final ItemComparator INSTANCE = new ItemComparator(); public int compare(ContactListItem a, ContactListItem b) { return String.CASE_INSENSITIVE_ORDER.compare(a.getContactName(), diff --git a/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..1fca60001339f83c8e94550cc9ce358ff8e052b3 --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/ConfigureGroupActivity.java @@ -0,0 +1,239 @@ +package net.sf.briar.android.groups; + +import static android.view.Gravity.CENTER_HORIZONTAL; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.widget.LinearLayout.VERTICAL; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static net.sf.briar.android.widgets.CommonLayoutParams.MATCH_MATCH; +import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP; + +import java.util.Collection; +import java.util.Collections; +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.contact.SelectContactsDialog; +import net.sf.briar.android.invitation.AddContactActivity; +import net.sf.briar.android.messages.NoContactsDialog; +import net.sf.briar.api.Contact; +import net.sf.briar.api.ContactId; +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.messaging.GroupId; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import com.google.inject.Inject; + +public class ConfigureGroupActivity extends BriarFragmentActivity +implements OnClickListener, NoContactsDialog.Listener, +SelectContactsDialog.Listener { + + private static final Logger LOG = + Logger.getLogger(ConfigureGroupActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + new BriarServiceConnection(); + + private boolean wasSubscribed = false; + private CheckBox subscribeCheckBox = null; + private RadioGroup radioGroup = null; + private RadioButton visibleToAll = null, visibleToSome = null; + private Button doneButton = null; + private ProgressBar progress = 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; + private volatile Collection<ContactId> selected = Collections.emptyList(); + + @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 IllegalArgumentException(); + setTitle(groupName); + wasSubscribed = i.getBooleanExtra("net.sf.briar.SUBSCRIBED", false); + boolean all = i.getBooleanExtra("net.sf.briar.VISIBLE_TO_ALL", false); + + LinearLayout layout = new LinearLayout(this); + layout.setLayoutParams(MATCH_MATCH); + layout.setOrientation(VERTICAL); + layout.setGravity(CENTER_HORIZONTAL); + + subscribeCheckBox = new CheckBox(this); + subscribeCheckBox.setText(R.string.subscribe_to_this_group); + subscribeCheckBox.setChecked(wasSubscribed); + subscribeCheckBox.setOnClickListener(this); + layout.addView(subscribeCheckBox); + + radioGroup = new RadioGroup(this); + radioGroup.setOrientation(VERTICAL); + radioGroup.setEnabled(wasSubscribed); + + visibleToAll = new RadioButton(this); + visibleToAll.setId(1); + visibleToAll.setText(R.string.group_visible_to_all); + visibleToAll.setOnClickListener(this); + radioGroup.addView(visibleToAll); + + visibleToSome = new RadioButton(this); + visibleToSome.setId(2); + visibleToSome.setText(R.string.group_visible_to_some); + visibleToSome.setOnClickListener(this); + radioGroup.addView(visibleToSome); + + if(all) radioGroup.check(1); + else radioGroup.check(2); + layout.addView(radioGroup); + + doneButton = new Button(this); + doneButton.setLayoutParams(WRAP_WRAP); + doneButton.setText(R.string.done_button); + doneButton.setOnClickListener(this); + layout.addView(doneButton); + + progress = new ProgressBar(this); + progress.setLayoutParams(WRAP_WRAP); + progress.setIndeterminate(true); + progress.setVisibility(GONE); + layout.addView(progress); + + 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 onDestroy() { + super.onDestroy(); + unbindService(serviceConnection); + } + + public void onClick(View view) { + if(view == subscribeCheckBox) { + radioGroup.setEnabled(subscribeCheckBox.isChecked()); + } else if(view == visibleToSome) { + loadContacts(); + } else if(view == doneButton) { + boolean subscribe = subscribeCheckBox.isChecked(); + boolean all = visibleToAll.isChecked(); + Collection<ContactId> visible = + Collections.unmodifiableCollection(selected); + // Replace the button with a progress bar + doneButton.setVisibility(GONE); + progress.setVisibility(VISIBLE); + // Update the blog in a background thread + if(subscribe || wasSubscribed) + updateGroup(subscribe, wasSubscribed, all, visible); + } + } + + private void loadContacts() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + Collection<Contact> contacts = db.getContacts(); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + displayContacts(contacts); + } 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 displayContacts(final Collection<Contact> contacts) { + runOnUiThread(new Runnable() { + public void run() { + if(contacts.isEmpty()) { + NoContactsDialog dialog = new NoContactsDialog(); + dialog.setListener(ConfigureGroupActivity.this); + dialog.show(getSupportFragmentManager(), + "NoContactsDialog"); + } else { + SelectContactsDialog dialog = new SelectContactsDialog(); + dialog.setListener(ConfigureGroupActivity.this); + dialog.setContacts(contacts); + dialog.show(getSupportFragmentManager(), + "SelectContactsDialog"); + } + } + }); + } + + private void updateGroup(final boolean subscribe, + final boolean wasSubscribed, final boolean all, + final Collection<ContactId> visible) { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + if(subscribe) { + if(!wasSubscribed) db.subscribe(db.getGroup(groupId)); + db.setVisibleToAll(groupId, all); + if(!all) db.setVisibility(groupId, visible); + } else if(wasSubscribed) { + db.unsubscribe(db.getGroup(groupId)); + } + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Update took " + duration + " ms"); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + finishOnUiThread(); + } + }); + } + + public void contactCreationSelected() { + startActivity(new Intent(this, AddContactActivity.class)); + } + + public void contactCreationCancelled() {} + + public void contactsSelected(Collection<ContactId> selected) { + this.selected = selected; + } + + public void contactSelectionCancelled() {} +} diff --git a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java index e6dab7ed0269620112a49091e63acde229cf8053..388fe72169bb2ce8863a2cc1f7bbc9a8fbdb24e2 100644 --- a/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/CreateGroupActivity.java @@ -189,11 +189,7 @@ SelectContactsDialog.Listener { } catch(IOException e) { throw new RuntimeException(e); } - runOnUiThread(new Runnable() { - public void run() { - finish(); - } - }); + finishOnUiThread(); } }); } 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 3deda412db42abff09c6296f61764dccc788d363..dcab38c8c34fcaab049bcf935030177c57baaef1 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -6,6 +6,7 @@ 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.groups.GroupListItem.MANAGE; 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; @@ -30,13 +31,18 @@ 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.RemoteSubscriptionsUpdatedEvent; +import net.sf.briar.api.db.event.SubscriptionAddedEvent; import net.sf.briar.api.db.event.SubscriptionRemovedEvent; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; 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; @@ -44,7 +50,8 @@ import android.widget.ListView; import com.google.inject.Inject; public class GroupListActivity extends BriarFragmentActivity -implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { +implements DatabaseListener, OnClickListener, NoGroupsDialog.Listener, +OnItemClickListener { private static final Logger LOG = Logger.getLogger(GroupListActivity.class.getName()); @@ -55,6 +62,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { private GroupListAdapter adapter = null; private ListView list = null; private ImageButton newGroupButton = null, composeButton = null; + private ImageButton manageGroupsButton = null; // Fields that are accessed from background threads must be volatile @Inject private volatile DatabaseComponent db; @@ -73,7 +81,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { // Give me all the width and all the unused height list.setLayoutParams(MATCH_WRAP_1); list.setAdapter(adapter); - list.setOnItemClickListener(adapter); + list.setOnItemClickListener(this); layout.addView(list); layout.addView(new HorizontalBorder(this)); @@ -97,6 +105,13 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { composeButton.setOnClickListener(this); footer.addView(composeButton); footer.addView(new HorizontalSpace(this)); + + manageGroupsButton = new ImageButton(this); + manageGroupsButton.setBackgroundResource(0); + manageGroupsButton.setImageResource(R.drawable.action_settings); + manageGroupsButton.setOnClickListener(this); + footer.addView(manageGroupsButton); + footer.addView(new HorizontalSpace(this)); layout.addView(footer); setContentView(layout); @@ -119,21 +134,28 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { public void run() { try { serviceConnection.waitForStartup(); + int available = 0; long now = System.currentTimeMillis(); - for(Group g : db.getSubscriptions()) { + for(GroupStatus s : db.getAvailableGroups()) { + Group g = s.getGroup(); if(g.isRestricted()) continue; - try { - Collection<GroupMessageHeader> headers = - db.getGroupMessageHeaders(g.getId()); - displayHeaders(g, headers); - } catch(NoSuchSubscriptionException e) { - if(LOG.isLoggable(INFO)) - LOG.info("Subscription removed"); + if(s.isSubscribed()) { + try { + Collection<GroupMessageHeader> headers = + db.getGroupMessageHeaders(g.getId()); + displayHeaders(g, headers); + } catch(NoSuchSubscriptionException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Subscription removed"); + } + } else { + available++; } } long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Full load took " + duration + " ms"); + displayAvailable(available); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -150,6 +172,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { runOnUiThread(new Runnable() { public void run() { adapter.clear(); + adapter.notifyDataSetChanged(); } }); } @@ -163,18 +186,28 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { if(item != null) adapter.remove(item); // Add a new item adapter.add(new GroupListItem(g, headers)); - adapter.sort(GroupComparator.INSTANCE); + adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); selectFirstUnread(); } }); } + private void displayAvailable(final int available) { + runOnUiThread(new Runnable() { + public void run() { + adapter.setAvailable(available); + adapter.notifyDataSetChanged(); + } + }); + } + private GroupListItem findGroup(GroupId g) { int count = adapter.getCount(); for(int i = 0; i < count; i++) { GroupListItem item = adapter.getItem(i); - if(item.getGroupId().equals(g)) return item; + if(item == MANAGE) continue; + if(item.getGroup().getId().equals(g)) return item; } return null; // Not found } @@ -182,7 +215,9 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { private void selectFirstUnread() { int firstUnread = -1, count = adapter.getCount(); for(int i = 0; i < count; i++) { - if(adapter.getItem(i).getUnreadCount() > 0) { + GroupListItem item = adapter.getItem(i); + if(item == MANAGE) continue; + if(item.getUnreadCount() > 0) { firstUnread = i; break; } @@ -203,20 +238,6 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { unbindService(serviceConnection); } - public void onClick(View view) { - if(view == newGroupButton) { - startActivity(new Intent(this, CreateGroupActivity.class)); - } else if(view == composeButton) { - if(adapter.isEmpty()) { - NoGroupsDialog dialog = new NoGroupsDialog(); - dialog.setListener(this); - dialog.show(getSupportFragmentManager(), "NoGroupsDialog"); - } else { - startActivity(new Intent(this, WriteGroupPostActivity.class)); - } - } - } - public void eventOccurred(DatabaseEvent e) { if(e instanceof GroupMessageAddedEvent) { Group g = ((GroupMessageAddedEvent) e).getGroup(); @@ -227,6 +248,16 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { } else if(e instanceof MessageExpiredEvent) { if(LOG.isLoggable(INFO)) LOG.info("Message expired, reloading"); loadHeaders(); + } else if(e instanceof RemoteSubscriptionsUpdatedEvent) { + if(LOG.isLoggable(INFO)) + LOG.info("Remote subscriptions changed, reloading"); + loadAvailable(); + } else if(e instanceof SubscriptionAddedEvent) { + Group g = ((SubscriptionAddedEvent) e).getGroup(); + if(!g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); + loadHeaders(); + } } else if(e instanceof SubscriptionRemovedEvent) { Group g = ((SubscriptionRemovedEvent) e).getGroup(); if(!g.isRestricted()) { @@ -277,23 +308,85 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { }); } + private void loadAvailable() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + int available = 0; + long now = System.currentTimeMillis(); + for(GroupStatus s : db.getAvailableGroups()) { + if(!s.getGroup().isRestricted() && !s.isSubscribed()) + available++; + } + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Loading available took " + duration + " ms"); + displayAvailable(available); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while waiting for service"); + Thread.currentThread().interrupt(); + } + } + }); + } + + public void onClick(View view) { + if(view == newGroupButton) { + startActivity(new Intent(this, CreateGroupActivity.class)); + } else if(view == composeButton) { + if(adapter.isEmpty()) { + NoGroupsDialog dialog = new NoGroupsDialog(); + dialog.setListener(this); + dialog.show(getSupportFragmentManager(), "NoGroupsDialog"); + } else { + startActivity(new Intent(this, WriteGroupPostActivity.class)); + } + } else if(view == manageGroupsButton) { + startActivity(new Intent(this, ManageGroupsActivity.class)); + } + } + public void groupCreationSelected() { startActivity(new Intent(this, CreateGroupActivity.class)); } public void groupCreationCancelled() {} - private static class GroupComparator implements Comparator<GroupListItem> { + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + GroupListItem item = adapter.getItem(position); + if(item == MANAGE) { + startActivity(new Intent(this, ManageGroupsActivity.class)); + } else { + Intent i = new Intent(this, GroupActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", + item.getGroup().getId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName()); + startActivity(i); + } + } + + private static class ItemComparator implements Comparator<GroupListItem> { - private static final GroupComparator INSTANCE = new GroupComparator(); + private static final ItemComparator INSTANCE = new ItemComparator(); public int compare(GroupListItem a, GroupListItem b) { + if(a == b) return 0; + // The manage groups item comes last + if(a == MANAGE) return 1; + if(b == MANAGE) return -1; // 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(); + String aName = a.getGroup().getName(); + String bName = b.getGroup().getName(); return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); } } 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 455941cdbff0d5859e912b55b6e68236b9970bc0..e6bc316444bb2f861ede3cbc938ead219687fad7 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListAdapter.java @@ -1,42 +1,79 @@ package net.sf.briar.android.groups; import static android.graphics.Typeface.BOLD; +import static android.view.Gravity.CENTER; import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.VERTICAL; import static java.text.DateFormat.SHORT; +import static net.sf.briar.android.groups.GroupListItem.MANAGE; import static net.sf.briar.android.widgets.CommonLayoutParams.WRAP_WRAP_1; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; 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.BaseAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -class GroupListAdapter extends ArrayAdapter<GroupListItem> -implements OnItemClickListener { +class GroupListAdapter extends BaseAdapter { + + private final Context ctx; + private final List<GroupListItem> list = new ArrayList<GroupListItem>(); + private int available = 0; GroupListAdapter(Context ctx) { - super(ctx, android.R.layout.simple_expandable_list_item_1, - new ArrayList<GroupListItem>()); + this.ctx = ctx; + } + + public void setAvailable(int available) { + this.available = available; + } + + public void add(GroupListItem item) { + list.add(item); + } + + public void clear() { + list.clear(); + } + + public int getCount() { + return available == 0 ? list.size() : list.size() + 1; + } + + public GroupListItem getItem(int position) { + return position == list.size() ? MANAGE : list.get(position); + } + + public long getItemId(int position) { + return android.R.layout.simple_expandable_list_item_1; } - @Override public View getView(int position, View convertView, ViewGroup parent) { GroupListItem item = getItem(position); - Context ctx = getContext(); Resources res = ctx.getResources(); + if(item == MANAGE) { + TextView manage = new TextView(ctx); + manage.setGravity(CENTER); + manage.setTextSize(18); + manage.setPadding(10, 10, 10, 10); + String format = res.getQuantityString(R.plurals.groups_available, + available); + manage.setText(String.format(format, available)); + return manage; + } + LinearLayout layout = new LinearLayout(ctx); layout.setOrientation(HORIZONTAL); if(item.getUnreadCount() > 0) @@ -52,8 +89,9 @@ implements OnItemClickListener { 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()); + String groupName = item.getGroup().getName(); + if(unread > 0) name.setText(groupName + " (" + unread + ")"); + else name.setText(groupName); innerLayout.addView(name); if(item.isEmpty()) { @@ -97,12 +135,16 @@ implements OnItemClickListener { return layout; } - public void onItemClick(AdapterView<?> parent, View view, int position, - long id) { - GroupListItem item = getItem(position); - Intent i = new Intent(getContext(), GroupActivity.class); - i.putExtra("net.sf.briar.GROUP_ID", item.getGroupId().getBytes()); - i.putExtra("net.sf.briar.GROUP_NAME", item.getGroupName()); - getContext().startActivity(i); + @Override + public boolean isEmpty() { + return false; + } + + public void remove(GroupListItem item) { + list.remove(item); + } + + public void sort(Comparator<GroupListItem> comparator) { + Collections.sort(list, comparator); } } 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 0fc071ad398c4a964c71a64ce3e331c8dbba56c0..7348ed7dbfe6a2651929d739f1717517e899ee2b 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListItem.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListItem.java @@ -9,10 +9,12 @@ 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 GroupListItem { + static final GroupListItem MANAGE = new GroupListItem(null, + Collections.<GroupMessageHeader>emptyList()); + private final Group group; private final boolean empty; private final String authorName, contentType, subject; @@ -45,12 +47,8 @@ class GroupListItem { } } - GroupId getGroupId() { - return group.getId(); - } - - String getGroupName() { - return group.getName(); + Group getGroup() { + return group; } boolean isEmpty() { diff --git a/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..ced4d15da86af602cd3e326dbf1d1f4a49db906c --- /dev/null +++ b/briar-android/src/net/sf/briar/android/groups/ManageGroupsActivity.java @@ -0,0 +1,168 @@ +package net.sf.briar.android.groups; + +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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import net.sf.briar.android.BriarFragmentActivity; +import net.sf.briar.android.BriarService; +import net.sf.briar.android.BriarService.BriarServiceConnection; +import net.sf.briar.android.ManageGroupsAdapter; +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.event.DatabaseEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.RemoteSubscriptionsUpdatedEvent; +import net.sf.briar.api.db.event.SubscriptionAddedEvent; +import net.sf.briar.api.db.event.SubscriptionRemovedEvent; +import net.sf.briar.api.messaging.Group; +import net.sf.briar.api.messaging.GroupStatus; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +import com.google.inject.Inject; + +public class ManageGroupsActivity extends BriarFragmentActivity +implements DatabaseListener, OnItemClickListener { + + private static final Logger LOG = + Logger.getLogger(ManageGroupsActivity.class.getName()); + + private final BriarServiceConnection serviceConnection = + new BriarServiceConnection(); + + private ManageGroupsAdapter 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; + + @Override + public void onCreate(Bundle state) { + super.onCreate(null); + + adapter = new ManageGroupsAdapter(this); + list = new ListView(this); + list.setLayoutParams(MATCH_MATCH); + list.setAdapter(adapter); + list.setOnItemClickListener(this); + setContentView(list); + + // 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); + loadAvailableGroups(); + } + + private void loadAvailableGroups() { + dbUiExecutor.execute(new Runnable() { + public void run() { + try { + serviceConnection.waitForStartup(); + long now = System.currentTimeMillis(); + List<GroupStatus> available = new ArrayList<GroupStatus>(); + for(GroupStatus s : db.getAvailableGroups()) + if(!s.getGroup().isRestricted()) available.add(s); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + available = Collections.unmodifiableList(available); + displayAvailableGroups(available); + } 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 displayAvailableGroups( + final Collection<GroupStatus> available) { + runOnUiThread(new Runnable() { + public void run() { + adapter.clear(); + for(GroupStatus g : available) adapter.add(g); + adapter.sort(ItemComparator.INSTANCE); + adapter.notifyDataSetChanged(); + } + }); + } + + @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 RemoteSubscriptionsUpdatedEvent) { + if(LOG.isLoggable(INFO)) + LOG.info("Remote subscriptions changed, reloading"); + loadAvailableGroups(); + } else if(e instanceof SubscriptionAddedEvent) { + Group g = ((SubscriptionAddedEvent) e).getGroup(); + if(g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group added, reloading"); + loadAvailableGroups(); + } + } else if(e instanceof SubscriptionRemovedEvent) { + Group g = ((SubscriptionRemovedEvent) e).getGroup(); + if(g.isRestricted()) { + if(LOG.isLoggable(INFO)) LOG.info("Group removed, reloading"); + loadAvailableGroups(); + } + } + } + + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + GroupStatus item = adapter.getItem(position); + Intent i = new Intent(this, ConfigureGroupActivity.class); + i.putExtra("net.sf.briar.GROUP_ID", item.getGroup().getId().getBytes()); + i.putExtra("net.sf.briar.GROUP_NAME", item.getGroup().getName()); + i.putExtra("net.sf.briar.SUBSCRIBED", item.isSubscribed()); + i.putExtra("net.sf.briar.VISIBLE_TO_ALL", item.isVisibleToAll()); + startActivity(i); + } + + private static class ItemComparator implements Comparator<GroupStatus> { + + private static final ItemComparator INSTANCE = new ItemComparator(); + + public int compare(GroupStatus a, GroupStatus b) { + String aName = a.getGroup().getName(); + String bName = b.getGroup().getName(); + return String.CASE_INSENSITIVE_ORDER.compare(aName, bName); + } + } +} diff --git a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java index e0d9493ece11783b611919b27da536db7bc3b2ff..995d7927e92e9bc874c13127a2c6966267a810c1 100644 --- a/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java +++ b/briar-android/src/net/sf/briar/android/identity/CreateIdentityActivity.java @@ -169,11 +169,7 @@ implements OnEditorActionListener, OnClickListener { LOG.info("Interrupted while waiting for service"); Thread.currentThread().interrupt(); } - runOnUiThread(new Runnable() { - public void run() { - finish(); - } - }); + finishOnUiThread(); } }); } diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java index 16db46e339bf51093e890eec96b3736358e5bd69..379cedee6116dc13533d75e7f35b0202e47a6abd 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationListActivity.java @@ -134,7 +134,7 @@ implements OnClickListener, DatabaseListener, NoContactsDialog.Listener { if(item != null) adapter.remove(item); // Add a new item adapter.add(new ConversationListItem(c, headers)); - adapter.sort(ConversationComparator.INSTANCE); + adapter.sort(ItemComparator.INSTANCE); adapter.notifyDataSetChanged(); selectFirstUnread(); } @@ -244,11 +244,10 @@ implements OnClickListener, DatabaseListener, NoContactsDialog.Listener { public void contactCreationCancelled() {} - private static class ConversationComparator + private static class ItemComparator implements Comparator<ConversationListItem> { - static final ConversationComparator INSTANCE = - new ConversationComparator(); + private static final ItemComparator INSTANCE = new ItemComparator(); public int compare(ConversationListItem a, ConversationListItem b) { // The item with the newest message comes first diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index 96c48cc7a9cfc8bb4ba668e8b3e7efad80643515..c0b3e43e43dd577926237cd1f35aea5c13757e6a 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -16,6 +16,7 @@ import net.sf.briar.api.db.event.DatabaseListener; import net.sf.briar.api.messaging.Ack; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; @@ -162,11 +163,8 @@ public interface DatabaseComponent { Collection<TransportUpdate> generateTransportUpdates(ContactId c, long maxLatency) throws DbException; - /** - * Returns any groups that contacts have made visible but to which the user - * does not subscribe. - */ - Collection<Group> getAvailableGroups() throws DbException; + /** Returns the status of all groups to which the user can subscribe. */ + Collection<GroupStatus> getAvailableGroups() throws DbException; /** Returns the configuration for the given transport. */ TransportConfig getConfig(TransportId t) throws DbException; @@ -362,7 +360,7 @@ public interface DatabaseComponent { * If <tt>visible</tt> is true, the group is also made visible to all * current contacts. */ - void setVisibleToAll(GroupId g, boolean visible) throws DbException; + void setVisibleToAll(GroupId g, boolean all) throws DbException; /** * Subscribes to the given group, or returns false if the user already has diff --git a/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java b/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..35074ed36fd90819f2f25b004ac4ce7d5b0c9298 --- /dev/null +++ b/briar-api/src/net/sf/briar/api/messaging/GroupStatus.java @@ -0,0 +1,25 @@ +package net.sf.briar.api.messaging; + +public class GroupStatus { + + private final Group group; + private final boolean subscribed, visibleToAll; + + public GroupStatus(Group group, boolean subscribed, boolean visibleToAll) { + this.group = group; + this.subscribed = subscribed; + this.visibleToAll = visibleToAll; + } + + public Group getGroup() { + return group; + } + + public boolean isSubscribed() { + return subscribed; + } + + public boolean isVisibleToAll() { + return visibleToAll; + } +} diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index 3af45362f3549f7360fb8b6bfc93311cadffbb30..a54751b800a8b1c67291ed999736598e5db1f457 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -17,6 +17,7 @@ import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; @@ -191,6 +192,14 @@ interface Database<T> { */ boolean containsContact(T txn, ContactId c) throws DbException; + /** + * Returns true if the database contains the given restricted group to + * which the user can post messages. + * <p> + * Locking: identity read. + */ + boolean containsLocalGroup(T txn, GroupId g) throws DbException; + /** * Returns true if the database contains the given message. * <p> @@ -222,10 +231,11 @@ interface Database<T> { throws DbException; /** - * Returns any groups that contacts have made visible but to which the user - * does not subscribe. + * Returns the status of all groups to which the user can subscribe. + * <p> + * Locking: subscription read. */ - Collection<Group> getAvailableGroups(T txn) throws DbException; + Collection<GroupStatus> getAvailableGroups(T txn) throws DbException; /** * Returns the configuration for the given transport. @@ -621,6 +631,13 @@ interface Database<T> { */ void removeContact(T txn, ContactId c) throws DbException; + /** + * Removes the given restricted group to which the user can post messages. + * <p> + * Locking: identity write. + */ + void removeLocalGroup(T txn, GroupId g) throws DbException; + /** * Removes a message (and all associated state) from the database. * <p> @@ -797,7 +814,7 @@ interface Database<T> { * <p> * Locking: subscription write. */ - void setVisibleToAll(T txn, GroupId g, boolean visible) throws DbException; + void setVisibleToAll(T txn, GroupId g, boolean all) throws DbException; /** * Updates the expiry times of the given messages with respect to the given diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index 70aa0adc9189b369d98169ed7a1e0a993cf49ed1..b1048be58b8ccc8a05d9136406ab510b9af88911 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -64,6 +64,7 @@ import net.sf.briar.api.lifecycle.ShutdownManager; import net.sf.briar.api.messaging.Ack; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; @@ -887,12 +888,12 @@ DatabaseCleaner.Callback { } } - public Collection<Group> getAvailableGroups() throws DbException { + public Collection<GroupStatus> getAvailableGroups() throws DbException { subscriptionLock.readLock().lock(); try { T txn = db.startTransaction(); try { - Collection<Group> groups = db.getAvailableGroups(txn); + Collection<GroupStatus> groups = db.getAvailableGroups(txn); db.commitTransaction(txn); return groups; } catch(DbException e) { @@ -2051,7 +2052,7 @@ DatabaseCleaner.Callback { callListeners(new LocalSubscriptionsUpdatedEvent(affected)); } - public void setVisibleToAll(GroupId g, boolean visible) throws DbException { + public void setVisibleToAll(GroupId g, boolean all) throws DbException { Collection<ContactId> affected = new ArrayList<ContactId>(); contactLock.readLock().lock(); try { @@ -2062,8 +2063,8 @@ DatabaseCleaner.Callback { if(!db.containsSubscription(txn, g)) throw new NoSuchSubscriptionException(); // Make the group visible or invisible to future contacts - db.setVisibleToAll(txn, g, visible); - if(visible) { + db.setVisibleToAll(txn, g, all); + if(all) { // Make the group visible to all current contacts Collection<ContactId> before = db.getVisibility(txn, g); before = new HashSet<ContactId>(before); @@ -2111,27 +2112,34 @@ DatabaseCleaner.Callback { public void unsubscribe(Group g) throws DbException { Collection<ContactId> affected; - messageLock.writeLock().lock(); + identityLock.writeLock().lock(); try { - subscriptionLock.writeLock().lock(); + messageLock.writeLock().lock(); try { - T txn = db.startTransaction(); + subscriptionLock.writeLock().lock(); try { - GroupId id = g.getId(); - if(!db.containsSubscription(txn, id)) - throw new NoSuchSubscriptionException(); - affected = db.getVisibility(txn, id); - db.removeSubscription(txn, id); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + T txn = db.startTransaction(); + try { + GroupId id = g.getId(); + if(!db.containsSubscription(txn, id)) + throw new NoSuchSubscriptionException(); + affected = db.getVisibility(txn, id); + db.removeSubscription(txn, id); + if(db.containsLocalGroup(txn, id)) + db.removeLocalGroup(txn, id); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); } } finally { - subscriptionLock.writeLock().unlock(); + messageLock.writeLock().unlock(); } } finally { - messageLock.writeLock().unlock(); + identityLock.writeLock().unlock(); } callListeners(new SubscriptionRemovedEvent(g)); callListeners(new LocalSubscriptionsUpdatedEvent(affected)); diff --git a/briar-core/src/net/sf/briar/db/H2Database.java b/briar-core/src/net/sf/briar/db/H2Database.java index 5203f64830c1c01b05c1fc1b58f7aba9ce011d45..600b0375b859eca16123d18f2b9a9c6c8567bbed 100644 --- a/briar-core/src/net/sf/briar/db/H2Database.java +++ b/briar-core/src/net/sf/briar/db/H2Database.java @@ -75,7 +75,9 @@ class H2Database extends JdbcDatabase { @Override protected Connection createConnection() throws SQLException { - char[] password = encodePassword(config.getEncryptionKey()); + byte[] key = config.getEncryptionKey(); + if(key == null) throw new IllegalStateException(); + char[] password = encodePassword(key); Properties props = new Properties(); props.setProperty("user", "user"); props.put("password", password); diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index d33c66950ed114611268b634c25b995e186e26cb..5ab537972db591177f0bc06e8c4d3956c1a19642 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -19,10 +19,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.logging.Logger; import net.sf.briar.api.Author; @@ -40,6 +42,7 @@ import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; @@ -743,8 +746,7 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void addLocalGroup(Connection txn, LocalGroup g) - throws DbException { + public void addLocalGroup(Connection txn, LocalGroup g) throws DbException { PreparedStatement ps = null; try { String sql = "INSERT INTO localGroups" @@ -1053,6 +1055,27 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public boolean containsLocalGroup(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM localGroups WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return found; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public boolean containsMessage(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; @@ -1139,25 +1162,41 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<Group> getAvailableGroups(Connection txn) + public Collection<GroupStatus> getAvailableGroups(Connection txn) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT cg.groupId, cg.name, cg.publicKey" - + " FROM contactGroups AS cg" - + " LEFT OUTER JOIN groups AS g" - + " ON cg.groupId = g.groupId" - + " WHERE g.groupId IS NULL" - + " GROUP BY cg.groupId"; + // Add all subscribed groups to the list + String sql = "SELECT groupId, name, publicKey, visibleToAll" + + " FROM groups"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + List<GroupStatus> groups = new ArrayList<GroupStatus>(); + Set<GroupId> subscribed = new HashSet<GroupId>(); + while(rs.next()) { + GroupId id = new GroupId(rs.getBytes(1)); + subscribed.add(id); + String name = rs.getString(2); + byte[] publicKey = rs.getBytes(3); + Group group = new Group(id, name, publicKey); + boolean visibleToAll = rs.getBoolean(4); + groups.add(new GroupStatus(group, true, visibleToAll)); + } + rs.close(); + ps.close(); + // Add all contact groups to the list, unless already added + sql = "SELECT DISTINCT groupId, name, publicKey" + + " FROM contactGroups"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); - List<Group> groups = new ArrayList<Group>(); while(rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); + if(subscribed.contains(id)) continue; String name = rs.getString(2); byte[] publicKey = rs.getBytes(3); - groups.add(new Group(id, name, publicKey)); + Group group = new Group(id, name, publicKey); + groups.add(new GroupStatus(group, false, false)); } rs.close(); ps.close(); @@ -2765,6 +2804,21 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void removeLocalGroup(Connection txn, GroupId g) throws DbException { + PreparedStatement ps = null; + try { + String sql = "DELETE FROM localGroups WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void removeMessage(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; try { @@ -3406,13 +3460,13 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setVisibleToAll(Connection txn, GroupId g, boolean visible) + public void setVisibleToAll(Connection txn, GroupId g, boolean all) throws DbException { PreparedStatement ps = null; try { String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?"; ps = txn.prepareStatement(sql); - ps.setBoolean(1, visible); + ps.setBoolean(1, all); ps.setBytes(2, g.getBytes()); int affected = ps.executeUpdate(); if(affected > 1) throw new DbStateException(); diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index b1e8cfcf6b5479abb54b89b8f87f85608887b47e..02e69ad48b2fa177303099038131b728d9364398 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -187,6 +187,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.emptyList())); oneOf(database).removeSubscription(txn, groupId); + oneOf(database).containsLocalGroup(txn, groupId); + will(returnValue(false)); oneOf(listener).eventOccurred(with(any( SubscriptionRemovedEvent.class))); oneOf(listener).eventOccurred(with(any( diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java index 65b53ff1be634d71f11e5c20fbc2ecb2c49d82b5..632f0cb72e163a80e7f3cf6ce05622a490d2f630 100644 --- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -35,6 +35,7 @@ import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.GroupMessageHeader; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.GroupStatus; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.transport.Endpoint; @@ -1831,21 +1832,82 @@ public class H2DatabaseTest extends BriarTestCase { @Test public void testGetAvailableGroups() throws Exception { + ContactId contactId1 = new ContactId(2); + AuthorId authorId1 = new AuthorId(TestUtils.getRandomId()); + Author author1 = new Author(authorId1, "Carol", new byte[60]); + Database<Connection> db = open(false); Connection txn = db.startTransaction(); - // Add a contact who subscribes to a group + // Add two contacts who subscribe to a group db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); + assertEquals(contactId1, db.addContact(txn, author1, localAuthorId)); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.setSubscriptions(txn, contactId1, Arrays.asList(group), 1); - // The group should be available + // The group should be available, not subscribed, not visible to all assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); - assertEquals(Arrays.asList(group), db.getAvailableGroups(txn)); + Iterator<GroupStatus> it = db.getAvailableGroups(txn).iterator(); + assertTrue(it.hasNext()); + GroupStatus status = it.next(); + assertEquals(group, status.getGroup()); + assertFalse(status.isSubscribed()); + assertFalse(status.isVisibleToAll()); + assertFalse(it.hasNext()); - // Subscribe to the group - it should no longer be available + // Subscribe to the group - it should be available, subscribed, + // not visible to all db.addSubscription(txn, group); assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + it = db.getAvailableGroups(txn).iterator(); + assertTrue(it.hasNext()); + status = it.next(); + assertEquals(group, status.getGroup()); + assertTrue(status.isSubscribed()); + assertFalse(status.isVisibleToAll()); + assertFalse(it.hasNext()); + + // The first contact unsubscribes - the group should be available, + // subscribed, not visible to all + db.setSubscriptions(txn, contactId, Collections.<Group>emptyList(), 2); + assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + it = db.getAvailableGroups(txn).iterator(); + assertTrue(it.hasNext()); + status = it.next(); + assertEquals(group, status.getGroup()); + assertTrue(status.isSubscribed()); + assertFalse(status.isVisibleToAll()); + assertFalse(it.hasNext()); + + // Make the group visible to all contacts - it should be available, + // subscribed, visible to all + db.setVisibleToAll(txn, groupId, true); + assertEquals(Arrays.asList(group), db.getSubscriptions(txn)); + it = db.getAvailableGroups(txn).iterator(); + assertTrue(it.hasNext()); + status = it.next(); + assertEquals(group, status.getGroup()); + assertTrue(status.isSubscribed()); + assertTrue(status.isVisibleToAll()); + assertFalse(it.hasNext()); + + // Unsubscribe from the group - it should be available, not subscribed, + // not visible to all + db.removeSubscription(txn, groupId); + assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); + it = db.getAvailableGroups(txn).iterator(); + assertTrue(it.hasNext()); + status = it.next(); + assertEquals(group, status.getGroup()); + assertFalse(status.isSubscribed()); + assertFalse(status.isVisibleToAll()); + assertFalse(it.hasNext()); + + // The second contact unsubscribes - the group should no longer be + // available + db.setSubscriptions(txn, contactId1, Collections.<Group>emptyList(), 2); + assertEquals(Collections.emptyList(), db.getSubscriptions(txn)); assertEquals(Collections.emptyList(), db.getAvailableGroups(txn)); db.commitTransaction(txn);