From 4dcf9f632ed59515da3973a44feca6ae05231fed Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Fri, 21 Mar 2014 18:08:08 +0000
Subject: [PATCH] Show which contacts subscribe to each forum. Dev task #79.

---
 briar-android/res/values/strings.xml          |   8 ++
 .../groups/ConfigureGroupActivity.java        | 128 ++++++++++++++++--
 .../api/db/DatabaseComponent.java             |   3 +
 .../src/org/briarproject/db/Database.java     |   7 +
 .../db/DatabaseComponentImpl.java             |  17 +++
 .../src/org/briarproject/db/JdbcDatabase.java |  34 +++++
 6 files changed, 187 insertions(+), 10 deletions(-)

diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index b605c730a5..716715ba11 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 c5c2faa1a4..b0ba112b3b 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 37ba4d4566..2d35402566 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 2e6d3e0bf5..ad645ddfb1 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 e25972c204..ed245931e0 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 3ea9e1abb8..42214788e8 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;
-- 
GitLab