diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index b605c730a5207c36a531ed5a3f87720161f6e5c8..716715ba117e6ba37e5ab5f4bf7c8529e11b683c 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -53,6 +53,14 @@ </plurals> <string name="no_posts">No posts</string> <string name="subscribe_to_this_forum">Subscribe to this forum</string> + <string name="no_subscribers">No contacts subscribe to this forum</string> + <plurals name="subscribers"> + <item quantity="one">%d contact subscribes to this forum:</item> + <item quantity="other">%d contacts subscribe to this forum:</item> + </plurals> + <string name="public_space_warning">Forums are public spaces. There may be other subscribers who are not your contacts.</string> + <string name="subscribe_button">Subscribe</string> + <string name="unsubscribe_button">Unsubscribe</string> <string name="create_forum_title">New Forum</string> <string name="choose_forum_name">Choose a name for your forum:</string> <string name="forum_visible_to_all">Share this forum with all contacts</string> diff --git a/briar-android/src/org/briarproject/android/groups/ConfigureGroupActivity.java b/briar-android/src/org/briarproject/android/groups/ConfigureGroupActivity.java index c5c2faa1a46c856c1f0bc97a5db9aac15b72a762..b0ba112b3b2466044ec9e05190aadc08e49f99c3 100644 --- a/briar-android/src/org/briarproject/android/groups/ConfigureGroupActivity.java +++ b/briar-android/src/org/briarproject/android/groups/ConfigureGroupActivity.java @@ -1,17 +1,21 @@ package org.briarproject.android.groups; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static android.view.Gravity.CENTER; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static android.widget.LinearLayout.HORIZONTAL; import static android.widget.LinearLayout.VERTICAL; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.logging.Logger; import javax.inject.Inject; @@ -25,22 +29,29 @@ import org.briarproject.api.Contact; import org.briarproject.api.ContactId; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent; +import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.messaging.Group; import org.briarproject.api.messaging.GroupId; import android.content.Intent; +import android.content.res.Resources; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.CheckBox; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RadioButton; import android.widget.RadioGroup; +import android.widget.TextView; public class ConfigureGroupActivity extends BriarActivity -implements OnClickListener, NoContactsDialog.Listener, +implements OnClickListener, EventListener, NoContactsDialog.Listener, SelectContactsDialog.Listener { private static final Logger LOG = @@ -51,6 +62,8 @@ SelectContactsDialog.Listener { private RadioGroup radioGroup = null; private RadioButton visibleToAll = null, visibleToSome = null; private Button doneButton = null; + private TextView subscribers = null; + private TextView subscriberNames = null; private ProgressBar progress = null; private boolean changed = false; @@ -86,6 +99,32 @@ SelectContactsDialog.Listener { int pad = LayoutUtils.getPadding(this); layout.setPadding(pad, pad, pad, pad); + subscribers = new TextView(this); + subscribers.setGravity(CENTER); + subscribers.setTextSize(18); + subscribers.setText("\u2026"); + layout.addView(subscribers); + + subscriberNames = new TextView(this); + subscriberNames.setGravity(CENTER); + subscriberNames.setTextSize(18); + subscriberNames.setVisibility(GONE); + layout.addView(subscriberNames); + + LinearLayout warning = new LinearLayout(this); + warning.setOrientation(HORIZONTAL); + warning.setPadding(pad, pad, pad, pad); + + ImageView icon = new ImageView(this); + icon.setImageResource(R.drawable.action_about); + warning.addView(icon); + + TextView publicSpace = new TextView(this); + publicSpace.setPadding(pad, 0, 0, 0); + publicSpace.setText(R.string.public_space_warning); + warning.addView(publicSpace); + layout.addView(warning); + subscribeCheckBox = new CheckBox(this); subscribeCheckBox.setId(1); subscribeCheckBox.setText(R.string.subscribe_to_this_forum); @@ -129,6 +168,65 @@ SelectContactsDialog.Listener { setContentView(layout); } + @Override + public void onResume() { + super.onResume(); + db.addListener(this); + loadSubscribers(); + } + + private void loadSubscribers() { + runOnDbThread(new Runnable() { + public void run() { + try { + long now = System.currentTimeMillis(); + Collection<Contact> contacts = db.getSubscribers(groupId); + long duration = System.currentTimeMillis() - now; + if(LOG.isLoggable(INFO)) + LOG.info("Load took " + duration + " ms"); + displaySubscribers(contacts); + } catch(DbException e) { + if(LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void displaySubscribers(final Collection<Contact> contacts) { + runOnUiThread(new Runnable() { + public void run() { + if(contacts.isEmpty()) { + subscribers.setText(R.string.no_subscribers); + subscriberNames.setVisibility(GONE); + } else { + int count = contacts.size(); + Resources res = getResources(); + String title = res.getQuantityString(R.plurals.subscribers, + count, count); + subscribers.setText(title); + List<String> names = new ArrayList<String>(); + for(Contact c : contacts) + names.add(c.getAuthor().getName()); + Collections.sort(names, String.CASE_INSENSITIVE_ORDER); + StringBuilder s = new StringBuilder(); + for(int i = 0; i < count; i++) { + s.append(names.get(i)); + if(i + 1 < count) s.append(", "); + } + subscriberNames.setText(s.toString()); + subscriberNames.setVisibility(VISIBLE); + } + } + }); + } + + @Override + public void onPause() { + super.onPause(); + db.removeListener(this); + } + public void onClick(View view) { if(view == subscribeCheckBox) { changed = true; @@ -139,24 +237,24 @@ SelectContactsDialog.Listener { changed = true; } else if(view == visibleToSome) { changed = true; - if(contacts == null) loadContacts(); - else displayContacts(); + if(contacts == null) loadVisibleContacts(); + else displayVisibleContacts(); } else if(view == doneButton) { if(changed) { - boolean subscribe = subscribeCheckBox.isChecked(); - boolean all = visibleToAll.isChecked(); // Replace the button with a progress bar doneButton.setVisibility(GONE); progress.setVisibility(VISIBLE); - // Update the blog in a background thread - if(subscribe || subscribed) updateGroup(subscribe, all); + // Update the group in a background thread + boolean subscribe = subscribeCheckBox.isChecked(); + boolean all = visibleToAll.isChecked(); + updateGroup(subscribe, all); } else { finish(); } } } - private void loadContacts() { + private void loadVisibleContacts() { runOnDbThread(new Runnable() { public void run() { try { @@ -166,7 +264,7 @@ SelectContactsDialog.Listener { long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); - displayContacts(); + displayVisibleContacts(); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -175,7 +273,7 @@ SelectContactsDialog.Listener { }); } - private void displayContacts() { + private void displayVisibleContacts() { runOnUiThread(new Runnable() { public void run() { if(contacts.isEmpty()) { @@ -232,6 +330,16 @@ SelectContactsDialog.Listener { }); } + public void eventOccurred(Event e) { + if(e instanceof LocalSubscriptionsUpdatedEvent) { + LOG.info("Local subscriptions updated, reloading"); + loadSubscribers(); + } else if(e instanceof RemoteSubscriptionsUpdatedEvent) { + LOG.info("Remote subscriptions updated, reloading"); + loadSubscribers(); + } + } + public void contactCreationSelected() { startActivity(new Intent(this, AddContactActivity.class)); } diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index 37ba4d4566f7d390d456ae185ace8d81a11e1ca1..2d35402566e204a09f0ab60f0e3320f08f8be5ee 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -231,6 +231,9 @@ public interface DatabaseComponent { /** Returns all settings. */ Settings getSettings() throws DbException; + /** Returns all contacts who subscribe to the given group. */ + Collection<Contact> getSubscribers(GroupId g) throws DbException; + /** Returns the maximum latencies of all local transports. */ Map<TransportId, Long> getTransportLatencies() throws DbException; diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 2e6d3e0bf50d838c1d5e82e76e54b928905a0a7f..ad645ddfb10531f4d2c627ae825239b9f7a86a2f 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -486,6 +486,13 @@ interface Database<T> { */ Settings getSettings(T txn) throws DbException; + /** + * Returns all contacts who subscribe to the given group. + * <p> + * Locking: subscription read. + */ + Collection<Contact> getSubscribers(T txn, GroupId g) throws DbException; + /** * Returns a subscription ack for the given contact, or null if no ack is * due. diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index e25972c2047854618f809a9ba7efc2026022a7fc..ed245931e0a26690485c8dc1197bad27e30f0f99 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -1154,6 +1154,23 @@ DatabaseCleaner.Callback { } } + public Collection<Contact> getSubscribers(GroupId g) throws DbException { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + Collection<Contact> contacts = db.getSubscribers(txn, g); + db.commitTransaction(txn); + return contacts; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } + public Map<TransportId, Long> getTransportLatencies() throws DbException { transportLock.readLock().lock(); try { diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index 3ea9e1abb87b7bdeca2dbfbbdc42845776524432..42214788e855005b03aae50fb4eddb0f58212ca9 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -2154,6 +2154,40 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Collection<Contact> getSubscribers(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT c.contactId, authorId, c.name, publicKey," + + " localAuthorId" + + " FROM contacts AS c" + + " JOIN contactGroups AS cg" + + " ON c.contactId = cg.contactId" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + List<Contact> contacts = new ArrayList<Contact>(); + while(rs.next()) { + ContactId contactId = new ContactId(rs.getInt(1)); + AuthorId authorId = new AuthorId(rs.getBytes(2)); + String name = rs.getString(3); + byte[] publicKey = rs.getBytes(4); + Author author = new Author(authorId, name, publicKey); + AuthorId localAuthorId = new AuthorId(rs.getBytes(5)); + contacts.add(new Contact(contactId, author, localAuthorId)); + } + rs.close(); + ps.close(); + return Collections.unmodifiableList(contacts); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public SubscriptionAck getSubscriptionAck(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null;