diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 025e452b2..43231803d 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -129,16 +129,6 @@
/>
-
-
-
-
-
-
-
-
+
+
diff --git a/briar-android/res/drawable/chevron48dp_up.xml b/briar-android/res/drawable/chevron48dp_up.xml
new file mode 100644
index 000000000..7d59523a9
--- /dev/null
+++ b/briar-android/res/drawable/chevron48dp_up.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/briar-android/res/drawable/level_indicator_circle.xml b/briar-android/res/drawable/level_indicator_circle.xml
new file mode 100644
index 000000000..22fc407d3
--- /dev/null
+++ b/briar-android/res/drawable/level_indicator_circle.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/drawable/selector_chevron.xml b/briar-android/res/drawable/selector_chevron.xml
new file mode 100644
index 000000000..bc08694ba
--- /dev/null
+++ b/briar-android/res/drawable/selector_chevron.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml
new file mode 100644
index 000000000..946ce34e8
--- /dev/null
+++ b/briar-android/res/layout/activity_forum.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/forum_discussion_cell.xml b/briar-android/res/layout/forum_discussion_cell.xml
new file mode 100644
index 000000000..57383a720
--- /dev/null
+++ b/briar-android/res/layout/forum_discussion_cell.xml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/layout/text_input_field.xml b/briar-android/res/layout/text_input_field.xml
new file mode 100644
index 000000000..c59e93c7e
--- /dev/null
+++ b/briar-android/res/layout/text_input_field.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/briar-android/res/values/attrs.xml b/briar-android/res/values/attrs.xml
new file mode 100644
index 000000000..380379e9e
--- /dev/null
+++ b/briar-android/res/values/attrs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index 3fb503978..14a0938bb 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -43,4 +43,6 @@
#61000000
@color/briar_blue_dark
+ #cfd2d4
+ #ffffff
\ No newline at end of file
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index 5ca081c56..942a2d5bf 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -41,5 +41,8 @@
14dp
51dp
15dp
+ 2dp
+ 24dp
+ 20dp
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 0e01dde61..1e1e8e28c 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -198,6 +198,17 @@
Introduced contact was added
You have been introduced to %1$s.
+
+ REPLY
+
+ - %1$d reply
+ - %1$d replies
+
+ Forum entry posted
+ New forum entry
+ New Entry
+ New Reply
+
Lost Password
Password recovery is not possible. Do you want to delete your account?\n\nCaution: This will permanently delete your identities, contacts and messages
@@ -225,4 +236,6 @@
Signing out of Briar..
Please wait..
+
+
diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml
index abb3e2457..38f609e83 100644
--- a/briar-android/res/values/styles.xml
+++ b/briar-android/res/values/styles.xml
@@ -140,4 +140,9 @@
- 16dp
+
+
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index b777e38a4..f28335cd8 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -8,10 +8,8 @@ import org.briarproject.android.forum.ContactSelectorFragment;
import org.briarproject.android.forum.CreateForumActivity;
import org.briarproject.android.forum.ForumActivity;
import org.briarproject.android.forum.ForumSharingStatusActivity;
-import org.briarproject.android.forum.ReadForumPostActivity;
import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.ShareForumMessageFragment;
-import org.briarproject.android.forum.WriteForumPostActivity;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.IntroductionActivity;
@@ -54,16 +52,12 @@ public interface ActivityComponent {
void inject(AvailableForumsActivity activity);
- void inject(WriteForumPostActivity activity);
-
void inject(CreateForumActivity activity);
void inject(ShareForumActivity activity);
void inject(ForumSharingStatusActivity activity);
- void inject(ReadForumPostActivity activity);
-
void inject(ForumActivity activity);
void inject(SettingsActivity activity);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index 0e13fbd0d..6766f3802 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -12,6 +12,8 @@ import org.briarproject.android.controller.ConfigController;
import org.briarproject.android.controller.ConfigControllerImpl;
import org.briarproject.android.controller.DbController;
import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumControllerImpl;
import org.briarproject.android.controller.NavDrawerController;
import org.briarproject.android.controller.NavDrawerControllerImpl;
import org.briarproject.android.controller.PasswordController;
@@ -21,6 +23,7 @@ import org.briarproject.android.controller.SetupControllerImpl;
import org.briarproject.android.controller.TransportStateListener;
import org.briarproject.android.forum.ContactSelectorFragment;
import org.briarproject.android.forum.ForumListFragment;
+import org.briarproject.android.forum.ForumTestControllerImpl;
import org.briarproject.android.forum.ShareForumMessageFragment;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.introduction.ContactChooserFragment;
@@ -98,6 +101,22 @@ public class ActivityModule {
return dbController;
}
+ @ActivityScope
+ @Provides
+ protected ForumController provideForumController(
+ ForumControllerImpl forumController) {
+ activity.addLifecycleController(forumController);
+ return forumController;
+ }
+
+ @Named("ForumTestController")
+ @ActivityScope
+ @Provides
+ protected ForumController provideForumTestController(
+ ForumTestControllerImpl forumController) {
+ return forumController;
+ }
+
@ActivityScope
@Provides
protected NavDrawerController provideNavDrawerController(
diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java
index 68d37071a..ed2ef2724 100644
--- a/briar-android/src/org/briarproject/android/AppModule.java
+++ b/briar-android/src/org/briarproject/android/AppModule.java
@@ -4,6 +4,7 @@ import android.app.Application;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.forum.ForumPersistentData;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.PublicKey;
import org.briarproject.api.crypto.SecretKey;
@@ -136,4 +137,10 @@ public class AppModule {
eventBus.addListener(notificationManager);
return notificationManager;
}
+
+ @Provides
+ @Singleton
+ ForumPersistentData provideForumPersistence(ForumPersistentData fpd) {
+ return fpd;
+ }
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index cfa26e8c8..b99cb9f47 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -3,18 +3,23 @@ package org.briarproject.android.forum;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.LinearLayout;
-import android.widget.ListView;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -22,76 +27,61 @@ import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
-import org.briarproject.android.util.ListLoadingProgressBar;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchGroupException;
-import org.briarproject.api.db.NoSuchMessageException;
-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.event.MessageStateChangedEvent;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumPostHeader;
-import org.briarproject.api.identity.Author;
+import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.util.CustomAnimations;
import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.logging.Logger;
import javax.inject.Inject;
+import im.delight.android.identicons.IdenticonDrawable;
+
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
-import static android.support.design.widget.Snackbar.LENGTH_LONG;
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
-import static android.widget.LinearLayout.VERTICAL;
import static android.widget.Toast.LENGTH_SHORT;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
-import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
-public class ForumActivity extends BriarActivity implements EventListener,
- OnItemClickListener {
+public class ForumActivity extends BriarActivity implements
+ ForumController.ForumPostListener {
- public static final String FORUM_NAME = "briar.FORUM_NAME";
- public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
-
- private static final int REQUEST_READ = 2;
- private static final int REQUEST_FORUM_SHARED = 3;
private static final Logger LOG =
Logger.getLogger(ForumActivity.class.getName());
- @Inject protected AndroidNotificationManager notificationManager;
- private Map bodyCache = new HashMap<>();
- private TextView empty = null;
- private ForumAdapter adapter = null;
- private ListView list = null;
- private ListLoadingProgressBar loading = null;
+ public static final String FORUM_NAME = "briar.FORUM_NAME";
+ public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
+ private static final int REQUEST_FORUM_SHARED = 3;
+
+ @Inject
+ protected AndroidNotificationManager notificationManager;
+
+ // uncomment the next line for a test component with dummy data
+// @Named("ForumTestController")
+ @Inject
+ protected ForumController forumController;
+
+ private BriarRecyclerView recyclerView;
+ private EditText textInput;
+ private ViewGroup inputContainer;
+ private LinearLayoutManager linearLayoutManager;
- // Fields that are accessed from background threads must be volatile
- @Inject protected volatile ForumManager forumManager;
- @Inject protected volatile EventBus eventBus;
private volatile GroupId groupId = null;
- private volatile Forum forum = null;
+
+ protected ForumAdapter forumAdapter;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
+ setContentView(R.layout.activity_forum);
+
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException();
@@ -99,32 +89,30 @@ public class ForumActivity extends BriarActivity implements EventListener,
String forumName = i.getStringExtra(FORUM_NAME);
if (forumName != null) setTitle(forumName);
- LinearLayout layout = new LinearLayout(this);
- layout.setLayoutParams(MATCH_MATCH);
- layout.setOrientation(VERTICAL);
- layout.setGravity(CENTER_HORIZONTAL);
-
- empty = new TextView(this);
- empty.setLayoutParams(MATCH_WRAP_1);
- empty.setGravity(CENTER);
- empty.setTextSize(18);
- empty.setText(R.string.no_forum_posts);
- empty.setVisibility(GONE);
- layout.addView(empty);
-
- adapter = new ForumAdapter(this);
- list = new ListView(this);
- list.setLayoutParams(MATCH_WRAP_1);
- list.setAdapter(adapter);
- list.setOnItemClickListener(this);
- list.setVisibility(GONE);
- layout.addView(list);
-
- // Show a progress bar while the list is loading
- loading = new ListLoadingProgressBar(this);
- layout.addView(loading);
-
- setContentView(layout);
+ inputContainer = (ViewGroup) findViewById(R.id.text_input_container);
+ inputContainer.setVisibility(GONE);
+ textInput = (EditText) findViewById(R.id.input_text);
+ recyclerView =
+ (BriarRecyclerView) findViewById(R.id.forum_discussion_list);
+ linearLayoutManager = new LinearLayoutManager(this);
+ recyclerView.setLayoutManager(linearLayoutManager);
+ recyclerView.showProgressBar();
+ forumController
+ .loadForum(groupId, new UiResultHandler(this) {
+ @Override
+ public void onResultUi(Boolean result) {
+ if (result) {
+ setTitle(forumController.getForumName());
+ forumAdapter = new ForumAdapter(
+ forumController.getForumEntries());
+ recyclerView.setAdapter(forumAdapter);
+ recyclerView.showData();
+ } else {
+ // TODO Maybe an error dialog ?
+ finish();
+ }
+ }
+ });
}
@Override
@@ -132,14 +120,20 @@ public class ForumActivity extends BriarActivity implements EventListener,
component.inject(this);
}
+ private void displaySnackbar(int stringId) {
+ Snackbar snackbar =
+ Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
+ snackbar.getView().setBackgroundResource(R.color.briar_primary);
+ snackbar.show();
+ }
+
@Override
- public void onResume() {
- super.onResume();
- eventBus.addListener(this);
- notificationManager.blockNotification(groupId);
- notificationManager.clearForumPostNotification(groupId);
- loadForum();
- loadHeaders();
+ protected void onActivityResult(int request, int result, Intent data) {
+ super.onActivityResult(request, result, data);
+
+ if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
+ displaySnackbar(R.string.forum_shared_snackbar);
+ }
}
@Override
@@ -151,6 +145,27 @@ public class ForumActivity extends BriarActivity implements EventListener,
return super.onCreateOptionsMenu(menu);
}
+ @Override
+ public void onBackPressed() {
+ if (inputContainer.getVisibility() == VISIBLE) {
+ inputContainer.setVisibility(GONE);
+ forumAdapter.setReplyEntry(null);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void showTextInput(boolean isNewMessage) {
+ // An animation here would be an overkill because of the keyboard
+ // popping up.
+ inputContainer.setVisibility(View.VISIBLE);
+ textInput.setText("");
+ textInput.requestFocus();
+ textInput.setHint(isNewMessage ? R.string.forum_new_message_hint :
+ R.string.forum_message_reply_hint);
+ showSoftKeyboard(textInput);
+ }
+
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
ActivityOptionsCompat options = ActivityOptionsCompat
@@ -159,11 +174,9 @@ public class ForumActivity extends BriarActivity implements EventListener,
// Handle presses on the action bar items
switch (item.getItemId()) {
case R.id.action_forum_compose_post:
- Intent i = new Intent(this, WriteForumPostActivity.class);
- i.putExtra(GROUP_ID, groupId.getBytes());
- i.putExtra(FORUM_NAME, forum.getName());
- i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
- startActivity(i);
+ if (inputContainer.getVisibility() != VISIBLE) {
+ showTextInput(true);
+ }
return true;
case R.id.action_forum_share:
Intent i2 = new Intent(this, ShareForumActivity.class);
@@ -187,225 +200,34 @@ public class ForumActivity extends BriarActivity implements EventListener,
}
}
- private void loadForum() {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- forum = forumManager.getForum(groupId);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading forum " + duration + " ms");
- displayForumName();
- } catch (NoSuchGroupException e) {
- finishOnUiThread();
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- private void displayForumName() {
- runOnUiThread(new Runnable() {
- public void run() {
- setTitle(forum.getName());
- }
- });
- }
-
- private void loadHeaders() {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- Collection headers =
- forumManager.getPostHeaders(groupId);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Load took " + duration + " ms");
- displayHeaders(headers);
- } catch (NoSuchGroupException e) {
- finishOnUiThread();
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- private void displayHeaders(final Collection headers) {
- runOnUiThread(new Runnable() {
- public void run() {
- loading.setVisibility(GONE);
- adapter.clear();
- if (headers.isEmpty()) {
- empty.setVisibility(VISIBLE);
- list.setVisibility(GONE);
- } else {
- empty.setVisibility(GONE);
- list.setVisibility(VISIBLE);
- for (ForumPostHeader h : headers) {
- ForumItem item = new ForumItem(h);
- byte[] body = bodyCache.get(h.getId());
- if (body == null) loadPostBody(h);
- else item.setBody(body);
- adapter.add(item);
- }
- adapter.sort(ForumItemComparator.INSTANCE);
- // Scroll to the bottom
- list.setSelection(adapter.getCount() - 1);
- }
- }
- });
- }
-
- private void loadPostBody(final ForumPostHeader h) {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- byte[] body = forumManager.getPostBody(h.getId());
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading message took " + duration + " ms");
- displayPost(h.getId(), body);
- } catch (NoSuchMessageException e) {
- // The item will be removed when we get the event
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- private void displayPost(final MessageId m, final byte[] body) {
- runOnUiThread(new Runnable() {
- public void run() {
- bodyCache.put(m, body);
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- ForumItem item = adapter.getItem(i);
- if (item.getHeader().getId().equals(m)) {
- item.setBody(body);
- adapter.notifyDataSetChanged();
- // Scroll to the bottom
- list.setSelection(count - 1);
- return;
- }
- }
- }
- });
- }
-
@Override
- protected void onActivityResult(int request, int result, Intent data) {
- super.onActivityResult(request, result, data);
- if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
- int position = data.getIntExtra("briar.POSITION", -1);
- if (position >= 0 && position < adapter.getCount())
- displayPost(position);
- }
- else if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
- Snackbar s = Snackbar.make(list, R.string.forum_shared_snackbar,
- LENGTH_LONG);
- s.getView().setBackgroundResource(R.color.briar_primary);
- s.show();
- }
+ public void onResume() {
+ super.onResume();
+ notificationManager.blockNotification(groupId);
+ notificationManager.clearForumPostNotification(groupId);
}
@Override
public void onPause() {
super.onPause();
- eventBus.removeListener(this);
notificationManager.unblockNotification(groupId);
- if (isFinishing()) markPostsRead();
}
- private void markPostsRead() {
- List unread = new ArrayList<>();
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- ForumPostHeader h = adapter.getItem(i).getHeader();
- if (!h.isRead()) unread.add(h.getId());
+ public void sendMessage(View view) {
+ String text = textInput.getText().toString();
+ if (text.trim().length() == 0)
+ return;
+ ForumEntry replyEntry = forumAdapter.getReplyEntry();
+ if (replyEntry == null) {
+ // root post
+ forumController.createPost(StringUtils.toUtf8(text));
+ } else {
+ forumController.createPost(StringUtils.toUtf8(text),
+ replyEntry.getMessageId());
}
- if (unread.isEmpty()) return;
- if (LOG.isLoggable(INFO))
- LOG.info("Marking " + unread.size() + " posts read");
- markPostsRead(Collections.unmodifiableList(unread));
- }
-
- private void markPostsRead(final Collection unread) {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- for (MessageId m : unread)
- forumManager.setReadFlag(m, 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);
- }
- }
- });
- }
-
- public void eventOccurred(Event e) {
- if (e instanceof MessageStateChangedEvent) {
- MessageStateChangedEvent m = (MessageStateChangedEvent) e;
- if (m.getState() == DELIVERED &&
- m.getMessage().getGroupId().equals(groupId)) {
- LOG.info("Message added, reloading");
- loadHeaders();
- }
- } else if (e instanceof GroupRemovedEvent) {
- GroupRemovedEvent s = (GroupRemovedEvent) e;
- if (s.getGroup().getId().equals(groupId)) {
- LOG.info("Forum removed");
- finishOnUiThread();
- }
- }
- }
-
- private long getMinTimestampForNewPost() {
- // Don't use an earlier timestamp than the newest post
- long timestamp = 0;
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- long t = adapter.getItem(i).getHeader().getTimestamp();
- if (t > timestamp) timestamp = t;
- }
- return timestamp + 1;
- }
-
- public void onItemClick(AdapterView> parent, View view, int position,
- long id) {
- displayPost(position);
- }
-
- private void displayPost(int position) {
- ForumPostHeader header = adapter.getItem(position).getHeader();
- Intent i = new Intent(this, ReadForumPostActivity.class);
- i.putExtra(GROUP_ID, groupId.getBytes());
- i.putExtra(FORUM_NAME, forum.getName());
- i.putExtra("briar.MESSAGE_ID", header.getId().getBytes());
- Author author = header.getAuthor();
- if (author != null) {
- i.putExtra("briar.AUTHOR_NAME", author.getName());
- i.putExtra("briar.AUTHOR_ID", author.getId().getBytes());
- }
- i.putExtra("briar.AUTHOR_STATUS", header.getAuthorStatus().name());
- i.putExtra("briar.CONTENT_TYPE", header.getContentType());
- i.putExtra("briar.TIMESTAMP", header.getTimestamp());
- i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
- i.putExtra("briar.POSITION", position);
- startActivityForResult(i, REQUEST_READ);
+ hideSoftKeyboard(textInput);
+ inputContainer.setVisibility(GONE);
+ forumAdapter.setReplyEntry(null);
}
private void showUnsubscribeDialog() {
@@ -413,10 +235,19 @@ public class ForumActivity extends BriarActivity implements EventListener,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
- unsubscribe(forum);
- Toast.makeText(ForumActivity.this,
- R.string.forum_left_toast, LENGTH_SHORT)
- .show();
+ forumController.unsubscribe(
+ new UiResultHandler(
+ ForumActivity.this) {
+ @Override
+ public void onResultUi(Boolean result) {
+ if (result) {
+ Toast.makeText(ForumActivity.this,
+ R.string.forum_left_toast,
+ LENGTH_SHORT)
+ .show();
+ }
+ }
+ });
}
};
AlertDialog.Builder builder =
@@ -429,21 +260,346 @@ public class ForumActivity extends BriarActivity implements EventListener,
builder.show();
}
- private void unsubscribe(final Forum f) {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- forumManager.removeForum(f);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Removing forum took " + duration + " ms");
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
+ @Override
+ public void addLocalEntry(int index, ForumEntry entry) {
+ forumAdapter.addEntry(index, entry, true);
+ displaySnackbar(R.string.forum_new_entry_posted);
}
+ @Override
+ public void addForeignEntry(int index, ForumEntry entry) {
+ forumAdapter.addEntry(index, entry, false);
+ displaySnackbar(R.string.forum_new_entry_received);
+ }
+
+ static class ForumViewHolder extends RecyclerView.ViewHolder {
+
+ public final TextView textView, lvlText, dateText, repliesText;
+ public final View[] lvls;
+ public final ImageView avatar;
+ public final View chevron, replyButton;
+ public final ViewGroup cell;
+ public final View bottomDivider;
+
+ public ForumViewHolder(View v) {
+ super(v);
+
+ textView = (TextView) v.findViewById(R.id.text);
+ lvlText = (TextView) v.findViewById(R.id.nested_line_text);
+ dateText = (TextView) v.findViewById(R.id.date);
+ repliesText = (TextView) v.findViewById(R.id.replies);
+ int[] nestedLineIds = {
+ R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
+ R.id.nested_line_4, R.id.nested_line_5
+ };
+ lvls = new View[nestedLineIds.length];
+ for (int i = 0; i < lvls.length; i++) {
+ lvls[i] = v.findViewById(nestedLineIds[i]);
+ }
+ avatar = (ImageView) v.findViewById(R.id.avatar);
+ chevron = v.findViewById(R.id.chevron);
+ replyButton = v.findViewById(R.id.btn_reply);
+ cell = (ViewGroup) v.findViewById(R.id.forum_cell);
+ bottomDivider = v.findViewById(R.id.bottom_divider);
+ }
+ }
+
+ public class ForumAdapter extends RecyclerView.Adapter {
+
+ private final List forumEntries;
+ // highlight not depandant on time
+ private ForumEntry replyEntry;
+// temporary highlight
+ private ForumEntry addedEntry;
+
+ public ForumAdapter(@NonNull List forumEntries) {
+ this.forumEntries = forumEntries;
+ }
+
+ private ForumEntry getReplyEntry() {
+ return replyEntry;
+ }
+
+ public void addEntry(int index, ForumEntry entry,
+ boolean isScrolling) {
+ forumEntries.add(index, entry);
+ boolean isShowingDescendants = false;
+ if (entry.getLevel() > 0) {
+ // update parent and make sure descendants are visible
+ // Note that the parent's visibility is guaranteed (otherwise
+ // the reply button would not be visible)
+ for (int i = index - 1; i >= 0; i--) {
+ ForumEntry higherEntry = forumEntries.get(i);
+ if (higherEntry.getLevel() < entry.getLevel()) {
+ // parent found
+ if (!higherEntry.isShowingDescendants()) {
+ isShowingDescendants = true;
+ showDescendants(higherEntry);
+ }
+ break;
+ }
+ }
+ }
+ if (!isShowingDescendants) {
+ int visiblePos = getVisiblePos(entry);
+ notifyItemInserted(visiblePos);
+ if (isScrolling)
+ linearLayoutManager
+ .scrollToPositionWithOffset(visiblePos, 0);
+ }
+ addedEntry = entry;
+ }
+
+ private boolean hasDescendants(ForumEntry forumEntry) {
+ int i = forumEntries.indexOf(forumEntry);
+ if (i >= 0 && i < forumEntries.size() - 1) {
+ if (forumEntries.get(i + 1).getLevel() >
+ forumEntry.getLevel()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasVisibleDescendants(int visiblePos) {
+ int levelLimit = forumEntries.get(visiblePos).getLevel();
+ for (int i = visiblePos + 1; i < getItemCount(); i++) {
+ ForumEntry entry = getVisibleEntry(i);
+ if (entry.getLevel() <= levelLimit)
+ break;
+ return true;
+ }
+ return false;
+ }
+
+ private int getReplyCount(ForumEntry entry) {
+ int counter = 0;
+ int pos = forumEntries.indexOf(entry);
+ if (pos >= 0) {
+ int ancestorLvl = forumEntries.get(pos).getLevel();
+ for (int i = pos + 1; i < forumEntries.size(); i++) {
+ int descendantLvl = forumEntries.get(i).getLevel();
+ if (descendantLvl <= ancestorLvl)
+ break;
+ if (descendantLvl == ancestorLvl + 1)
+ counter++;
+ }
+ }
+ return counter;
+ }
+
+ public void setReplyEntry(ForumEntry entry) {
+ if (replyEntry != null) {
+ notifyItemChanged(getVisiblePos(replyEntry));
+ }
+ replyEntry = entry;
+ if (replyEntry != null) {
+ notifyItemChanged(getVisiblePos(replyEntry));
+ }
+ }
+
+ private List getSubTreeIndexes(int pos, int levelLimit) {
+ List indexList = new ArrayList<>();
+
+ for (int i = pos + 1; i < getItemCount(); i++) {
+ ForumEntry entry = getVisibleEntry(i);
+ if (entry.getLevel() > levelLimit) {
+ indexList.add(i);
+ } else {
+ break;
+ }
+ }
+ return indexList;
+ }
+
+ public void showDescendants(ForumEntry forumEntry) {
+ forumEntry.setShowingDescendants(true);
+ int visiblePos = getVisiblePos(forumEntry);
+ List indexList =
+ getSubTreeIndexes(visiblePos, forumEntry.getLevel());
+ if (!indexList.isEmpty()) {
+ if (indexList.size() == 1) {
+ notifyItemInserted(indexList.get(0));
+ } else {
+ notifyItemRangeInserted(indexList.get(0),
+ indexList.size());
+ }
+ }
+ }
+
+ public void hideDescendants(ForumEntry forumEntry) {
+ int visiblePos = getVisiblePos(forumEntry);
+ List indexList =
+ getSubTreeIndexes(visiblePos, forumEntry.getLevel());
+ if (!indexList.isEmpty()) {
+ if (indexList.size() == 1) {
+ notifyItemRemoved(indexList.get(0));
+ } else {
+ notifyItemRangeRemoved(indexList.get(0),
+ indexList.size());
+ }
+ }
+ forumEntry.setShowingDescendants(false);
+ }
+
+ public int getVisiblePos(ForumEntry entry) {
+ int visibleCounter = 0;
+ int levelLimit = -1;
+ for (int i = 0; i < forumEntries.size(); i++) {
+ ForumEntry forumEntry = forumEntries.get(i);
+ if (forumEntry.equals(entry)) {
+ return visibleCounter;
+ } else if (levelLimit >= 0 &&
+ levelLimit < forumEntry.getLevel()) {
+ // entry is in a hidden sub-tree
+ continue;
+ }
+ levelLimit = -1;
+ if (!forumEntry.isShowingDescendants()) {
+ levelLimit = forumEntry.getLevel();
+ }
+ visibleCounter++;
+ }
+ return -1;
+ }
+
+ @NonNull
+ public ForumEntry getVisibleEntry(int position) {
+ int levelLimit = -1;
+ for (ForumEntry forumEntry : forumEntries) {
+ if (levelLimit >= 0) {
+ if (forumEntry.getLevel() > levelLimit) {
+ continue;
+ }
+ levelLimit = -1;
+ }
+ if (!forumEntry.isShowingDescendants()) {
+ levelLimit = forumEntry.getLevel();
+ }
+ if (position-- == 0) {
+ return forumEntry;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ForumViewHolder onCreateViewHolder(
+ ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.forum_discussion_cell, parent, false);
+ return new ForumViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(
+ final ForumViewHolder ui, final int position) {
+ final ForumEntry data = getVisibleEntry(position);
+ if (!data.isRead()) {
+ data.setRead(true);
+ forumController.entryRead(data);
+ }
+ ui.textView.setText(data.getText());
+
+ for (int i = 0; i < ui.lvls.length; i++) {
+ ui.lvls[i].setVisibility(i < data.getLevel() ? VISIBLE : GONE);
+ }
+ if (data.getLevel() > 5) {
+ ui.lvlText.setVisibility(VISIBLE);
+ ui.lvlText.setText("" + data.getLevel());
+ } else {
+ ui.lvlText.setVisibility(GONE);
+ }
+ ui.dateText.setText(DateUtils
+ .getRelativeTimeSpanString(ForumActivity.this,
+ data.getTimestamp()) + " " + data.getAuthor());
+
+ int replies = getReplyCount(data);
+ if (replies == 0) {
+ ui.repliesText.setText("");
+ } else {
+ ui.repliesText.setText(getResources()
+ .getQuantityString(R.plurals.message_replies, replies,
+ replies));
+ }
+ ui.avatar.setImageDrawable(
+ new IdenticonDrawable(data.getAuthorId().getBytes()));
+
+ if (hasDescendants(data)) {
+ ui.chevron.setVisibility(VISIBLE);
+ if (hasVisibleDescendants(position)) {
+ ui.chevron.setSelected(false);
+ } else {
+ ui.chevron.setSelected(true);
+ }
+ ui.chevron.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ui.chevron.setSelected(!ui.chevron.isSelected());
+ if (ui.chevron.isSelected()) {
+ hideDescendants(data);
+ } else {
+ showDescendants(data);
+ }
+ }
+ });
+ } else {
+ ui.chevron.setVisibility(INVISIBLE);
+ }
+ if (data.equals(replyEntry)) {
+ ui.cell.setBackgroundColor(ContextCompat
+ .getColor(ForumActivity.this,
+ R.color.forum_cell_highlight));
+ } else if (data.equals(addedEntry)) {
+ CustomAnimations.animateColorTransition(ui.cell, ContextCompat
+ .getColor(ForumActivity.this,
+ R.color.window_background), 3000,
+ new ResultHandler() {
+ @Override
+ public void onResult(Void result) {
+ ui.setIsRecyclable(true);
+ }
+ });
+ // don't allow cell recycling until the animation finishes
+ ui.setIsRecyclable(false);
+ addedEntry = null;
+ } else {
+ ui.cell.setBackgroundColor(ContextCompat
+ .getColor(ForumActivity.this,
+ R.color.window_background));
+ }
+ ui.replyButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (inputContainer.getVisibility() != VISIBLE) {
+ showTextInput(false);
+ }
+ setReplyEntry(data);
+ linearLayoutManager
+ .scrollToPositionWithOffset(position, 0);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ int visibleCounter = 0;
+ int levelLimit = -1;
+ for (ForumEntry forumEntry : forumEntries) {
+ if (levelLimit >= 0) {
+ if (forumEntry.getLevel() > levelLimit) {
+ continue;
+ }
+ levelLimit = -1;
+ }
+ if (!forumEntry.isShowingDescendants()) {
+ levelLimit = forumEntry.getLevel();
+ }
+ visibleCounter++;
+ }
+ return visibleCounter;
+ }
+ }
+
+
}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java
new file mode 100644
index 000000000..2fa4f0dab
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumController.java
@@ -0,0 +1,27 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+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, UiResultHandler resultHandler);
+ String getForumName();
+ List getForumEntries();
+ void unsubscribe(UiResultHandler resultHandler);
+ void entryRead(ForumEntry forumEntry);
+ void entriesRead(Collection messageIds);
+ void createPost(byte[] body);
+ void createPost(byte[] body, MessageId parentId);
+
+ public interface ForumPostListener {
+ void addLocalEntry(int index, ForumEntry entry);
+ void addForeignEntry(int index, ForumEntry entry);
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
new file mode 100644
index 000000000..676f2d51d
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java
@@ -0,0 +1,371 @@
+package org.briarproject.android.forum;
+
+import android.app.Activity;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.crypto.KeyParser;
+import org.briarproject.api.crypto.PrivateKey;
+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.event.MessageStateChangedEvent;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumPost;
+import org.briarproject.api.forum.ForumPostFactory;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.IdentityManager;
+import org.briarproject.api.identity.LocalAuthor;
+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.Stack;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
+
+public class ForumControllerImpl extends DbControllerImpl
+ implements ForumController, EventListener {
+
+ private static final Logger LOG =
+ Logger.getLogger(ForumControllerImpl.class.getName());
+
+ @Inject
+ protected Activity activity;
+ @Inject
+ @CryptoExecutor
+ protected Executor cryptoExecutor;
+ @Inject
+ protected volatile ForumPostFactory forumPostFactory;
+ @Inject
+ protected volatile CryptoComponent crypto;
+ @Inject
+ protected volatile ForumManager forumManager;
+ @Inject
+ protected volatile EventBus eventBus;
+ @Inject
+ protected volatile IdentityManager identityManager;
+ @Inject
+ protected ForumPersistentData data;
+
+ private ForumPostListener listener;
+ private MessageId localAdd = null;
+
+ @Inject
+ ForumControllerImpl() {
+
+ }
+
+ @Override
+ public void onActivityCreate() {
+ if (activity instanceof ForumPostListener) {
+ listener = (ForumPostListener) activity;
+ } else {
+ throw new IllegalStateException(
+ "An activity that injects the ForumController must " +
+ "implement the ForumPostListener");
+ }
+ }
+
+ @Override
+ public void onActivityResume() {
+ eventBus.addListener(this);
+ }
+
+ @Override
+ public void onActivityPause() {
+ eventBus.removeListener(this);
+ }
+
+ @Override
+ public void onActivityDestroy() {
+ if (activity.isFinishing()) {
+ data.clearAll();
+ }
+ }
+
+ private void findSingleNewEntry() {
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ List oldEntries = getForumEntries();
+ data.clearHeaders();
+ try {
+ loadPosts();
+ List allEntries = getForumEntries();
+ int i = 0;
+ for (ForumEntry entry : allEntries) {
+ boolean isNew = true;
+ for (ForumEntry oldEntry : oldEntries) {
+ if (entry.getMessageId()
+ .equals(oldEntry.getMessageId())) {
+ isNew = false;
+ break;
+ }
+ }
+ if (isNew) {
+ if (localAdd != null &&
+ entry.getMessageId().equals(localAdd)) {
+ addLocalEntry(i, entry);
+ } else {
+ addForeignEntry(i, entry);
+ }
+ break;
+ }
+ i++;
+ }
+ } catch (DbException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void eventOccurred(Event e) {
+ if (e instanceof MessageStateChangedEvent) {
+ MessageStateChangedEvent m = (MessageStateChangedEvent) e;
+ if (m.getState() == DELIVERED &&
+ m.getMessage().getGroupId().equals(data.getGroupId())) {
+ LOG.info("Message added, reloading");
+ findSingleNewEntry();
+ }
+ } else if (e instanceof GroupRemovedEvent) {
+ GroupRemovedEvent s = (GroupRemovedEvent) e;
+ if (s.getGroup().getId().equals(data.getGroupId())) {
+ LOG.info("Forum removed");
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ activity.finish();
+ }
+ });
+ }
+ }
+ }
+
+ private void loadAuthor() throws DbException {
+ Collection localAuthors =
+ identityManager.getLocalAuthors();
+
+ for (LocalAuthor author : localAuthors) {
+ if (author == null)
+ continue;
+ data.setLocalAuthor(author);
+ break;
+ }
+ }
+
+ private void loadPosts() throws DbException {
+ long now = System.currentTimeMillis();
+ Collection headers =
+ forumManager.getPostHeaders(data.getGroupId());
+ data.addHeaders(headers);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading headers took " + duration + " ms");
+ now = System.currentTimeMillis();
+ for (ForumPostHeader header : headers) {
+ if (data.getBody(header.getId()) == null) {
+ byte[] body = forumManager.getPostBody(header.getId());
+ data.addBody(header.getId(), body);
+ }
+ }
+ duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading bodies took " + duration + " ms");
+ }
+
+ @Override
+ public void loadForum(final GroupId groupId,
+ final UiResultHandler resultHandler) {
+ LOG.info("Loading forum...");
+
+ runOnDbThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (data.getGroupId() == null ||
+ !data.getGroupId().equals(groupId)) {
+ data.setGroupId(groupId);
+ long now = System.currentTimeMillis();
+ data.setForum(forumManager.getForum(groupId));
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading forum took " + duration +
+ " ms");
+ now = System.currentTimeMillis();
+ loadAuthor();
+ duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info("Loading author took " + duration +
+ " ms");
+ loadPosts();
+ }
+ resultHandler.onResult(true);
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ resultHandler.onResult(false);
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public String getForumName() {
+ return data.getForum() == null ? null : data.getForum().getName();
+ }
+
+ @Override
+ public List getForumEntries() {
+ Collection headers = data.getHeaders();
+ List forumEntries = new ArrayList<>();
+ Stack idStack = new Stack<>();
+
+ for (ForumPostHeader h : headers) {
+ if (h.getParentId() == null) {
+ idStack.clear();
+ } else if (idStack.isEmpty() ||
+ !idStack.contains(h.getParentId())) {
+ idStack.push(h.getParentId());
+ } else if (!h.getParentId().equals(idStack.peek())) {
+ do {
+ idStack.pop();
+ } while (!h.getParentId().equals(idStack.peek()));
+ }
+ forumEntries.add(new ForumEntry(h,
+ StringUtils.fromUtf8(data.getBody(h.getId())),
+ idStack.size()));
+ }
+ return forumEntries;
+ }
+
+ @Override
+ public void unsubscribe(final UiResultHandler resultHandler) {
+ runOnDbThread(new Runnable() {
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ forumManager.removeForum(data.getForum());
+ 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 forumEntries) {
+ runOnDbThread(new Runnable() {
+ public void run() {
+ try {
+ long now = System.currentTimeMillis();
+ for (ForumEntry fe : forumEntries) {
+ forumManager.setReadFlag(fe.getMessageId(), 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);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void createPost(byte[] body) {
+ createPost(body, null);
+ }
+
+ @Override
+ public void createPost(final byte[] body, final MessageId parentId) {
+ cryptoExecutor.execute(new Runnable() {
+ public void run() {
+ // Don't use an earlier timestamp than the newest post
+ long timestamp = System.currentTimeMillis();
+ ForumPost p;
+ try {
+ KeyParser keyParser = crypto.getSignatureKeyParser();
+ byte[] b = data.getLocalAuthor().getPrivateKey();
+ PrivateKey authorKey = keyParser.parsePrivateKey(b);
+ p = forumPostFactory.createPseudonymousPost(
+ data.getGroupId(), timestamp, parentId,
+ data.getLocalAuthor(), "text/plain", body,
+ authorKey);
+ } catch (GeneralSecurityException | FormatException e) {
+ throw new RuntimeException(e);
+ }
+ storePost(p);
+ }
+ });
+ }
+
+ private void addLocalEntry(final int index, final ForumEntry entry) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ listener.addLocalEntry(index, entry);
+ }
+ });
+ }
+
+ private void addForeignEntry(final int index, final ForumEntry entry) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ listener.addForeignEntry(index, entry);
+ }
+ });
+ }
+
+ private void storePost(final ForumPost p) {
+ runOnDbThread(new Runnable() {
+ public void run() {
+ try {
+ localAdd = p.getMessage().getId();
+ long now = System.currentTimeMillis();
+ forumManager.addLocalPost(p);
+ long duration = System.currentTimeMillis() - now;
+ if (LOG.isLoggable(INFO))
+ LOG.info(
+ "Storing message took " + duration + " ms");
+ } catch (DbException e) {
+ if (LOG.isLoggable(WARNING))
+ LOG.log(WARNING, e.toString(), e);
+ }
+ }
+ });
+ }
+
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
new file mode 100644
index 000000000..c3c668f7a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
@@ -0,0 +1,73 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.MessageId;
+
+public class ForumEntry {
+
+ private final MessageId messageId;
+ private final String text;
+ private final int level;
+ private final long timestamp;
+ private final String author;
+ private final AuthorId authorId;
+ private boolean isShowingDescendants = true;
+ private boolean isRead = true;
+
+ public ForumEntry(ForumPostHeader h, String text, int level) {
+ this(h.getId(), text, level, h.getTimestamp(), h.getAuthor().getName(),
+ h.getAuthor().getId());
+ this.isRead = h.isRead();
+ }
+
+ public ForumEntry(MessageId messageId, String text, int level,
+ long timestamp, String author, AuthorId authorId) {
+ this.messageId = messageId;
+ this.text = text;
+ this.level = level;
+ this.timestamp = timestamp;
+ this.author = author;
+ this.authorId = authorId;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public AuthorId getAuthorId() {
+ return authorId;
+ }
+
+ public boolean isShowingDescendants() {
+ return isShowingDescendants;
+ }
+
+ public void setShowingDescendants(boolean showingDescendants) {
+ this.isShowingDescendants = showingDescendants;
+ }
+
+ public MessageId getMessageId() {
+ return messageId;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public void setRead(boolean read) {
+ isRead = read;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java
new file mode 100644
index 000000000..630c371ae
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java
@@ -0,0 +1,88 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.api.clients.MessageTree;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.LocalAuthor;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.clients.MessageTreeImpl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+/**
+ * This class is a singleton that defines the data that should persist, i.e.
+ * still be present in memory after activity restarts. This class is not thread
+ * safe.
+ */
+public class ForumPersistentData {
+
+ protected volatile MessageTree tree =
+ new MessageTreeImpl<>();
+ private volatile Map bodyCache = new HashMap<>();
+ private volatile LocalAuthor localAuthor;
+ private volatile Forum forum;
+ private volatile GroupId groupId;
+
+ @Inject
+ public ForumPersistentData() {
+
+ }
+
+ public void clearAll() {
+ tree.clear();
+ bodyCache.clear();
+ localAuthor = null;
+ forum = null;
+ groupId = null;
+ }
+
+ public void clearHeaders() {
+ tree.clear();
+ }
+
+ public void addHeaders(Collection headers) {
+ tree.add(headers);
+ }
+
+ public Collection getHeaders() {
+ return tree.depthFirstOrder();
+ }
+
+ public void addBody(MessageId messageId, byte[] body) {
+ bodyCache.put(messageId, body);
+ }
+
+ public byte[] getBody(MessageId messageId) {
+ return bodyCache.get(messageId);
+ }
+
+ public LocalAuthor getLocalAuthor() {
+ return localAuthor;
+ }
+
+ public void setLocalAuthor(
+ LocalAuthor localAuthor) {
+ this.localAuthor = localAuthor;
+ }
+
+ public Forum getForum() {
+ return forum;
+ }
+
+ public void setForum(Forum forum) {
+ this.forum = forum;
+ }
+
+ public GroupId getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(GroupId groupId) {
+ this.groupId = groupId;
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
new file mode 100644
index 000000000..ec1470ed8
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
@@ -0,0 +1,179 @@
+package org.briarproject.android.forum;
+
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+public class ForumTestControllerImpl implements ForumController {
+
+ private static final Logger LOG =
+ Logger.getLogger(ForumControllerImpl.class.getName());
+
+ private final static String[] AUTHORS = {
+ "Guðmundur",
+ "Jónas",
+ "Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
+ "Baldur Friðrik",
+ "Anna Katrín",
+ "Þór",
+ "Anna Þorbjörg",
+ "Guðrún",
+ "Helga",
+ "Haraldur"
+ };
+
+ private final static AuthorId[] AUTHOR_ID = new AuthorId[AUTHORS.length];
+
+ static {
+ SecureRandom random = new SecureRandom();
+ for (int i = 0; i < AUTHOR_ID.length; i++) {
+ byte[] b = new byte[UniqueId.LENGTH];
+ random.nextBytes(b);
+ AUTHOR_ID[i] = new AuthorId(b);
+
+ }
+ }
+
+ private final static String SAGA =
+ "Það er upphaf á sögu þessari að Hákon konungur " +
+ "Aðalsteinsfóstri réð fyrir Noregi og var þetta á ofanverðum " +
+ "hans dögum. Þorkell hét maður; hann var kallaður skerauki; " +
+ "hann bjó í Súrnadal og var hersir að nafnbót. Hann átti sér " +
+ "konu er Ísgerður hét og sonu þrjá barna; hét einn Ari, annar " +
+ "Gísli, þriðji Þorbjörn, hann var þeirra yngstur, og uxu allir " +
+ "upp heima þar. " +
+ "Maður er nefndur Ísi; hann bjó í firði er Fibuli heitir á " +
+ "Norðmæri; kona hans hét Ingigerður en Ingibjörg dóttir. Ari, " +
+ "sonur Þorkels Sýrdæls, biður hennar og var hún honum gefin " +
+ "með miklu fé. Kolur hét þræll er í brott fór með henni.";
+
+ private ForumEntry[] forumEntries;
+
+ @Inject
+ public ForumTestControllerImpl() {
+
+ }
+
+ private void textRandomize(SecureRandom random, int[] i) {
+ for (int e = 0; e < forumEntries.length; e++) {
+ // select a random white-space for the cut-off
+ do {
+ i[e] = Math.abs(random.nextInt() % (SAGA.length()));
+ } while (SAGA.charAt(i[e]) != ' ');
+ }
+ }
+
+ private int levelRandomize(SecureRandom random, int[] l) {
+ int maxl = 0;
+ int lastl = 0;
+ l[0] = 0;
+ for (int e = 1; e < forumEntries.length; e++) {
+ // select random level 1-10
+ do {
+ l[e] = Math.abs(random.nextInt() % 10);
+ } while (l[e] > lastl + 1);
+ lastl = l[e];
+ if (lastl > maxl)
+ maxl = lastl;
+ }
+ return maxl;
+ }
+
+ @Override
+ public void loadForum(GroupId groupId,
+ UiResultHandler resultHandler) {
+ SecureRandom random = new SecureRandom();
+ forumEntries = new ForumEntry[100];
+ // string cut off index
+ int[] i = new int[forumEntries.length];
+ // entry discussion level
+ int[] l = new int[forumEntries.length];
+
+ textRandomize(random, i);
+ int maxLevel;
+ // make sure we get a deep discussion
+ do {
+ maxLevel = levelRandomize(random, l);
+ } while (maxLevel < 6);
+ for (int e = 0; e < forumEntries.length; e++) {
+ int authorIndex = Math.abs(random.nextInt() % AUTHORS.length);
+ long timestamp =
+ System.currentTimeMillis() - Math.abs(random.nextInt());
+ byte[] b = new byte[UniqueId.LENGTH];
+ random.nextBytes(b);
+ forumEntries[e] =
+ new ForumEntry(new MessageId(b), SAGA.substring(0, i[e]),
+ l[e], timestamp, AUTHORS[authorIndex],
+ AUTHOR_ID[authorIndex]);
+ }
+ LOG.info("forum entries: " + forumEntries.length);
+ resultHandler.onResult(true);
+ }
+
+ @Override
+ public String getForumName() {
+ return "SAGA";
+ }
+
+ @Override
+ public List getForumEntries() {
+ return forumEntries == null ? null :
+ new ArrayList(Arrays.asList(forumEntries));
+ }
+
+ @Override
+ public void unsubscribe(UiResultHandler resultHandler) {
+
+ }
+
+ @Override
+ public void entryRead(ForumEntry forumEntry) {
+
+ }
+
+ @Override
+ public void entriesRead(Collection messageIds) {
+
+ }
+
+ @Override
+ public void createPost(byte[] body) {
+
+ }
+
+ @Override
+ public void createPost(byte[] body, MessageId parentId) {
+
+ }
+
+ @Override
+ public void onActivityCreate() {
+
+ }
+
+ @Override
+ public void onActivityResume() {
+
+ }
+
+ @Override
+ public void onActivityPause() {
+
+ }
+
+ @Override
+ public void onActivityDestroy() {
+
+ }
+}
diff --git a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java
deleted file mode 100644
index cfc80ace2..000000000
--- a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java
+++ /dev/null
@@ -1,249 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.text.format.DateUtils;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.android.AndroidComponent;
-import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.AuthorView;
-import org.briarproject.android.util.ElasticHorizontalSpace;
-import org.briarproject.android.util.HorizontalBorder;
-import org.briarproject.android.util.LayoutUtils;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchMessageException;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.identity.Author;
-import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
-
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import static android.view.Gravity.CENTER;
-import static android.view.Gravity.CENTER_VERTICAL;
-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.forum.ForumActivity.FORUM_NAME;
-import static org.briarproject.android.forum.ForumActivity.MIN_TIMESTAMP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
-import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
-
-public class ReadForumPostActivity extends BriarActivity
-implements OnClickListener {
-
- static final int RESULT_REPLY = RESULT_FIRST_USER;
- static final int RESULT_PREV_NEXT = RESULT_FIRST_USER + 1;
-
- private static final Logger LOG =
- Logger.getLogger(ReadForumPostActivity.class.getName());
-
- private GroupId groupId = null;
- private String forumName = null;
- private long minTimestamp = -1;
- private ImageButton prevButton = null, nextButton = null;
- private ImageButton replyButton = null;
- private TextView content = null;
- private int position = -1;
-
- // Fields that are accessed from background threads must be volatile
- @Inject protected volatile ForumManager forumManager;
- private volatile MessageId messageId = null;
-
- @Override
- public void onCreate(Bundle state) {
- super.onCreate(state);
-
- Intent i = getIntent();
- byte[] b = i.getByteArrayExtra(GROUP_ID);
- if (b == null) throw new IllegalStateException();
- groupId = new GroupId(b);
- forumName = i.getStringExtra(FORUM_NAME);
- if (forumName == null) throw new IllegalStateException();
- setTitle(forumName);
- b = i.getByteArrayExtra("briar.MESSAGE_ID");
- if (b == null) throw new IllegalStateException();
- messageId = new MessageId(b);
- String contentType = i.getStringExtra("briar.CONTENT_TYPE");
- if (contentType == null) throw new IllegalStateException();
- long timestamp = i.getLongExtra("briar.TIMESTAMP", -1);
- if (timestamp == -1) throw new IllegalStateException();
- minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
- if (minTimestamp == -1) throw new IllegalStateException();
- position = i.getIntExtra("briar.POSITION", -1);
- if (position == -1) throw new IllegalStateException();
- String authorName = i.getStringExtra("briar.AUTHOR_NAME");
- AuthorId authorId = null;
- b = i.getByteArrayExtra("briar.AUTHOR_ID");
- if (b != null) authorId = new AuthorId(b);
- String s = i.getStringExtra("briar.AUTHOR_STATUS");
- if (s == null) throw new IllegalStateException();
- Author.Status authorStatus = Author.Status.valueOf(s);
-
- LinearLayout layout = new LinearLayout(this);
- layout.setLayoutParams(MATCH_MATCH);
- layout.setOrientation(VERTICAL);
-
- ScrollView scrollView = new ScrollView(this);
- scrollView.setLayoutParams(MATCH_WRAP_1);
-
- LinearLayout message = new LinearLayout(this);
- message.setOrientation(VERTICAL);
-
- LinearLayout header = new LinearLayout(this);
- header.setLayoutParams(MATCH_WRAP);
- header.setOrientation(HORIZONTAL);
- header.setGravity(CENTER_VERTICAL);
-
- int pad = LayoutUtils.getPadding(this);
-
- AuthorView authorView = new AuthorView(this);
- authorView.setPadding(0, pad, pad, pad);
- authorView.setLayoutParams(WRAP_WRAP_1);
- authorView.init(authorName, authorId, authorStatus);
- header.addView(authorView);
-
- TextView date = new TextView(this);
- date.setPadding(pad, pad, pad, pad);
- date.setText(DateUtils.getRelativeTimeSpanString(this, timestamp));
- header.addView(date);
- message.addView(header);
-
- if (contentType.equals("text/plain")) {
- // Load and display the message body
- content = new TextView(this);
- content.setPadding(pad, 0, pad, pad);
- message.addView(content);
- loadPostBody();
- }
- scrollView.addView(message);
- layout.addView(scrollView);
-
- layout.addView(new HorizontalBorder(this));
-
- LinearLayout footer = new LinearLayout(this);
- footer.setLayoutParams(MATCH_WRAP);
- footer.setOrientation(HORIZONTAL);
- footer.setGravity(CENTER);
- Resources res = getResources();
- footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
-
- prevButton = new ImageButton(this);
- prevButton.setBackgroundResource(0);
- prevButton.setImageResource(R.drawable.navigation_previous_item);
- prevButton.setOnClickListener(this);
- footer.addView(prevButton);
- footer.addView(new ElasticHorizontalSpace(this));
-
- nextButton = new ImageButton(this);
- nextButton.setBackgroundResource(0);
- nextButton.setImageResource(R.drawable.navigation_next_item);
- nextButton.setOnClickListener(this);
- footer.addView(nextButton);
- footer.addView(new ElasticHorizontalSpace(this));
-
- replyButton = new ImageButton(this);
- replyButton.setBackgroundResource(0);
- replyButton.setImageResource(R.drawable.social_reply_all);
- replyButton.setOnClickListener(this);
- footer.addView(replyButton);
- layout.addView(footer);
-
- setContentView(layout);
- }
-
- @Override
- public void injectActivity(ActivityComponent component) {
- component.inject(this);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (isFinishing()) markPostRead();
- }
-
- private void markPostRead() {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- forumManager.setReadFlag(messageId, 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);
- }
- }
- });
- }
-
- private void loadPostBody() {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- byte[] body = forumManager.getPostBody(messageId);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Loading post took " + duration + " ms");
- displayPostBody(StringUtils.fromUtf8(body));
- } catch (NoSuchMessageException e) {
- finishOnUiThread();
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- private void displayPostBody(final String body) {
- runOnUiThread(new Runnable() {
- public void run() {
- content.setText(body);
- }
- });
- }
-
- public void onClick(View view) {
- if (view == prevButton) {
- Intent i = new Intent();
- i.putExtra("briar.POSITION", position - 1);
- setResult(RESULT_PREV_NEXT, i);
- finish();
- } else if (view == nextButton) {
- Intent i = new Intent();
- i.putExtra("briar.POSITION", position + 1);
- setResult(RESULT_PREV_NEXT, i);
- finish();
- } else if (view == replyButton) {
- Intent i = new Intent(this, WriteForumPostActivity.class);
- i.putExtra(GROUP_ID, groupId.getBytes());
- i.putExtra(FORUM_NAME, forumName);
- i.putExtra("briar.PARENT_ID", messageId.getBytes());
- i.putExtra(MIN_TIMESTAMP, minTimestamp);
- startActivity(i);
- setResult(RESULT_REPLY);
- finish();
- }
- }
-}
diff --git a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
deleted file mode 100644
index d2185aa46..000000000
--- a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
+++ /dev/null
@@ -1,317 +0,0 @@
-package org.briarproject.android.forum;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.InputType;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.EditText;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.briarproject.R;
-import org.briarproject.android.ActivityComponent;
-import org.briarproject.android.AndroidComponent;
-import org.briarproject.android.BriarActivity;
-import org.briarproject.android.identity.CreateIdentityActivity;
-import org.briarproject.android.identity.LocalAuthorItem;
-import org.briarproject.android.identity.LocalAuthorItemComparator;
-import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
-import org.briarproject.android.util.CommonLayoutParams;
-import org.briarproject.android.util.LayoutUtils;
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.CryptoExecutor;
-import org.briarproject.api.crypto.KeyParser;
-import org.briarproject.api.crypto.PrivateKey;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.forum.ForumPost;
-import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.identity.AuthorId;
-import org.briarproject.api.identity.IdentityManager;
-import org.briarproject.api.identity.LocalAuthor;
-import org.briarproject.api.sync.GroupId;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.util.StringUtils;
-
-import java.security.GeneralSecurityException;
-import java.util.Collection;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import static android.text.InputType.TYPE_CLASS_TEXT;
-import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
-import static android.widget.LinearLayout.VERTICAL;
-import static android.widget.RelativeLayout.ALIGN_PARENT_LEFT;
-import static android.widget.RelativeLayout.ALIGN_PARENT_RIGHT;
-import static android.widget.RelativeLayout.CENTER_VERTICAL;
-import static android.widget.RelativeLayout.LEFT_OF;
-import static android.widget.RelativeLayout.RIGHT_OF;
-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.android.forum.ForumActivity.MIN_TIMESTAMP;
-import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
-
-public class WriteForumPostActivity extends BriarActivity
-implements OnItemSelectedListener, OnClickListener {
-
- private static final int REQUEST_CREATE_IDENTITY = 2;
- private static final Logger LOG =
- Logger.getLogger(WriteForumPostActivity.class.getName());
-
- @Inject @CryptoExecutor protected Executor cryptoExecutor;
- private LocalAuthorSpinnerAdapter adapter = null;
- private Spinner spinner = null;
- private ImageButton sendButton = null;
- private EditText content = null;
- private AuthorId localAuthorId = null;
- private GroupId groupId = null;
-
- // Fields that are accessed from background threads must be volatile
- @Inject protected volatile IdentityManager identityManager;
- @Inject protected volatile ForumManager forumManager;
- @Inject protected volatile ForumPostFactory forumPostFactory;
- @Inject protected volatile CryptoComponent crypto;
- private volatile MessageId parentId = null;
- private volatile long minTimestamp = -1;
- private volatile LocalAuthor localAuthor = null;
- private volatile Forum forum = null;
-
- @Override
- public void onCreate(Bundle state) {
- super.onCreate(state);
-
- Intent i = getIntent();
- byte[] b = i.getByteArrayExtra(GROUP_ID);
- if (b == null) throw new IllegalStateException();
- groupId = new GroupId(b);
- String forumName = i.getStringExtra(FORUM_NAME);
- if (forumName == null) throw new IllegalStateException();
- setTitle(forumName);
- minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
- if (minTimestamp == -1) throw new IllegalStateException();
- b = i.getByteArrayExtra("briar.PARENT_ID");
- if (b != null) parentId = new MessageId(b);
-
- if (state != null) {
- b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
- if (b != null) localAuthorId = new AuthorId(b);
- }
-
- LinearLayout layout = new LinearLayout(this);
- layout.setLayoutParams(MATCH_WRAP);
- layout.setOrientation(VERTICAL);
- int pad = LayoutUtils.getPadding(this);
- layout.setPadding(pad, 0, pad, pad);
-
- RelativeLayout header = new RelativeLayout(this);
-
- TextView from = new TextView(this);
- from.setId(1);
- from.setTextSize(18);
- from.setText(R.string.from);
- RelativeLayout.LayoutParams left = CommonLayoutParams.relative();
- left.addRule(ALIGN_PARENT_LEFT);
- left.addRule(CENTER_VERTICAL);
- header.addView(from, left);
-
- adapter = new LocalAuthorSpinnerAdapter(this, true);
- spinner = new Spinner(this);
- spinner.setId(2);
- spinner.setAdapter(adapter);
- spinner.setOnItemSelectedListener(this);
- RelativeLayout.LayoutParams between = CommonLayoutParams.relative();
- between.addRule(CENTER_VERTICAL);
- between.addRule(RIGHT_OF, 1);
- between.addRule(LEFT_OF, 3);
- header.addView(spinner, between);
-
- sendButton = new ImageButton(this);
- sendButton.setId(3);
- sendButton.setBackgroundResource(0);
- sendButton.setImageResource(R.drawable.social_send_now);
- sendButton.setEnabled(false); // Enabled after loading the forum
- sendButton.setOnClickListener(this);
- RelativeLayout.LayoutParams right = CommonLayoutParams.relative();
- right.addRule(ALIGN_PARENT_RIGHT);
- right.addRule(CENTER_VERTICAL);
- header.addView(sendButton, right);
- layout.addView(header);
-
- content = new EditText(this);
- content.setId(4);
- int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
- | TYPE_TEXT_FLAG_CAP_SENTENCES;
- content.setInputType(inputType);
- content.setHint(R.string.forum_post_hint);
- layout.addView(content);
-
- setContentView(layout);
- }
-
- @Override
- public void injectActivity(ActivityComponent component) {
- component.inject(this);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- loadAuthorsAndForum();
- }
-
- private void loadAuthorsAndForum() {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- Collection localAuthors =
- identityManager.getLocalAuthors();
- forum = forumManager.getForum(groupId);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Load took " + duration + " ms");
- displayAuthorsAndForum(localAuthors);
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-
- private void displayAuthorsAndForum(
- final Collection localAuthors) {
- runOnUiThread(new Runnable() {
- public void run() {
- if (localAuthors.isEmpty()) throw new IllegalStateException();
- adapter.clear();
- for (LocalAuthor a : localAuthors)
- adapter.add(new LocalAuthorItem(a));
- adapter.sort(LocalAuthorItemComparator.INSTANCE);
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- LocalAuthorItem item = adapter.getItem(i);
- if (item == LocalAuthorItem.ANONYMOUS) continue;
- if (item == LocalAuthorItem.NEW) continue;
- if (item.getLocalAuthor().getId().equals(localAuthorId)) {
- localAuthor = item.getLocalAuthor();
- spinner.setSelection(i);
- break;
- }
- }
- setTitle(forum.getName());
- sendButton.setEnabled(true);
- }
- });
- }
-
- @Override
- public void onSaveInstanceState(Bundle state) {
- super.onSaveInstanceState(state);
- if (localAuthorId != null) {
- byte[] b = localAuthorId.getBytes();
- state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
- }
- }
-
- @Override
- protected void onActivityResult(int request, int result, Intent data) {
- super.onActivityResult(request, result, data);
- if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
- byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
- if (b == null) throw new IllegalStateException();
- localAuthorId = new AuthorId(b);
- loadAuthorsAndForum();
- }
- }
-
- public void onItemSelected(AdapterView> parent, View view, int position,
- long id) {
- LocalAuthorItem item = adapter.getItem(position);
- if (item == LocalAuthorItem.ANONYMOUS) {
- localAuthor = null;
- localAuthorId = null;
- } else if (item == LocalAuthorItem.NEW) {
- localAuthor = null;
- localAuthorId = null;
- Intent i = new Intent(this, CreateIdentityActivity.class);
- startActivityForResult(i, REQUEST_CREATE_IDENTITY);
- } else {
- localAuthor = item.getLocalAuthor();
- localAuthorId = localAuthor.getId();
- }
- }
-
- public void onNothingSelected(AdapterView> parent) {
- localAuthor = null;
- localAuthorId = null;
- }
-
- public void onClick(View view) {
- if (forum == null) throw new IllegalStateException();
- String body = content.getText().toString();
- if (body.equals("")) return;
- createPost(StringUtils.toUtf8(body));
- Toast.makeText(this, R.string.post_sent_toast, LENGTH_LONG).show();
- finish();
- }
-
- private void createPost(final byte[] body) {
- cryptoExecutor.execute(new Runnable() {
- public void run() {
- // Don't use an earlier timestamp than the newest post
- long timestamp = System.currentTimeMillis();
- timestamp = Math.max(timestamp, minTimestamp);
- ForumPost p;
- try {
- if (localAuthor == null) {
- p = forumPostFactory.createAnonymousPost(groupId,
- timestamp, parentId, "text/plain", body);
- } else {
- KeyParser keyParser = crypto.getSignatureKeyParser();
- byte[] b = localAuthor.getPrivateKey();
- PrivateKey authorKey = keyParser.parsePrivateKey(b);
- p = forumPostFactory.createPseudonymousPost(groupId,
- timestamp, parentId, localAuthor, "text/plain",
- body, authorKey);
- }
- } catch (GeneralSecurityException e) {
- throw new RuntimeException(e);
- } catch (FormatException e) {
- throw new RuntimeException(e);
- }
- storePost(p);
- }
- });
- }
-
- private void storePost(final ForumPost p) {
- runOnDbThread(new Runnable() {
- public void run() {
- try {
- long now = System.currentTimeMillis();
- forumManager.addLocalPost(p);
- long duration = System.currentTimeMillis() - now;
- if (LOG.isLoggable(INFO))
- LOG.info("Storing message took " + duration + " ms");
- } catch (DbException e) {
- if (LOG.isLoggable(WARNING))
- LOG.log(WARNING, e.toString(), e);
- }
- }
- });
- }
-}
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
index 93eeb669c..e399ecda0 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
+++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
@@ -1,6 +1,7 @@
package org.briarproject.android.util;
import android.content.Context;
+import android.content.res.TypedArray;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
@@ -18,6 +19,7 @@ public class BriarRecyclerView extends FrameLayout {
private TextView emptyView;
private ProgressBar progressBar;
private RecyclerView.AdapterDataObserver emptyObserver;
+ private boolean isScrollingToEnd = false;
public BriarRecyclerView(Context context) {
super(context);
@@ -25,6 +27,11 @@ public class BriarRecyclerView extends FrameLayout {
public BriarRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
+
+ TypedArray attributes = context.obtainStyledAttributes(attrs,
+ R.styleable.BriarRecyclerView);
+ isScrollingToEnd = attributes
+ .getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true);
}
public BriarRecyclerView(Context context, AttributeSet attrs,
@@ -44,7 +51,7 @@ public class BriarRecyclerView extends FrameLayout {
showProgressBar();
// scroll down when opening keyboard
- if (Build.VERSION.SDK_INT >= 11) {
+ if (isScrollingToEnd && Build.VERSION.SDK_INT >= 11) {
recyclerView.addOnLayoutChangeListener(
new View.OnLayoutChangeListener() {
@Override
diff --git a/briar-android/src/org/briarproject/android/util/CustomAnimations.java b/briar-android/src/org/briarproject/android/util/CustomAnimations.java
index 07dec1324..6705194a4 100644
--- a/briar-android/src/org/briarproject/android/util/CustomAnimations.java
+++ b/briar-android/src/org/briarproject/android/util/CustomAnimations.java
@@ -1,11 +1,16 @@
package org.briarproject.android.util;
import android.animation.Animator;
+import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
+import android.graphics.drawable.ColorDrawable;
import android.os.Build;
+import android.view.View;
import android.view.ViewGroup;
+import org.briarproject.android.controller.handler.ResultHandler;
+
import static android.view.View.GONE;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.View.VISIBLE;
@@ -21,6 +26,49 @@ public class CustomAnimations {
}
}
+ @SuppressLint("NewApi")
+ public static void animateColorTransition(final View view, int color,
+ int duration, final ResultHandler finishedCallback) {
+ // No soup for Gingerbread
+ if (Build.VERSION.SDK_INT < 11) {
+ return;
+ }
+ ValueAnimator anim = new ValueAnimator();
+ ColorDrawable viewColor = (ColorDrawable) view.getBackground();
+ anim.setIntValues(viewColor.getColor(), color);
+ anim.setEvaluator(new ArgbEvaluator());
+ anim.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (finishedCallback != null) finishedCallback.onResult(null);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
+ anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ view.setBackgroundColor((Integer)valueAnimator.getAnimatedValue());
+ }
+ });
+ anim.setDuration(duration);
+
+ anim.start();
+ }
+
private static void animateHeightGingerbread(ViewGroup viewGroup,
boolean isExtending) {
// No animations for Gingerbread
diff --git a/briar-android/test/java/briarproject/activity/ForumActivityTest.java b/briar-android/test/java/briarproject/activity/ForumActivityTest.java
new file mode 100644
index 000000000..ee044b112
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/ForumActivityTest.java
@@ -0,0 +1,125 @@
+package briarproject.activity;
+
+import android.content.Intent;
+
+import junit.framework.Assert;
+
+import org.briarproject.BuildConfig;
+import org.briarproject.TestUtils;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.forum.ForumActivity;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumEntry;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = BuildConfig.class, sdk = 21,
+ application = TestBriarApplication.class)
+public class ForumActivityTest {
+
+ private final static String AUTHOR_1 = "Author 1";
+ private final static String AUTHOR_2 = "Author 2";
+ private final static String AUTHOR_3 = "Author 3";
+ private final static String AUTHOR_4 = "Author 4";
+ private final static String AUTHOR_5 = "Author 5";
+ private final static String AUTHOR_6 = "Author 6";
+
+ private final static String[] AUTHORS = {
+ AUTHOR_1, AUTHOR_2, AUTHOR_3, AUTHOR_4, AUTHOR_5, AUTHOR_6
+ };
+
+ /*
+ 1
+ -> 2
+ -> 3
+ -> 4
+ 5
+ 6
+ */
+ private final static int[] LEVELS = {
+ 0, 1, 2, 3, 1, 0
+ };
+
+ private TestForumActivity forumActivity;
+ @Captor
+ private ArgumentCaptor> rc;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Intent intent = new Intent();
+ intent.putExtra("briar.GROUP_ID", TestUtils.getRandomId());
+ forumActivity = Robolectric.buildActivity(TestForumActivity.class)
+ .withIntent(intent).create().resume().get();
+ }
+
+
+ private List getDummyData() {
+ ForumEntry[] forumEntries = new ForumEntry[6];
+ for (int i = 0; i < forumEntries.length; i++) {
+ forumEntries[i] =
+ new ForumEntry(new MessageId(TestUtils.getRandomId()),
+ AUTHORS[i], LEVELS[i], System.currentTimeMillis(),
+ AUTHORS[i], new AuthorId(TestUtils.getRandomId()));
+ }
+ return new ArrayList(Arrays.asList(forumEntries));
+ }
+
+ @Test
+ public void testNestedEntries() {
+ ForumController mc = forumActivity.getController();
+ List dummyData = getDummyData();
+ Mockito.when(mc.getForumEntries()).thenReturn(dummyData);
+ // Verify that the forum load is called once
+ verify(mc, times(1))
+ .loadForum(Mockito.any(GroupId.class), rc.capture());
+ rc.getValue().onResult(true);
+ verify(mc, times(1)).getForumEntries();
+ ForumActivity.ForumAdapter adapter = forumActivity.getAdapter();
+ Assert.assertNotNull(adapter);
+ // Cascade close
+ assertEquals(6, adapter.getItemCount());
+ adapter.hideDescendants(dummyData.get(2));
+ assertEquals(5, adapter.getItemCount());
+ adapter.hideDescendants(dummyData.get(1));
+ assertEquals(4, adapter.getItemCount());
+ adapter.hideDescendants(dummyData.get(0));
+ assertEquals(2, adapter.getItemCount());
+ assertTrue(dummyData.get(0).getText()
+ .equals(adapter.getVisibleEntry(0).getText()));
+ assertTrue(dummyData.get(5).getText()
+ .equals(adapter.getVisibleEntry(1).getText()));
+ // Cascade re-open
+ adapter.showDescendants(dummyData.get(0));
+ assertEquals(4, adapter.getItemCount());
+ adapter.showDescendants(dummyData.get(1));
+ assertEquals(5, adapter.getItemCount());
+ adapter.showDescendants(dummyData.get(2));
+ assertEquals(6, adapter.getItemCount());
+ assertTrue(dummyData.get(2).getText()
+ .equals(adapter.getVisibleEntry(2).getText()));
+ assertTrue(dummyData.get(4).getText()
+ .equals(adapter.getVisibleEntry(4).getText()));
+ }
+}
diff --git a/briar-android/test/java/briarproject/activity/TestForumActivity.java b/briar-android/test/java/briarproject/activity/TestForumActivity.java
new file mode 100644
index 000000000..f73d83e18
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/TestForumActivity.java
@@ -0,0 +1,42 @@
+package briarproject.activity;
+
+import org.briarproject.android.ActivityModule;
+import org.briarproject.android.controller.BriarController;
+import org.briarproject.android.controller.BriarControllerImpl;
+import org.briarproject.android.forum.ForumActivity;
+import org.briarproject.android.forum.ForumController;
+import org.briarproject.android.forum.ForumControllerImpl;
+import org.mockito.Mockito;
+
+/**
+ * This class exposes the SetupController and offers the possibility to
+ * override it.
+ */
+public class TestForumActivity extends ForumActivity {
+
+ public ForumController getController() {
+ return forumController;
+ }
+
+ public ForumAdapter getAdapter() {
+ return forumAdapter;
+ }
+
+ protected ActivityModule getActivityModule() {
+ return new ActivityModule(this) {
+ @Override
+ protected BriarController provideBriarController(
+ BriarControllerImpl briarControllerImpl) {
+ BriarController c = Mockito.mock(BriarController.class);
+ Mockito.when(c.hasEncryptionKey()).thenReturn(true);
+ return c;
+ }
+
+ @Override
+ protected ForumController provideForumController(
+ ForumControllerImpl forumController) {
+ return Mockito.mock(ForumController.class);
+ }
+ };
+ }
+}
diff --git a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
index 55f69cf54..4759f39fd 100644
--- a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
+++ b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java
@@ -11,8 +11,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import javax.inject.Inject;
-
public class MessageTreeImpl
implements MessageTree {
@@ -26,11 +24,6 @@ public class MessageTreeImpl
}
};
- @Inject
- public MessageTreeImpl() {
-
- }
-
@Override
public void clear() {
roots.clear();
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index 04d934a21..e7494850e 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -2,7 +2,6 @@ package org.briarproject.forum;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.MessageQueueManager;
-import org.briarproject.api.clients.MessageTree;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.data.MetadataEncoder;
@@ -10,14 +9,12 @@ import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.forum.ForumFactory;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPostFactory;
-import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.forum.ForumSharingManager;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
-import org.briarproject.clients.MessageTreeImpl;
import java.security.SecureRandom;
@@ -104,10 +101,4 @@ public class ForumModule {
return forumSharingManager;
}
- @Provides
- @Singleton
- MessageTree provideForumMessageTree(
- MessageTreeImpl messageTree) {
- return messageTree;
- }
}