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; - } }