diff --git a/briar-android/src/org/briarproject/android/BriarActivity.java b/briar-android/src/org/briarproject/android/BriarActivity.java index 49dc574a858a2590b120bba32c74ae28a6291294..4c01808af7cba45255b7d091657471145c1d3112 100644 --- a/briar-android/src/org/briarproject/android/BriarActivity.java +++ b/briar-android/src/org/briarproject/android/BriarActivity.java @@ -26,6 +26,7 @@ public abstract class BriarActivity extends BaseActivity { "briar.LOCAL_AUTHOR_HANDLE"; public static final String KEY_STARTUP_FAILED = "briar.STARTUP_FAILED"; public static final String GROUP_ID = "briar.GROUP_ID"; + public static final String GROUP_NAME = "briar.GROUP_NAME"; public static final int REQUEST_PASSWORD = 1; diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java index ff1644b1c95052d0d28d3849d7bf3be891d6ee94..4c902459998fd68ce540933aa72f1295869b2c34 100644 --- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java @@ -31,7 +31,6 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static org.briarproject.android.forum.ForumActivity.FORUM_NAME; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; public class CreateForumActivity extends BriarActivity @@ -150,7 +149,7 @@ public class CreateForumActivity extends BriarActivity Intent i = new Intent(CreateForumActivity.this, ForumActivity.class); i.putExtra(GROUP_ID, f.getId().getBytes()); - i.putExtra(FORUM_NAME, f.getName()); + i.putExtra(GROUP_NAME, f.getName()); startActivity(i); Toast.makeText(CreateForumActivity.this, R.string.forum_created_toast, LENGTH_LONG).show(); diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index 0d74863a87e2a5d3c1d59cd9c2fae41a4164ee1b..74537eed76d8c4ee4fc16948803d96c1d7853a61 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -2,9 +2,7 @@ package org.briarproject.android.forum; import android.content.DialogInterface; import android.content.Intent; -import android.os.Bundle; import android.support.annotation.LayoutRes; -import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.app.AlertDialog; @@ -17,18 +15,13 @@ import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.controller.handler.UiResultExceptionHandler; -import org.briarproject.android.controller.handler.UiResultHandler; -import org.briarproject.android.forum.ForumController.ForumPostListener; import org.briarproject.android.sharing.ShareForumActivity; import org.briarproject.android.sharing.SharingStatusForumActivity; import org.briarproject.android.threaded.ThreadListActivity; +import org.briarproject.android.threaded.ThreadListController; import org.briarproject.api.db.DbException; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumPostHeader; -import org.briarproject.util.StringUtils; - -import java.util.ArrayList; -import java.util.List; import javax.inject.Inject; @@ -37,10 +30,8 @@ import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation; import static android.widget.Toast.LENGTH_SHORT; -public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAdapter> - implements ForumPostListener { - - static final String FORUM_NAME = "briar.FORUM_NAME"; +public class ForumActivity extends + ThreadListActivity<Forum, ForumEntry, ForumPostHeader, NestedForumAdapter> { private static final int REQUEST_FORUM_SHARED = 3; @@ -53,41 +44,8 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda } @Override - public void onCreate(final Bundle state) { - super.onCreate(state); - - Intent i = getIntent(); - String forumName = i.getStringExtra(FORUM_NAME); - if (forumName != null) setTitle(forumName); - - forumController.loadForum(groupId, - new UiResultExceptionHandler<List<ForumEntry>, DbException>( - this) { - @Override - public void onResultUi(List<ForumEntry> result) { - Forum forum = forumController.getForum(); - if (forum != null) setTitle(forum.getName()); - List<ForumEntry> entries = new ArrayList<>(result); - if (entries.isEmpty()) { - list.showData(); - } else { - adapter.setItems(entries); - list.showData(); - if (state != null) { - byte[] replyId = - state.getByteArray(KEY_REPLY_ID); - if (replyId != null) - adapter.setReplyItemById(replyId); - } - } - } - - @Override - public void onExceptionUi(DbException exception) { - // TODO Improve UX ? - finish(); - } - }); + protected ThreadListController<Forum, ForumEntry, ForumPostHeader> getController() { + return forumController; } @Override @@ -101,12 +59,6 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda return new NestedForumAdapter(this, layoutManager); } - @Override - public void onResume() { - super.onResume(); - notificationManager.clearForumPostNotification(groupId); - } - @Override protected void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); @@ -157,55 +109,6 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda } } - protected void markItemRead(ForumEntry entry) { - forumController.entryRead(entry); - } - - @Override - public void onForumPostReceived(ForumPostHeader header) { - forumController.loadPost(header, - new UiResultExceptionHandler<ForumEntry, DbException>(this) { - @Override - public void onResultUi(final ForumEntry result) { - addItem(result, false); - } - - @Override - public void onExceptionUi(DbException exception) { - // TODO add proper exception handling - } - }); - } - - @Override - protected void sendItem(String text, @Nullable ForumEntry replyItem) { - UiResultExceptionHandler<ForumEntry, DbException> handler = - new UiResultExceptionHandler<ForumEntry, DbException>(this) { - @Override - public void onResultUi(ForumEntry result) { - addItem(result, true); - } - - @Override - public void onExceptionUi(DbException exception) { - // TODO Improve UX ? - finish(); - } - }; - if (replyItem == null) { - // root post - forumController.createPost(StringUtils.toUtf8(text), handler); - } else { - forumController.createPost(StringUtils.toUtf8(text), - replyItem.getId(), handler); - } - } - - @Override - public void onForumRemoved() { - supportFinishAfterTransition(); - } - @Override protected int getItemPostedString() { return R.string.forum_new_entry_posted; @@ -220,18 +123,24 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda DialogInterface.OnClickListener okListener = new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { - forumController.unsubscribe( - new UiResultHandler<Boolean>( + public void onClick(final DialogInterface dialog, + int which) { + forumController.deleteGroupItem( + new UiResultExceptionHandler<Void, DbException>( ForumActivity.this) { @Override - public void onResultUi(Boolean result) { - if (result) { - Toast.makeText(ForumActivity.this, - R.string.forum_left_toast, - LENGTH_SHORT) - .show(); - } + public void onResultUi(Void v) { + Toast.makeText(ForumActivity.this, + R.string.forum_left_toast, + LENGTH_SHORT) + .show(); + } + + @Override + public void onExceptionUi( + DbException exception) { + // TODO proper error handling + dialog.dismiss(); } }); } @@ -241,8 +150,8 @@ public class ForumActivity extends ThreadListActivity<ForumEntry, NestedForumAda R.style.BriarDialogTheme); builder.setTitle(getString(R.string.dialog_title_leave_forum)); builder.setMessage(getString(R.string.dialog_message_leave_forum)); - builder.setPositiveButton(R.string.dialog_button_leave, okListener); - builder.setNegativeButton(android.R.string.cancel, null); + builder.setNegativeButton(R.string.dialog_button_leave, okListener); + builder.setPositiveButton(R.string.cancel, null); builder.show(); } diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java index ad08dbc4dd534a918736c43486fcfc6fa44917ce..275ccc135e3344272e961a8b133f738061a9be5f 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumController.java +++ b/briar-android/src/org/briarproject/android/forum/ForumController.java @@ -1,51 +1,12 @@ package org.briarproject.android.forum; -import android.support.annotation.Nullable; -import android.support.annotation.UiThread; - -import org.briarproject.android.DestroyableContext; -import org.briarproject.android.controller.ActivityLifecycleController; import org.briarproject.android.controller.handler.ResultExceptionHandler; -import org.briarproject.android.controller.handler.ResultHandler; +import org.briarproject.android.threaded.ThreadListController; import org.briarproject.api.db.DbException; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumPostHeader; -import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.MessageId; - -import java.util.Collection; -import java.util.List; - -public interface ForumController extends ActivityLifecycleController { - - void loadForum(GroupId groupId, - ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler); - - @Nullable - Forum getForum(); - - void loadPost(ForumPostHeader header, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - void unsubscribe(ResultHandler<Boolean> resultHandler); - - void entryRead(ForumEntry forumEntry); - - void entriesRead(Collection<ForumEntry> messageIds); - - void createPost(byte[] body, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - void createPost(byte[] body, MessageId parentId, - ResultExceptionHandler<ForumEntry, DbException> resultHandler); - - interface ForumPostListener extends DestroyableContext { - - @UiThread - void onForumPostReceived(ForumPostHeader header); - @UiThread - void onForumRemoved(); - } +public interface ForumController + extends ThreadListController<Forum, ForumEntry, ForumPostHeader> { } diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java index b6a8ddce527efa65d6ff3585fb59e1716b8e5ef8..442ce23a50a63f86848542ac0149a53fc6cab218 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java +++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java @@ -1,11 +1,10 @@ package org.briarproject.android.forum; -import android.app.Activity; import android.support.annotation.Nullable; -import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.controller.handler.ResultExceptionHandler; -import org.briarproject.android.controller.handler.ResultHandler; +import org.briarproject.android.threaded.ThreadListControllerImpl; import org.briarproject.api.FormatException; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoExecutor; @@ -15,9 +14,7 @@ import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; -import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ForumPostReceivedEvent; -import org.briarproject.api.event.GroupRemovedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPost; @@ -26,19 +23,12 @@ import org.briarproject.api.forum.ForumPostHeader; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.lifecycle.LifecycleManager; -import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import org.briarproject.util.StringUtils; import java.security.GeneralSecurityException; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; import javax.inject.Inject; @@ -47,25 +37,15 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.api.identity.Author.Status.OURSELVES; -public class ForumControllerImpl extends DbControllerImpl - implements ForumController, EventListener { +public class ForumControllerImpl + extends ThreadListControllerImpl<Forum, ForumEntry, ForumPostHeader> + implements ForumController { private static final Logger LOG = Logger.getLogger(ForumControllerImpl.class.getName()); - private final Executor cryptoExecutor; private final ForumPostFactory forumPostFactory; - private final CryptoComponent crypto; private final ForumManager forumManager; - private final EventBus eventBus; - private final IdentityManager identityManager; - - private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>(); - private final AtomicLong newestTimeStamp = new AtomicLong(); - - private volatile LocalAuthor localAuthor = null; - private volatile Forum forum = null; - private volatile ForumPostListener listener; @Inject ForumControllerImpl(@DatabaseExecutor Executor dbExecutor, @@ -73,257 +53,70 @@ public class ForumControllerImpl extends DbControllerImpl @CryptoExecutor Executor cryptoExecutor, ForumPostFactory forumPostFactory, CryptoComponent crypto, ForumManager forumManager, EventBus eventBus, - IdentityManager identityManager) { - super(dbExecutor, lifecycleManager); - this.cryptoExecutor = cryptoExecutor; - this.forumPostFactory = forumPostFactory; - this.crypto = crypto; + IdentityManager identityManager, + AndroidNotificationManager notificationManager) { + super(dbExecutor, lifecycleManager, cryptoExecutor, crypto, eventBus, + identityManager, notificationManager); this.forumManager = forumManager; - this.eventBus = eventBus; - this.identityManager = identityManager; - } - - @Override - public void onActivityCreate(Activity activity) { - if (activity instanceof ForumPostListener) { - listener = (ForumPostListener) activity; - } else { - throw new IllegalStateException( - "An activity that injects the ForumController must " + - "implement the ForumPostListener"); - } + this.forumPostFactory = forumPostFactory; } @Override public void onActivityResume() { - eventBus.addListener(this); - } - - @Override - public void onActivityPause() { - eventBus.removeListener(this); - } - - @Override - public void onActivityDestroy() { + super.onActivityResume(); + notificationManager.clearForumPostNotification(groupId); } @Override public void eventOccurred(Event e) { - if (forum == null) return; + super.eventOccurred(e); + if (e instanceof ForumPostReceivedEvent) { final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e; - if (pe.getGroupId().equals(forum.getId())) { + if (pe.getGroupId().equals(groupId)) { LOG.info("Forum post received, adding..."); final ForumPostHeader fph = pe.getForumPostHeader(); updateNewestTimestamp(fph.getTimestamp()); listener.runOnUiThreadUnlessDestroyed(new Runnable() { @Override public void run() { - listener.onForumPostReceived(fph); - } - }); - } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent s = (GroupRemovedEvent) e; - if (s.getGroup().getId().equals(forum.getId())) { - LOG.info("Forum removed"); - listener.runOnUiThreadUnlessDestroyed(new Runnable() { - @Override - public void run() { - listener.onForumRemoved(); + listener.onHeaderReceived(fph); } }); } } } - /** - * This should only be run from the DbThread. - * - * @throws DbException - */ - private void loadForum(GroupId groupId) throws DbException { - // Get Forum - long now = System.currentTimeMillis(); - forum = forumManager.getForum(groupId); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading forum took " + duration + " ms"); - - // Get First Identity - now = System.currentTimeMillis(); - localAuthor = identityManager.getLocalAuthor(); - duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading author took " + duration + " ms"); + @Override + protected Forum loadGroupItem() throws DbException { + return forumManager.getForum(groupId); } - /** - * This should only be run from the DbThread. - * - * @throws DbException - */ - private Collection<ForumPostHeader> loadHeaders() throws DbException { - if (forum == null) - throw new RuntimeException("Forum has not been initialized"); - - // Get Headers - long now = System.currentTimeMillis(); - Collection<ForumPostHeader> headers = - forumManager.getPostHeaders(forum.getId()); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading headers took " + duration + " ms"); - return headers; + @Override + protected Collection<ForumPostHeader> loadHeaders() throws DbException { + return forumManager.getPostHeaders(groupId); } - /** - * This should only be run from the DbThread. - * - * @throws DbException - */ - private void loadBodies(Collection<ForumPostHeader> headers) + @Override + protected void loadBodies(Collection<ForumPostHeader> headers) throws DbException { - // Get Bodies - long now = System.currentTimeMillis(); for (ForumPostHeader header : headers) { if (!bodyCache.containsKey(header.getId())) { - byte[] body = forumManager.getPostBody(header.getId()); + String body = StringUtils + .fromUtf8(forumManager.getPostBody(header.getId())); bodyCache.put(header.getId(), body); } } - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Loading bodies took " + duration + " ms"); - } - - private List<ForumEntry> buildForumEntries( - Collection<ForumPostHeader> headers) { - List<ForumEntry> entries = new ArrayList<>(); - for (ForumPostHeader h : headers) { - byte[] body = bodyCache.get(h.getId()); - entries.add(new ForumEntry(h, StringUtils.fromUtf8(body))); - } - return entries; - } - - private void updateNewestTimeStamp(Collection<ForumPostHeader> headers) { - for (ForumPostHeader h : headers) { - updateNewestTimestamp(h.getTimestamp()); - } - } - - @Override - public void loadForum(final GroupId groupId, - final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) { - runOnDbThread(new Runnable() { - @Override - public void run() { - LOG.info("Loading forum..."); - try { - if (forum == null) { - loadForum(groupId); - } - // Get Forum Posts and Bodies - Collection<ForumPostHeader> headers = loadHeaders(); - updateNewestTimeStamp(headers); - loadBodies(headers); - resultHandler.onResult(buildForumEntries(headers)); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - resultHandler.onException(e); - } - } - }); - } - - @Override - @Nullable - public Forum getForum() { - return forum; - } - - @Override - public void loadPost(final ForumPostHeader header, - final ResultExceptionHandler<ForumEntry, DbException> resultHandler) { - runOnDbThread(new Runnable() { - @Override - public void run() { - LOG.info("Loading post..."); - try { - loadBodies(Collections.singletonList(header)); - resultHandler.onResult(new ForumEntry(header, StringUtils - .fromUtf8(bodyCache.get(header.getId())))); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - resultHandler.onException(e); - } - } - }); - } - - @Override - public void unsubscribe(final ResultHandler<Boolean> resultHandler) { - if (forum == null) return; - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - long now = System.currentTimeMillis(); - forumManager.removeForum(forum); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Removing forum took " + duration + " ms"); - resultHandler.onResult(true); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - resultHandler.onResult(false); - } - } - }); - } - - @Override - public void entryRead(ForumEntry forumEntry) { - entriesRead(Collections.singletonList(forumEntry)); } @Override - public void entriesRead(final Collection<ForumEntry> forumEntries) { - if (forum == null) return; - runOnDbThread(new Runnable() { - @Override - public void run() { - try { - long now = System.currentTimeMillis(); - for (ForumEntry fe : forumEntries) { - forumManager - .setReadFlag(forum.getId(), fe.getId(), true); - } - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Marking read took " + duration + " ms"); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } - } - }); + protected void markRead(MessageId id) throws DbException { + forumManager.setReadFlag(groupId, id, true); } @Override - public void createPost(byte[] body, - ResultExceptionHandler<ForumEntry, DbException> resultHandler) { - createPost(body, null, resultHandler); - } - - @Override - public void createPost(final byte[] body, final MessageId parentId, - final ResultExceptionHandler<ForumEntry, DbException> resultHandler) { + public void send(final String body, @Nullable final MessageId parentId, + final ResultExceptionHandler<ForumEntry, DbException> handler) { cryptoExecutor.execute(new Runnable() { @Override public void run() { @@ -332,17 +125,24 @@ public class ForumControllerImpl extends DbControllerImpl timestamp = Math.max(timestamp, newestTimeStamp.get()); ForumPost p; try { + LocalAuthor a = identityManager.getLocalAuthor(); KeyParser keyParser = crypto.getSignatureKeyParser(); - byte[] b = localAuthor.getPrivateKey(); - PrivateKey authorKey = keyParser.parsePrivateKey(b); - p = forumPostFactory.createPseudonymousPost( - forum.getId(), timestamp, parentId, localAuthor, - "text/plain", body, authorKey); + byte[] k = a.getPrivateKey(); + PrivateKey authorKey = keyParser.parsePrivateKey(k); + byte[] b = StringUtils.toUtf8(body); + p = forumPostFactory + .createPseudonymousPost(groupId, timestamp, + parentId, a, "text/plain", b, authorKey); } catch (GeneralSecurityException | FormatException e) { throw new RuntimeException(e); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + return; } bodyCache.put(p.getMessage().getId(), body); - storePost(p, resultHandler); + storePost(p, handler); } }); } @@ -366,9 +166,8 @@ public class ForumControllerImpl extends DbControllerImpl p.getMessage().getTimestamp(), p.getAuthor(), OURSELVES, true); - resultHandler.onResult(new ForumEntry(h, StringUtils - .fromUtf8(bodyCache.get(p.getMessage().getId())))); - + resultHandler.onResult(new ForumEntry(h, + bodyCache.get(p.getMessage().getId()))); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -378,11 +177,14 @@ public class ForumControllerImpl extends DbControllerImpl }); } - private void updateNewestTimestamp(long update) { - long newest = newestTimeStamp.get(); - while (newest < update) { - if (newestTimeStamp.compareAndSet(newest, update)) return; - newest = newestTimeStamp.get(); - } + @Override + protected void deleteGroupItem(Forum forum) throws DbException { + forumManager.removeForum(forum); } + + @Override + protected ForumEntry buildItem(ForumPostHeader header) { + return new ForumEntry(header, bodyCache.get(header.getId())); + } + } diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java index ca237eb35e8847002c3aec56dd3894a0cd285911..040b81662b884d6394d67fdcdc8b16ea31ea489d 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java @@ -2,7 +2,6 @@ package org.briarproject.android.forum; import android.content.Context; import android.content.Intent; -import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -21,7 +20,7 @@ import static android.support.v7.util.SortedList.INVALID_POSITION; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static org.briarproject.android.BriarActivity.GROUP_ID; -import static org.briarproject.android.forum.ForumActivity.FORUM_NAME; +import static org.briarproject.android.BriarActivity.GROUP_NAME; class ForumListAdapter extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> { @@ -84,7 +83,7 @@ class ForumListAdapter Intent i = new Intent(ctx, ForumActivity.class); Forum f = item.getForum(); i.putExtra(GROUP_ID, f.getId().getBytes()); - i.putExtra(FORUM_NAME, f.getName()); + i.putExtra(GROUP_NAME, f.getName()); ctx.startActivity(i); } }); @@ -115,17 +114,6 @@ class ForumListAdapter return a.getForum().equals(b.getForum()); } - @Nullable - public ForumListItem findItem(GroupId g) { - for (int i = 0; i < items.size(); i++) { - ForumListItem item = items.get(i); - if (item.getForum().getGroup().getId().equals(g)) { - return item; - } - } - return null; - } - int findItemPosition(GroupId g) { int count = getItemCount(); for (int i = 0; i < count; i++) { diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java index ea18e720f5aac12d79835dd2b03edaeec0c39130..6a747792303a972309c7bb5b45a4b9ea8f93899c 100644 --- a/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListActivity.java @@ -14,36 +14,44 @@ import android.view.View; import org.briarproject.R; import org.briarproject.android.BriarActivity; -import org.briarproject.android.api.AndroidNotificationManager; +import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener; +import org.briarproject.android.threaded.ThreadListController.ThreadListListener; import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.android.view.TextInputView; import org.briarproject.android.view.TextInputView.TextInputListener; +import org.briarproject.api.clients.BaseGroup; +import org.briarproject.api.clients.PostHeader; +import org.briarproject.api.db.DbException; import org.briarproject.api.sync.GroupId; -import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import static android.support.design.widget.Snackbar.make; import static android.view.View.GONE; import static android.view.View.VISIBLE; -public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>> +public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>> extends BriarActivity - implements TextInputListener, ThreadItemListener<I> { + implements ThreadListListener<H>, TextInputListener, + ThreadItemListener<I> { protected static final String KEY_INPUT_VISIBILITY = "inputVisibility"; protected static final String KEY_REPLY_ID = "replyId"; - @Inject - protected AndroidNotificationManager notificationManager; - protected A adapter; protected BriarRecyclerView list; protected TextInputView textInput; protected GroupId groupId; + private byte[] replyId; + + protected abstract ThreadListController<G, I, H> getController(); @CallSuper @Override + @SuppressWarnings("ConstantConditions") public void onCreate(final Bundle state) { super.onCreate(state); @@ -53,6 +61,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI byte[] b = i.getByteArrayExtra(GROUP_ID); if (b == null) throw new IllegalStateException("No GroupId in intent."); groupId = new GroupId(b); + getController().setGroupId(groupId); + String groupName = i.getStringExtra(GROUP_NAME); + if (groupName != null) setTitle(groupName); + else loadAndSetTitle(); textInput = (TextInputView) findViewById(R.id.text_input_container); textInput.setVisibility(GONE); @@ -62,17 +74,64 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI list.setLayoutManager(linearLayoutManager); adapter = createAdapter(linearLayoutManager); list.setAdapter(adapter); + + if (state != null) { + replyId = state.getByteArray(KEY_REPLY_ID); + } + + loadItems(); } protected abstract @LayoutRes int getLayout(); protected abstract A createAdapter(LinearLayoutManager layoutManager); + private void loadAndSetTitle() { + getController().loadGroupItem( + new UiResultExceptionHandler<G, DbException>(this) { + @Override + public void onResultUi(G forum) { + setTitle(forum.getName()); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO Proper error handling + finish(); + } + }); + } + + private void loadItems() { + getController().loadItems( + new UiResultExceptionHandler<Collection<I>, DbException>( + this) { + @Override + public void onResultUi(Collection<I> result) { + // FIXME What's the benefit of copying the collection? + List<I> items = new ArrayList<>(result); + if (items.isEmpty()) { + list.showData(); + } else { + adapter.setItems(items); + list.showData(); + if (replyId != null) + adapter.setReplyItemById(replyId); + } + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO Proper error handling + finish(); + } + }); + } + @CallSuper @Override public void onResume() { super.onResume(); - notificationManager.blockNotification(groupId); list.startPeriodicUpdate(); } @@ -80,7 +139,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI @Override public void onPause() { super.onPause(); - notificationManager.unblockNotification(groupId); list.stopPeriodicUpdate(); } @@ -130,12 +188,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI public void onItemVisible(I item) { if (!item.isRead()) { item.setRead(true); - markItemRead(item); + getController().markItemRead(item); } } - protected abstract void markItemRead(I item); - @Override public void onReplyClick(I item) { showTextInput(item); @@ -167,13 +223,50 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI if (text.trim().length() == 0) return; I replyItem = adapter.getReplyItem(); - sendItem(text, replyItem); + UiResultExceptionHandler<I, DbException> handler = + new UiResultExceptionHandler<I, DbException>(this) { + @Override + public void onResultUi(I result) { + addItem(result, true); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO add proper exception handling + finish(); + } + }; + if (replyItem == null) { + // root post + getController().send(text, handler); + } else { + getController().send(text, replyItem.getId(), handler); + } textInput.hideSoftKeyboard(); textInput.setVisibility(GONE); adapter.setReplyItem(null); } - protected abstract void sendItem(String text, I replyToItem); + @Override + public void onHeaderReceived(H header) { + getController().loadItem(header, + new UiResultExceptionHandler<I, DbException>(this) { + @Override + public void onResultUi(final I result) { + addItem(result, false); + } + + @Override + public void onExceptionUi(DbException exception) { + // TODO add proper exception handling + } + }); + } + + @Override + public void onGroupRemoved() { + supportFinishAfterTransition(); + } protected void addItem(final I item, boolean isLocal) { adapter.add(item); diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListController.java b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java new file mode 100644 index 0000000000000000000000000000000000000000..ec876f3602c5e72375612d3a0e0983cbab7e2969 --- /dev/null +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListController.java @@ -0,0 +1,48 @@ +package org.briarproject.android.threaded; + +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import org.briarproject.android.DestroyableContext; +import org.briarproject.android.controller.ActivityLifecycleController; +import org.briarproject.android.controller.handler.ResultExceptionHandler; +import org.briarproject.api.clients.BaseGroup; +import org.briarproject.api.clients.PostHeader; +import org.briarproject.api.db.DbException; +import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.Collection; + +public interface ThreadListController<G extends BaseGroup, I extends ThreadItem, H extends PostHeader> + extends ActivityLifecycleController { + + void setGroupId(GroupId groupId); + + void loadGroupItem(ResultExceptionHandler<G, DbException> handler); + + void loadItem(H header, ResultExceptionHandler<I, DbException> handler); + + void loadItems(ResultExceptionHandler<Collection<I>, DbException> handler); + + void markItemRead(I item); + + void markItemsRead(Collection<I> items); + + void send(String body, ResultExceptionHandler<I, DbException> handler); + + void send(String body, @Nullable MessageId parentId, + ResultExceptionHandler<I, DbException> handler); + + void deleteGroupItem(ResultExceptionHandler<Void, DbException> handler); + + interface ThreadListListener<H> extends DestroyableContext { + @UiThread + void onHeaderReceived(H header); + + @UiThread + void onGroupRemoved(); + } + +} diff --git a/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e60f310c4525e204dce212563b157cb0a7f24ccb --- /dev/null +++ b/briar-android/src/org/briarproject/android/threaded/ThreadListControllerImpl.java @@ -0,0 +1,327 @@ +package org.briarproject.android.threaded; + +import android.app.Activity; +import android.support.annotation.CallSuper; + +import org.briarproject.android.api.AndroidNotificationManager; +import org.briarproject.android.controller.DbControllerImpl; +import org.briarproject.android.controller.handler.ResultExceptionHandler; +import org.briarproject.api.clients.BaseGroup; +import org.briarproject.api.clients.PostHeader; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.CryptoExecutor; +import org.briarproject.api.db.DatabaseExecutor; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.GroupRemovedEvent; +import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumPostFactory; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends ThreadItem, H extends PostHeader> + extends DbControllerImpl + implements ThreadListController<G, I, H>, EventListener { + + private static final Logger LOG = + Logger.getLogger(ThreadListControllerImpl.class.getName()); + + protected final Executor cryptoExecutor; + protected final CryptoComponent crypto; + protected final EventBus eventBus; + protected final IdentityManager identityManager; + protected final AndroidNotificationManager notificationManager; + + protected final Map<MessageId, String> bodyCache = + new ConcurrentHashMap<>(); + protected final AtomicLong newestTimeStamp = new AtomicLong(); + + protected volatile GroupId groupId; + + protected ThreadListListener<H> listener; + + protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + @CryptoExecutor Executor cryptoExecutor, CryptoComponent crypto, + EventBus eventBus, IdentityManager identityManager, + AndroidNotificationManager notificationManager) { + super(dbExecutor, lifecycleManager); + this.cryptoExecutor = cryptoExecutor; + this.crypto = crypto; + this.eventBus = eventBus; + this.identityManager = identityManager; + this.notificationManager = notificationManager; + } + + @Override + public void setGroupId(GroupId groupId) { + this.groupId = groupId; + } + + @CallSuper + @SuppressWarnings("unchecked") + @Override + public void onActivityCreate(Activity activity) { + listener = (ThreadListListener<H>) activity; + } + + @CallSuper + @Override + public void onActivityResume() { + checkGroupId(); + notificationManager.blockNotification(groupId); + eventBus.addListener(this); + } + + @CallSuper + @Override + public void onActivityPause() { + notificationManager.unblockNotification(groupId); + eventBus.removeListener(this); + } + + @Override + public void onActivityDestroy() { + } + + @CallSuper + @Override + public void eventOccurred(Event e) { + if (e instanceof GroupRemovedEvent) { + GroupRemovedEvent s = (GroupRemovedEvent) e; + if (s.getGroup().getId().equals(groupId)) { + LOG.info("Group removed"); + listener.runOnUiThreadUnlessDestroyed(new Runnable() { + @Override + public void run() { + listener.onGroupRemoved(); + } + }); + } + } + } + + @Override + public void loadGroupItem( + final ResultExceptionHandler<G, DbException> handler) { + checkGroupId(); + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + G groupItem = loadGroupItem(); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading forum took " + duration + " ms"); + handler.onResult(groupItem); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + /** + * This should only be run from the DbThread. + * + * @throws DbException + */ + protected abstract G loadGroupItem() throws DbException; + + @Override + public void loadItems( + final ResultExceptionHandler<Collection<I>, DbException> handler) { + checkGroupId(); + runOnDbThread(new Runnable() { + @Override + public void run() { + LOG.info("Loading items..."); + try { + // Load headers + long now = System.currentTimeMillis(); + Collection<H> headers = loadHeaders(); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading headers took " + duration + " ms"); + + // Update timestamp of newest item + updateNewestTimeStamp(headers); + + // Load bodies + now = System.currentTimeMillis(); + loadBodies(headers); + duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading bodies took " + duration + " ms"); + + // Build and hand over items + handler.onResult(buildItems(headers)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + /** + * This should only be run from the DbThread. + * + * @throws DbException + */ + protected abstract Collection<H> loadHeaders() throws DbException; + + /** + * This should only be run from the DbThread. + * + * @throws DbException + */ + protected abstract void loadBodies(Collection<H> headers) + throws DbException; + + @Override + public void loadItem(final H header, + final ResultExceptionHandler<I, DbException> handler) { + runOnDbThread(new Runnable() { + @Override + public void run() { + LOG.info("Loading item..."); + try { + loadBodies(Collections.singletonList(header)); + I item = buildItem(header); + handler.onResult(item); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + @Override + public void markItemRead(I item) { + markItemsRead(Collections.singletonList(item)); + } + + @Override + public void markItemsRead(final Collection<I> items) { + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + for (I i : items) { + markRead(i.getId()); + } + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Marking read took " + duration + " ms"); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + /** + * This should only be run from the DbThread. + * + * @throws DbException + */ + protected abstract void markRead(MessageId id) throws DbException; + + @Override + public void send(String body, + ResultExceptionHandler<I, DbException> resultHandler) { + send(body, null, resultHandler); + } + + @Override + public void deleteGroupItem( + final ResultExceptionHandler<Void, DbException> handler) { + runOnDbThread(new Runnable() { + @Override + public void run() { + try { + long now = System.currentTimeMillis(); + G groupItem = loadGroupItem(); + deleteGroupItem(groupItem); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Removing group took " + duration + " ms"); + //noinspection ConstantConditions + handler.onResult(null); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + handler.onException(e); + } + } + }); + } + + /** + * This should only be run from the DbThread. + * + * @throws DbException + */ + protected abstract void deleteGroupItem(G groupItem) throws DbException; + + private List<I> buildItems(Collection<H> headers) { + List<I> entries = new ArrayList<>(); + for (H h : headers) { + entries.add(buildItem(h)); + } + return entries; + } + + /** + * When building the item, the body can be assumed to be cached + */ + protected abstract I buildItem(H header); + + private void updateNewestTimeStamp(Collection<H> headers) { + for (H h : headers) { + updateNewestTimestamp(h.getTimestamp()); + } + } + + protected void updateNewestTimestamp(long update) { + long newest = newestTimeStamp.get(); + while (newest < update) { + if (newestTimeStamp.compareAndSet(newest, update)) return; + newest = newestTimeStamp.get(); + } + } + + private void checkGroupId() { + if (groupId == null) { + throw new IllegalStateException( + "You must set the GroupId before the controller is started."); + } + } + +} diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java index 7842c11e2a424f42b27ded87da284bb1ff773f1d..4a7fcf02e0b766b20d601c8f4f1478ea5361f238 100644 --- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java +++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java @@ -26,6 +26,7 @@ import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import static junit.framework.Assert.assertEquals; @@ -81,7 +82,7 @@ public class ForumActivityTest { private TestForumActivity forumActivity; @Captor - private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>> + private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumEntry>, DbException>> rc; @Before @@ -112,8 +113,7 @@ public class ForumActivityTest { public void testNestedEntries() { ForumController mc = forumActivity.getController(); List<ForumEntry> dummyData = getDummyData(); - verify(mc, times(1)) - .loadForum(Mockito.any(GroupId.class), rc.capture()); + verify(mc, times(1)).loadItems(rc.capture()); rc.getValue().onResult(dummyData); NestedForumAdapter adapter = forumActivity.getAdapter(); Assert.assertNotNull(adapter);