Skip to content
Snippets Groups Projects
Unverified Commit 2d59b909 authored by Ernir Erlingsson's avatar Ernir Erlingsson
Browse files

Fixing concurrency issues and refactoring code

parent f461ec4a
No related branches found
No related tags found
No related merge requests found
Showing
with 767 additions and 808 deletions
......@@ -23,9 +23,6 @@ import org.briarproject.android.controller.SetupControllerImpl;
import org.briarproject.android.controller.TransportStateListener;
import org.briarproject.android.forum.ForumController;
import org.briarproject.android.forum.ForumControllerImpl;
import org.briarproject.android.forum.ForumTestControllerImpl;
import javax.inject.Named;
import dagger.Module;
import dagger.Provides;
......@@ -103,14 +100,6 @@ public class ActivityModule {
return forumController;
}
@Named("ForumTestController")
@ActivityScope
@Provides
protected ForumController provideForumTestController(
ForumTestControllerImpl forumController) {
return forumController;
}
@ActivityScope
@Provides
BlogController provideBlogController(BlogControllerImpl blogController) {
......
package org.briarproject.android.forum;
import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.annotation.Nullable;
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.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
......@@ -30,52 +21,46 @@ import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.forum.ForumController.ForumPostListener;
import org.briarproject.android.forum.NestedForumAdapter.OnNestedForumListener;
import org.briarproject.android.sharing.ShareForumActivity;
import org.briarproject.android.sharing.SharingStatusForumActivity;
import org.briarproject.android.view.AuthorView;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.android.view.TextInputView;
import org.briarproject.android.view.TextInputView.TextInputListener;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
public class ForumActivity extends BriarActivity implements
ForumPostListener, TextInputListener {
ForumPostListener, TextInputListener, OnNestedForumListener {
static final String FORUM_NAME = "briar.FORUM_NAME";
private static final int REQUEST_FORUM_SHARED = 3;
private static final int UNDEFINED = -1;
private static final String KEY_INPUT_VISIBILITY = "inputVisibility";
private static final String KEY_REPLY_ID = "replyId";
@Inject
AndroidNotificationManager notificationManager;
// uncomment the next line for a test component with dummy data
// @Named("ForumTestController")
@Inject
protected ForumController forumController;
// Protected access for testing
protected ForumAdapter forumAdapter;
protected NestedForumAdapter forumAdapter;
private BriarRecyclerView recyclerView;
private TextInputView textInput;
......@@ -96,42 +81,42 @@ public class ForumActivity extends BriarActivity implements
String forumName = i.getStringExtra(FORUM_NAME);
if (forumName != null) setTitle(forumName);
forumAdapter = new ForumAdapter();
textInput = (TextInputView) findViewById(R.id.text_input_container);
textInput.setVisibility(GONE);
textInput.setListener(this);
recyclerView =
(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
recyclerView.setAdapter(forumAdapter);
linearLayoutManager = new LinearLayoutManager(this);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
forumAdapter = new NestedForumAdapter(this, this, linearLayoutManager);
recyclerView.setAdapter(forumAdapter);
recyclerView.setEmptyText(R.string.no_forum_posts);
forumController.loadForum(groupId, new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(Boolean result) {
if (result) {
Forum forum = forumController.getForum();
if (forum != null) setTitle(forum.getName());
List<ForumEntry> entries =
forumController.getForumEntries();
if (entries.isEmpty()) {
recyclerView.showData();
} else {
forumAdapter.setEntries(entries);
if (state != null) {
byte[] replyId = state.getByteArray(KEY_REPLY_ID);
if (replyId != null)
forumAdapter.setReplyEntryById(replyId);
forumController.loadForum(groupId,
new UiResultHandler<List<ForumEntry>>(this) {
@Override
public void onResultUi(List<ForumEntry> result) {
if (result != null) {
Forum forum = forumController.getForum();
if (forum != null) setTitle(forum.getName());
List<ForumEntry> entries = new ArrayList<>(result);
if (entries.isEmpty()) {
recyclerView.showData();
} else {
forumAdapter.setEntries(entries);
if (state != null) {
byte[] replyId =
state.getByteArray(KEY_REPLY_ID);
if (replyId != null)
forumAdapter.setReplyEntryById(replyId);
}
}
} else {
// TODO Improve UX ?
finish();
}
}
} else {
// TODO Maybe an error dialog ?
finish();
}
}
});
});
}
@Override
......@@ -265,12 +250,27 @@ public class ForumActivity extends BriarActivity implements
return;
if (forumController.getForum() == null) return;
ForumEntry replyEntry = forumAdapter.getReplyEntry();
UiResultHandler<ForumPost> resultHandler =
new UiResultHandler<ForumPost>(this) {
@Override
public void onResultUi(ForumPost result) {
forumController.storePost(result,
new UiResultHandler<ForumEntry>(
ForumActivity.this) {
@Override
public void onResultUi(ForumEntry result) {
onForumEntryAdded(result, true);
}
});
}
};
if (replyEntry == null) {
// root post
forumController.createPost(StringUtils.toUtf8(text));
forumController.createPost(StringUtils.toUtf8(text), resultHandler);
} else {
forumController.createPost(StringUtils.toUtf8(text),
replyEntry.getMessageId());
forumController
.createPost(StringUtils.toUtf8(text), replyEntry.getId(),
resultHandler);
}
hideSoftKeyboard(textInput);
textInput.setVisibility(GONE);
......@@ -308,412 +308,47 @@ public class ForumActivity extends BriarActivity implements
}
@Override
public void addLocalEntry(int index, ForumEntry entry) {
forumAdapter.addEntry(index, entry, true);
displaySnackbarShort(R.string.forum_new_entry_posted);
public void onEntryVisible(ForumEntry forumEntry) {
if (!forumEntry.isRead()) {
forumEntry.setRead(true);
forumController.entryRead(forumEntry);
}
}
@Override
public void addForeignEntry(final int index, final ForumEntry entry) {
forumAdapter.addEntry(index, entry, false);
Snackbar snackbar =
Snackbar.make(recyclerView, R.string.forum_new_entry_received,
Snackbar.LENGTH_LONG);
snackbar.setActionTextColor(
ContextCompat.getColor(this, R.color.briar_button_positive));
snackbar.setAction(R.string.show, new View.OnClickListener() {
@Override
public void onClick(View v) {
forumAdapter.scrollToEntry(entry);
}
});
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
public void onReplyClick(ForumEntry forumEntry) {
showTextInput(forumEntry);
}
static class ForumViewHolder extends RecyclerView.ViewHolder {
final TextView textView, lvlText, repliesText;
final AuthorView author;
final View[] lvls;
final View chevron, replyButton;
final ViewGroup cell;
final View topDivider;
ForumViewHolder(View v) {
super(v);
textView = (TextView) v.findViewById(R.id.text);
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
author = (AuthorView) v.findViewById(R.id.author);
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]);
}
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply);
cell = (ViewGroup) v.findViewById(R.id.forum_cell);
topDivider = v.findViewById(R.id.top_divider);
}
}
public class ForumAdapter extends RecyclerView.Adapter<ForumViewHolder> {
private final List<ForumEntry> forumEntries = new ArrayList<>();
private final Map<ForumEntry, ValueAnimator> animatingEntries =
new HashMap<>();
// highlight not dependant on time
private ForumEntry replyEntry;
// temporary highlight
private ForumEntry addedEntry;
private ForumEntry getReplyEntry() {
return replyEntry;
}
void setEntries(List<ForumEntry> entries) {
forumEntries.clear();
forumEntries.addAll(entries);
notifyItemRangeInserted(0, entries.size());
}
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);
}
notifyItemChanged(getVisiblePos(higherEntry));
break;
}
}
}
if (!isShowingDescendants) {
int visiblePos = getVisiblePos(entry);
notifyItemInserted(visiblePos);
if (isScrolling)
linearLayoutManager
.scrollToPositionWithOffset(visiblePos, 0);
}
addedEntry = entry;
}
void scrollToEntry(ForumEntry entry) {
int visiblePos = getVisiblePos(entry);
linearLayoutManager.scrollToPositionWithOffset(visiblePos, 0);
}
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(ForumEntry forumEntry) {
int visiblePos = getVisiblePos(forumEntry);
int levelLimit = forumEntry.getLevel();
// FIXME This loop doesn't really loop. @ernir please review!
for (int i = visiblePos + 1; i < getItemCount(); i++) {
ForumEntry entry = getVisibleEntry(i);
if (entry != null && 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;
}
void setReplyEntryById(byte[] id) {
MessageId messageId = new MessageId(id);
for (ForumEntry entry : forumEntries) {
if (entry.getMessageId().equals(messageId)) {
setReplyEntry(entry);
break;
}
}
}
void setReplyEntry(ForumEntry entry) {
if (replyEntry != null) {
notifyItemChanged(getVisiblePos(replyEntry));
}
replyEntry = entry;
if (replyEntry != null) {
notifyItemChanged(getVisiblePos(replyEntry));
}
}
private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
List<Integer> indexList = new ArrayList<>();
for (int i = pos + 1; i < getItemCount(); i++) {
ForumEntry entry = getVisibleEntry(i);
if (entry != null && entry.getLevel() > levelLimit) {
indexList.add(i);
} else {
break;
}
}
return indexList;
}
void showDescendants(ForumEntry forumEntry) {
forumEntry.setShowingDescendants(true);
int visiblePos = getVisiblePos(forumEntry);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
if (!indexList.isEmpty()) {
if (indexList.size() == 1) {
notifyItemInserted(indexList.get(0));
} else {
notifyItemRangeInserted(indexList.get(0),
indexList.size());
}
}
}
void hideDescendants(ForumEntry forumEntry) {
int visiblePos = getVisiblePos(forumEntry);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
if (!indexList.isEmpty()) {
// stop animating children
for (int index : indexList) {
ValueAnimator anim =
animatingEntries.get(forumEntries.get(index));
if (anim != null && anim.isRunning()) {
anim.cancel();
}
}
if (indexList.size() == 1) {
notifyItemRemoved(indexList.get(0));
} else {
notifyItemRangeRemoved(indexList.get(0),
indexList.size());
}
}
forumEntry.setShowingDescendants(false);
}
@Nullable
ForumEntry getVisibleEntry(int position) {
int levelLimit = UNDEFINED;
for (ForumEntry forumEntry : forumEntries) {
if (levelLimit >= 0) {
if (forumEntry.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (!forumEntry.isShowingDescendants()) {
levelLimit = forumEntry.getLevel();
}
if (position-- == 0) {
return forumEntry;
}
}
return null;
}
private void animateFadeOut(final ForumViewHolder ui,
final ForumEntry addedEntry) {
ui.setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
animatingEntries.put(addedEntry, anim);
ColorDrawable viewColor = (ColorDrawable) ui.cell.getBackground();
anim.setIntValues(viewColor.getColor(), ContextCompat
private void onForumEntryAdded(final ForumEntry entry, boolean isLocal) {
forumAdapter.addEntry(entry);
if (isLocal) {
displaySnackbarShort(R.string.forum_new_entry_posted);
} else {
Snackbar snackbar = Snackbar.make(recyclerView,
R.string.forum_new_entry_received, Snackbar.LENGTH_LONG);
snackbar.setActionTextColor(ContextCompat
.getColor(ForumActivity.this,
R.color.window_background));
anim.setEvaluator(new ArgbEvaluator());
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
ui.setIsRecyclable(true);
animatingEntries.remove(addedEntry);
}
@Override
public void onAnimationCancel(Animator animation) {
ui.setIsRecyclable(true);
animatingEntries.remove(addedEntry);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
ui.cell.setBackgroundColor(
(Integer) valueAnimator.getAnimatedValue());
}
});
anim.setDuration(5000);
anim.start();
}
@Override
public ForumViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_forum_post, parent, false);
return new ForumViewHolder(v);
}
@Override
public void onBindViewHolder(
final ForumViewHolder ui, final int position) {
final ForumEntry data = getVisibleEntry(position);
if (data == null) return;
if (!data.isRead()) {
data.setRead(true);
forumController.entryRead(data);
}
ui.textView.setText(StringUtils.trim(data.getText()));
if (position == 0) {
ui.topDivider.setVisibility(View.INVISIBLE);
} else {
ui.topDivider.setVisibility(View.VISIBLE);
}
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.author.setAuthor(data.getAuthor());
ui.author.setDate(data.getTimestamp());
ui.author.setAuthorStatus(data.getStatus());
int replies = getReplyCount(data);
if (replies == 0) {
ui.repliesText.setText("");
} else {
ui.repliesText.setText(getResources()
.getQuantityString(R.plurals.message_replies, replies,
replies));
}
if (hasDescendants(data)) {
ui.chevron.setVisibility(VISIBLE);
if (hasVisibleDescendants(data)) {
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)) {
ui.cell.setBackgroundColor(ContextCompat
.getColor(ForumActivity.this,
R.color.forum_cell_highlight));
animateFadeOut(ui, addedEntry);
addedEntry = null;
} else {
ui.cell.setBackgroundColor(ContextCompat
.getColor(ForumActivity.this,
R.color.window_background));
}
ui.replyButton.setOnClickListener(new View.OnClickListener() {
R.color.briar_button_positive));
snackbar.setAction(R.string.show, new View.OnClickListener() {
@Override
public void onClick(View v) {
showTextInput(data);
linearLayoutManager
.scrollToPositionWithOffset(getVisiblePos(data), 0);
forumAdapter.scrollToEntry(entry);
}
});
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
}
}
private int getVisiblePos(ForumEntry sEntry) {
int visibleCounter = 0;
int levelLimit = UNDEFINED;
for (ForumEntry fEntry : forumEntries) {
if (levelLimit >= 0) {
if (fEntry.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (sEntry != null && sEntry.equals(fEntry)) {
return visibleCounter;
} else if (!fEntry.isShowingDescendants()) {
levelLimit = fEntry.getLevel();
}
visibleCounter++;
@Override
public void onExternalEntryAdded(ForumPostHeader header) {
forumController.loadPost(header, new UiResultHandler<ForumEntry>(this) {
@Override
public void onResultUi(final ForumEntry result) {
onForumEntryAdded(result, false);
}
return sEntry == null ? visibleCounter : NO_POSITION;
}
});
@Override
public int getItemCount() {
return getVisiblePos(null);
}
}
}
package org.briarproject.android.forum;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.ResultHandler;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.forum.ForumPost;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
......@@ -13,12 +16,14 @@ import java.util.List;
public interface ForumController extends ActivityLifecycleController {
void loadForum(GroupId groupId, ResultHandler<Boolean> resultHandler);
void loadForum(GroupId groupId,
ResultHandler<List<ForumEntry>> resultHandler);
@Nullable
Forum getForum();
List<ForumEntry> getForumEntries();
void loadPost(ForumPostHeader header,
ResultHandler<ForumEntry> resultHandler);
void unsubscribe(ResultHandler<Boolean> resultHandler);
......@@ -26,14 +31,16 @@ public interface ForumController extends ActivityLifecycleController {
void entriesRead(Collection<ForumEntry> messageIds);
void createPost(byte[] body);
void createPost(byte[] body, ResultHandler<ForumPost> resultHandler);
void createPost(byte[] body, MessageId parentId);
void createPost(byte[] body, MessageId parentId,
ResultHandler<ForumPost> resultHandler);
interface ForumPostListener {
void addLocalEntry(int index, ForumEntry entry);
void storePost(ForumPost post, ResultHandler<ForumEntry> resultHandler);
void addForeignEntry(int index, ForumEntry entry);
interface ForumPostListener {
@UiThread
void onExternalEntryAdded(ForumPostHeader header);
}
}
......@@ -6,7 +6,6 @@ import android.support.annotation.Nullable;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultHandler;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.MessageTree;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.crypto.KeyParser;
......@@ -26,7 +25,6 @@ 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.clients.MessageTreeImpl;
import org.briarproject.util.StringUtils;
import java.security.GeneralSecurityException;
......@@ -35,9 +33,9 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import javax.inject.Inject;
......@@ -69,12 +67,9 @@ public class ForumControllerImpl extends DbControllerImpl
protected volatile IdentityManager identityManager;
private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
private final MessageTree<ForumPostHeader> tree = new MessageTreeImpl<>();
private volatile AtomicLong newestTimeStamp = new AtomicLong();
private volatile LocalAuthor localAuthor = null;
private volatile Forum forum = null;
// FIXME: This collection isn't thread-safe, isn't updated atomically
private volatile List<ForumEntry> forumEntries = null;
private ForumPostListener listener;
......@@ -111,13 +106,21 @@ public class ForumControllerImpl extends DbControllerImpl
@Override
public void eventOccurred(Event e) {
if (forum == null) return;
if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
if (pe.getGroupId().equals(forum.getId())) {
LOG.info("Forum Post received, adding...");
// FIXME: Don't make blocking calls in event handlers
addNewPost(pe.getForumPostHeader());
final ForumPostHeader fph = pe.getForumPostHeader();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (this) {
if (fph.getTimestamp() > newestTimeStamp.get())
newestTimeStamp.set(fph.getTimestamp());
}
listener.onExternalEntryAdded(fph);
}
});
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e;
......@@ -133,46 +136,37 @@ public class ForumControllerImpl extends DbControllerImpl
}
}
private void addNewPost(final ForumPostHeader h) {
if (forum == null) return;
runOnDbThread(new Runnable() {
@Override
public void run() {
if (!bodyCache.containsKey(h.getId())) {
try {
byte[] body = forumManager.getPostBody(h.getId());
bodyCache.put(h.getId(), body);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return;
}
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
private void loadForum(GroupId groupId) throws DbException {
// Get Forum
long now = System.currentTimeMillis();
forum = forumManager.getForum(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading forum took " + duration +
" ms");
tree.add(h);
forumEntries = null;
// FIXME we should not need to calculate the index here
// the index is essentially stored in two different locations
int i = 0;
for (ForumEntry entry : getForumEntries()) {
if (entry.getMessageId().equals(h.getId())) {
if (localAuthor != null && localAuthor.equals(h.getAuthor())) {
addLocalEntry(i, entry);
} else {
addForeignEntry(i, entry);
}
}
i++;
}
}
});
// Get First Identity
now = System.currentTimeMillis();
localAuthor =
identityManager.getLocalAuthors().iterator()
.next();
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading author took " + duration +
" ms");
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
private void loadPosts() throws DbException {
private Collection<ForumPostHeader> loadHeaders() throws DbException {
if (forum == null)
throw new RuntimeException("Forum has not been initialized");
......@@ -180,59 +174,71 @@ public class ForumControllerImpl extends DbControllerImpl
long now = System.currentTimeMillis();
Collection<ForumPostHeader> headers =
forumManager.getPostHeaders(forum.getId());
tree.add(headers);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading headers took " + duration + " ms");
return headers;
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
private void loadBodies(Collection<ForumPostHeader> headers)
throws DbException {
// Get Bodies
now = System.currentTimeMillis();
long now = System.currentTimeMillis();
for (ForumPostHeader header : headers) {
if (!bodyCache.containsKey(header.getId())) {
byte[] body = forumManager.getPostBody(header.getId());
bodyCache.put(header.getId(), body);
}
}
duration = System.currentTimeMillis() - now;
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading bodies took " + duration + " ms");
}
private List<ForumEntry> buildForumEntries(
Collection<ForumPostHeader> headers) {
List<ForumEntry> entries = new ArrayList<>();
for (ForumPostHeader h : headers) {
byte[] body = bodyCache.get(h.getId());
entries.add(new ForumEntry(h, StringUtils.fromUtf8(body)));
}
return entries;
}
private synchronized void checkNewestTimeStamp(
Collection<ForumPostHeader> headers) {
for (ForumPostHeader h : headers) {
if (h.getTimestamp() > newestTimeStamp.get())
newestTimeStamp.set(h.getTimestamp());
}
}
@Override
public void loadForum(final GroupId groupId,
final ResultHandler<Boolean> resultHandler) {
final ResultHandler<List<ForumEntry>> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
LOG.info("Loading forum...");
if (LOG.isLoggable(INFO))
LOG.info("Loading forum...");
try {
if (forum == null) {
// Get Forum
long now = System.currentTimeMillis();
forum = forumManager.getForum(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading forum took " + duration +
" ms");
// Get First Identity
now = System.currentTimeMillis();
localAuthor =
identityManager.getLocalAuthors().iterator()
.next();
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading author took " + duration +
" ms");
// Get Forum Posts and Bodies
loadPosts();
loadForum(groupId);
}
resultHandler.onResult(true);
// Get Forum Posts and Bodies
Collection<ForumPostHeader> headers = loadHeaders();
checkNewestTimeStamp(headers);
loadBodies(headers);
resultHandler.onResult(buildForumEntries(headers));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false);
resultHandler.onResult(null);
}
}
});
......@@ -245,31 +251,21 @@ public class ForumControllerImpl extends DbControllerImpl
}
@Override
public List<ForumEntry> getForumEntries() {
if (forumEntries != null) {
return forumEntries;
}
Collection<ForumPostHeader> headers = getHeaders();
List<ForumEntry> entries = new ArrayList<>();
Stack<MessageId> 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()));
public void loadPost(final ForumPostHeader header,
final ResultHandler<ForumEntry> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
LOG.info("Loading post...");
try {
loadBodies(Collections.singletonList(header));
resultHandler.onResult(new ForumEntry(header, StringUtils
.fromUtf8(bodyCache.get(header.getId()))));
} catch (DbException e) {
e.printStackTrace();
}
}
byte[] body = bodyCache.get(h.getId());
entries.add(new ForumEntry(h, StringUtils.fromUtf8(body),
idStack.size()));
}
forumEntries = entries;
return entries;
});
}
@Override
......@@ -307,7 +303,7 @@ public class ForumControllerImpl extends DbControllerImpl
try {
long now = System.currentTimeMillis();
for (ForumEntry fe : forumEntries) {
forumManager.setReadFlag(fe.getMessageId(), true);
forumManager.setReadFlag(fe.getId(), true);
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
......@@ -321,77 +317,67 @@ public class ForumControllerImpl extends DbControllerImpl
}
@Override
public void createPost(byte[] body) {
createPost(body, null);
public void createPost(byte[] body,
ResultHandler<ForumPost> resultHandler) {
createPost(body, null, resultHandler);
}
@Override
public void createPost(final byte[] body, final MessageId parentId) {
public void createPost(final byte[] body, final MessageId parentId,
final ResultHandler<ForumPost> resultHandler) {
cryptoExecutor.execute(new Runnable() {
@Override
public void run() {
if (LOG.isLoggable(INFO))
LOG.info("create post..");
long timestamp = System.currentTimeMillis();
long newestTimeStamp = 0;
Collection<ForumPostHeader> headers = getHeaders();
if (headers != null) {
for (ForumPostHeader h : headers) {
if (h.getTimestamp() > newestTimeStamp)
newestTimeStamp = h.getTimestamp();
}
}
// Don't use an earlier timestamp than the newest post
if (timestamp < newestTimeStamp) {
timestamp = newestTimeStamp;
}
// FIXME next two lines Synchronized ?
// Only reading the atomic value, and even if it is changed
// between the first and second get, the condition will hold
if (timestamp < newestTimeStamp.get())
timestamp = newestTimeStamp.get();
ForumPost p;
try {
KeyParser keyParser = crypto.getSignatureKeyParser();
byte[] b = localAuthor.getPrivateKey();
PrivateKey authorKey = keyParser.parsePrivateKey(b);
p = forumPostFactory.createPseudonymousPost(
forum.getId(), timestamp, parentId,
localAuthor, "text/plain", body,
authorKey);
forum.getId(), timestamp, parentId, localAuthor,
"text/plain", body, authorKey);
} catch (GeneralSecurityException | FormatException e) {
throw new RuntimeException(e);
}
bodyCache.put(p.getMessage().getId(), body);
storePost(p);
// FIXME: Don't make DB calls on the crypto executor
addNewPost(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);
resultHandler.onResult(p);
}
});
}
private void storePost(final ForumPost p) {
public void storePost(final ForumPost p,
final ResultHandler<ForumEntry> resultHandler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
if (LOG.isLoggable(INFO))
LOG.info("Store post...");
long now = System.currentTimeMillis();
forumManager.addLocalPost(p);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info(
"Storing message took " + duration + " ms");
ForumPostHeader h =
new ForumPostHeader(p.getMessage().getId(),
p.getParent(),
p.getMessage().getTimestamp(),
p.getAuthor(), VERIFIED,
true);
resultHandler.onResult(new ForumEntry(h, StringUtils
.fromUtf8(bodyCache.get(p.getMessage().getId()))));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
......@@ -400,16 +386,4 @@ public class ForumControllerImpl extends DbControllerImpl
});
}
private void addNewPost(final ForumPost p) {
ForumPostHeader h =
new ForumPostHeader(p.getMessage().getId(), p.getParent(),
p.getMessage().getTimestamp(), p.getAuthor(), VERIFIED,
false);
addNewPost(h);
}
private Collection<ForumPostHeader> getHeaders() {
return tree.depthFirstOrder();
}
}
package org.briarproject.android.forum;
import org.briarproject.api.clients.MessageTree;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.Author.Status;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.sync.MessageId;
public class ForumEntry {
public class ForumEntry implements MessageTree.MessageNode {
public final static int LEVEL_UNDEFINED = -1;
private final MessageId messageId;
private final MessageId parentId;
private final String text;
private final int level;
private final long timestamp;
private final Author author;
private Status status;
private int level = LEVEL_UNDEFINED;
private boolean isShowingDescendants = true;
private boolean isRead = true;
ForumEntry(ForumPostHeader h, String text, int level) {
this(h.getId(), text, level, h.getTimestamp(), h.getAuthor(),
ForumEntry(ForumPostHeader h, String text) {
this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
h.getAuthorStatus());
this.isRead = h.isRead();
}
public ForumEntry(MessageId messageId, String text, int level,
public ForumEntry(MessageId messageId, MessageId parentId, String text,
long timestamp, Author author, Status status) {
this.messageId = messageId;
this.parentId = parentId;
this.text = text;
this.level = level;
this.timestamp = timestamp;
this.author = author;
this.status = status;
......@@ -41,6 +44,16 @@ public class ForumEntry {
return level;
}
@Override
public MessageId getId() {
return messageId;
}
@Override
public MessageId getParentId() {
return parentId;
}
public long getTimestamp() {
return timestamp;
}
......@@ -57,6 +70,10 @@ public class ForumEntry {
return isShowingDescendants;
}
void setLevel(int level) {
this.level = level;
}
void setShowingDescendants(boolean showingDescendants) {
this.isShowingDescendants = showingDescendants;
}
......
package org.briarproject.android.forum;
import org.briarproject.android.controller.handler.ResultHandler;
import org.briarproject.api.UniqueId;
import org.briarproject.api.forum.Forum;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.AuthorFactory;
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;
import static org.briarproject.api.identity.Author.Status.UNVERIFIED;
public class ForumTestControllerImpl implements ForumController {
@Inject
AuthorFactory authorFactory;
private static final Logger LOG =
Logger.getLogger(ForumControllerImpl.class.getName());
private final Author[] AUTHORS = {
authorFactory.createAuthor("Guðmundur", new byte[42]),
authorFactory.createAuthor("Jónas", new byte[42]),
authorFactory.createAuthor(
"Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
new byte[42]),
authorFactory.createAuthor("Baldur Friðrik", new byte[42]),
authorFactory.createAuthor("Anna Katrín", new byte[42]),
authorFactory.createAuthor("Þór", new byte[42]),
authorFactory.createAuthor("Anna Þorbjörg", new byte[42]),
authorFactory.createAuthor("Guðrún", new byte[42]),
authorFactory.createAuthor("Helga", new byte[42]),
authorFactory.createAuthor("Haraldur", new byte[42])
};
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
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,
ResultHandler<Boolean> 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], UNVERIFIED);
}
LOG.info("forum entries: " + forumEntries.length);
resultHandler.onResult(true);
}
@Override
public Forum getForum() {
return null;
}
@Override
public List<ForumEntry> getForumEntries() {
return forumEntries == null ? null :
new ArrayList<>(Arrays.asList(forumEntries));
}
@Override
public void unsubscribe(ResultHandler<Boolean> resultHandler) {
}
@Override
public void entryRead(ForumEntry forumEntry) {
}
@Override
public void entriesRead(Collection<ForumEntry> 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() {
}
}
package org.briarproject.android.forum;
import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.util.NestedTreeList;
import org.briarproject.android.view.AuthorView;
import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
public class NestedForumAdapter
extends RecyclerView.Adapter<NestedForumAdapter.NestedForumHolder> {
private static final int UNDEFINED = -1;
private final NestedTreeList<ForumEntry> forumEntries =
new NestedTreeList<>();
private final Map<ForumEntry, ValueAnimator> animatingEntries =
new HashMap<>();
// highlight not dependant on time
private ForumEntry replyEntry;
// temporary highlight
private ForumEntry addedEntry;
private final Context ctx;
private final OnNestedForumListener listener;
private final LinearLayoutManager layoutManager;
public NestedForumAdapter(Context ctx, OnNestedForumListener listener,
LinearLayoutManager layoutManager) {
this.ctx = ctx;
this.listener = listener;
this.layoutManager = layoutManager;
}
ForumEntry getReplyEntry() {
return replyEntry;
}
private void setForumEntryLevels() {
Stack<MessageId> idStack = new Stack<>();
for (ForumEntry forumEntry : forumEntries) {
if (forumEntry.getParentId() == null) {
idStack.clear();
} else if (idStack.isEmpty() ||
!idStack.contains(forumEntry.getParentId())) {
idStack.push(forumEntry.getParentId());
} else if (!forumEntry.getParentId().equals(idStack.peek())) {
do {
idStack.pop();
} while (!forumEntry.getParentId().equals(idStack.peek()));
}
forumEntry.setLevel(idStack.size());
}
}
void setEntries(List<ForumEntry> entries) {
forumEntries.clear();
forumEntries.addAll(entries);
setForumEntryLevels();
notifyItemRangeInserted(0, entries.size());
}
void addEntry(ForumEntry entry) {
boolean isShowingDescendants = false;
forumEntries.add(entry);
setForumEntryLevels();
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 = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
ForumEntry higherEntry = forumEntries.get(i);
if (higherEntry.getLevel() < entry.getLevel()) {
// parent found
if (!higherEntry.isShowingDescendants()) {
isShowingDescendants = true;
showDescendants(higherEntry);
}
notifyItemChanged(getVisiblePos(higherEntry));
break;
}
}
}
if (!isShowingDescendants) {
int visiblePos = getVisiblePos(entry);
notifyItemInserted(visiblePos);
}
addedEntry = entry;
}
void scrollToEntry(ForumEntry entry) {
layoutManager
.scrollToPositionWithOffset(getVisiblePos(entry), 0);
}
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(ForumEntry forumEntry) {
int visiblePos = getVisiblePos(forumEntry);
int levelLimit = forumEntry.getLevel();
if (visiblePos + 1 < getItemCount()) {
ForumEntry entry = getVisibleEntry(visiblePos + 1);
if (entry == null || entry.getLevel() > levelLimit)
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;
}
void setReplyEntryById(byte[] id) {
MessageId messageId = new MessageId(id);
for (ForumEntry entry : forumEntries) {
if (entry.getId().equals(messageId)) {
setReplyEntry(entry);
break;
}
}
}
void setReplyEntry(ForumEntry entry) {
if (replyEntry != null) {
notifyItemChanged(getVisiblePos(replyEntry));
}
replyEntry = entry;
if (replyEntry != null) {
notifyItemChanged(getVisiblePos(replyEntry));
}
}
private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
List<Integer> indexList = new ArrayList<>();
for (int i = pos + 1; i < getItemCount(); i++) {
ForumEntry entry = getVisibleEntry(i);
if (entry != null && entry.getLevel() > levelLimit) {
indexList.add(i);
} else {
break;
}
}
return indexList;
}
void showDescendants(ForumEntry forumEntry) {
forumEntry.setShowingDescendants(true);
int visiblePos = getVisiblePos(forumEntry);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
if (!indexList.isEmpty()) {
if (indexList.size() == 1) {
notifyItemInserted(indexList.get(0));
} else {
notifyItemRangeInserted(indexList.get(0),
indexList.size());
}
}
}
void hideDescendants(ForumEntry forumEntry) {
int visiblePos = getVisiblePos(forumEntry);
List<Integer> indexList =
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
if (!indexList.isEmpty()) {
if (Build.VERSION.SDK_INT >= 11) {
// stop animating children
for (int index : indexList) {
ValueAnimator anim =
animatingEntries.get(forumEntries.get(index));
if (anim != null && anim.isRunning()) {
anim.cancel();
}
}
}
if (indexList.size() == 1) {
notifyItemRemoved(indexList.get(0));
} else {
notifyItemRangeRemoved(indexList.get(0),
indexList.size());
}
}
forumEntry.setShowingDescendants(false);
}
@Nullable
ForumEntry getVisibleEntry(int position) {
int levelLimit = UNDEFINED;
for (ForumEntry forumEntry : forumEntries) {
if (levelLimit >= 0) {
if (forumEntry.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (!forumEntry.isShowingDescendants()) {
levelLimit = forumEntry.getLevel();
}
if (position-- == 0) {
return forumEntry;
}
}
return null;
}
@TargetApi(11)
private void animateFadeOut(final NestedForumHolder ui,
final ForumEntry addedEntry) {
ui.setIsRecyclable(false);
ValueAnimator anim = new ValueAnimator();
animatingEntries.put(addedEntry, anim);
ColorDrawable viewColor = (ColorDrawable) ui.cell.getBackground();
anim.setIntValues(viewColor.getColor(), ContextCompat
.getColor(ctx, R.color.window_background));
anim.setEvaluator(new ArgbEvaluator());
anim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
ui.setIsRecyclable(true);
animatingEntries.remove(addedEntry);
}
@Override
public void onAnimationCancel(Animator animation) {
ui.setIsRecyclable(true);
animatingEntries.remove(addedEntry);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
ui.cell.setBackgroundColor(
(Integer) valueAnimator.getAnimatedValue());
}
});
anim.setDuration(5000);
anim.start();
}
@Override
public NestedForumHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_forum_post, parent, false);
return new NestedForumHolder(v);
}
@Override
public void onBindViewHolder(
final NestedForumHolder ui, final int position) {
final ForumEntry entry = getVisibleEntry(position);
if (entry == null) return;
listener.onEntryVisible(entry);
ui.textView.setText(StringUtils.trim(entry.getText()));
if (position == 0) {
ui.topDivider.setVisibility(View.INVISIBLE);
} else {
ui.topDivider.setVisibility(View.VISIBLE);
}
for (int i = 0; i < ui.lvls.length; i++) {
ui.lvls[i].setVisibility(i < entry.getLevel() ? VISIBLE : GONE);
}
if (entry.getLevel() > 5) {
ui.lvlText.setVisibility(VISIBLE);
ui.lvlText.setText("" + entry.getLevel());
} else {
ui.lvlText.setVisibility(GONE);
}
ui.author.setAuthor(entry.getAuthor());
ui.author.setDate(entry.getTimestamp());
ui.author.setAuthorStatus(entry.getStatus());
int replies = getReplyCount(entry);
if (replies == 0) {
ui.repliesText.setText("");
} else {
ui.repliesText.setText(
ctx.getResources()
.getQuantityString(R.plurals.message_replies,
replies, replies));
}
if (hasDescendants(entry)) {
ui.chevron.setVisibility(VISIBLE);
if (hasVisibleDescendants(entry)) {
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(entry);
} else {
showDescendants(entry);
}
}
});
} else {
ui.chevron.setVisibility(INVISIBLE);
}
if (entry.equals(replyEntry)) {
ui.cell.setBackgroundColor(ContextCompat
.getColor(ctx, R.color.forum_cell_highlight));
} else if (entry.equals(addedEntry)) {
ui.cell.setBackgroundColor(ContextCompat
.getColor(ctx, R.color.forum_cell_highlight));
if (Build.VERSION.SDK_INT >= 11) {
animateFadeOut(ui, addedEntry);
}
addedEntry = null;
} else {
ui.cell.setBackgroundColor(ContextCompat
.getColor(ctx, R.color.window_background));
}
ui.replyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onReplyClick(entry);
scrollToEntry(entry);
}
});
}
private int getVisiblePos(ForumEntry sEntry) {
int visibleCounter = 0;
int levelLimit = UNDEFINED;
for (ForumEntry fEntry : forumEntries) {
if (levelLimit >= 0) {
if (fEntry.getLevel() > levelLimit) {
continue;
}
levelLimit = UNDEFINED;
}
if (sEntry != null && sEntry.equals(fEntry)) {
return visibleCounter;
} else if (!fEntry.isShowingDescendants()) {
levelLimit = fEntry.getLevel();
}
visibleCounter++;
}
return sEntry == null ? visibleCounter : NO_POSITION;
}
@Override
public int getItemCount() {
return getVisiblePos(null);
}
static class NestedForumHolder extends RecyclerView.ViewHolder {
final TextView textView, lvlText, repliesText;
final AuthorView author;
final View[] lvls;
final View chevron, replyButton;
final ViewGroup cell;
final View topDivider;
NestedForumHolder(View v) {
super(v);
textView = (TextView) v.findViewById(R.id.text);
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
author = (AuthorView) v.findViewById(R.id.author);
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]);
}
chevron = v.findViewById(R.id.chevron);
replyButton = v.findViewById(R.id.btn_reply);
cell = (ViewGroup) v.findViewById(R.id.forum_cell);
topDivider = v.findViewById(R.id.top_divider);
}
}
interface OnNestedForumListener {
void onEntryVisible(ForumEntry forumEntry);
void onReplyClick(ForumEntry forumEntry);
}
}
package org.briarproject.android.util;
import org.briarproject.api.clients.MessageTree;
import org.briarproject.clients.MessageTreeImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/* This class is not thread safe */
public class NestedTreeList<T extends MessageTree.MessageNode>
implements Iterable<T> {
private final MessageTree<T> tree = new MessageTreeImpl<>();
private List<T> depthFirstCollection = new ArrayList<>();
public void addAll(Collection<T> collection) {
tree.add(collection);
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
}
public void add(T elem) {
tree.add(elem);
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
}
public void clear() {
tree.clear();
depthFirstCollection.clear();
}
public T get(int index) {
return depthFirstCollection.get(index);
}
public int indexOf(T elem) {
return depthFirstCollection.indexOf(elem);
}
public int size() {
return depthFirstCollection.size();
}
@Override
public Iterator<T> iterator() {
return depthFirstCollection.iterator();
}
}
......@@ -50,6 +50,22 @@ public class ForumActivityTest {
AUTHOR_1, AUTHOR_2, AUTHOR_3, AUTHOR_4, AUTHOR_5, AUTHOR_6
};
private final static MessageId[] AUTHOR_IDS = new MessageId[AUTHORS.length];
static {
for (int i = 0; i < AUTHOR_IDS.length; i++)
AUTHOR_IDS[i] = new MessageId(TestUtils.getRandomId());
}
private final static MessageId[] PARENT_AUTHOR_IDS = {
null,
AUTHOR_IDS[0],
AUTHOR_IDS[1],
AUTHOR_IDS[2],
AUTHOR_IDS[0],
null
};
/*
1
-> 2
......@@ -64,7 +80,7 @@ public class ForumActivityTest {
private TestForumActivity forumActivity;
@Captor
private ArgumentCaptor<UiResultHandler<Boolean>> rc;
private ArgumentCaptor<UiResultHandler<List<ForumEntry>>> rc;
@Before
public void setUp() {
......@@ -82,9 +98,11 @@ public class ForumActivityTest {
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
Author author = new Author(authorId, AUTHORS[i], publicKey);
forumEntries[i] = new ForumEntry(
new MessageId(TestUtils.getRandomId()), AUTHORS[i],
LEVELS[i], System.currentTimeMillis(), author, UNKNOWN);
forumEntries[i] =
new ForumEntry(AUTHOR_IDS[i], PARENT_AUTHOR_IDS[i],
AUTHORS[i], System.currentTimeMillis(), author,
UNKNOWN);
forumEntries[i].setLevel(LEVELS[i]);
}
return new ArrayList<>(Arrays.asList(forumEntries));
}
......@@ -93,13 +111,10 @@ public class ForumActivityTest {
public void testNestedEntries() {
ForumController mc = forumActivity.getController();
List<ForumEntry> 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();
rc.getValue().onResult(dummyData);
NestedForumAdapter adapter = forumActivity.getAdapter();
Assert.assertNotNull(adapter);
// Cascade close
assertEquals(6, adapter.getItemCount());
......
......@@ -15,7 +15,7 @@ public class TestForumActivity extends ForumActivity {
return forumController;
}
public ForumAdapter getAdapter() {
public NestedForumAdapter getAdapter() {
return forumAdapter;
}
......
package org.briarproject.api.forum;
import org.briarproject.api.clients.MessageTree;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.MessageId;
public class ForumPostHeader extends PostHeader
implements MessageTree.MessageNode {
public class ForumPostHeader extends PostHeader {
public ForumPostHeader(MessageId id, MessageId parentId, long timestamp,
Author author, Author.Status authorStatus, boolean read) {
......
......@@ -26,13 +26,13 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
};
@Override
public void clear() {
public synchronized void clear() {
roots.clear();
nodeMap.clear();
}
@Override
public void add(Collection<T> nodes) {
public synchronized void add(Collection<T> nodes) {
// add all nodes to the node map
for (T node : nodes) {
nodeMap.put(node.getId(), new ArrayList<T>());
......@@ -45,7 +45,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
}
@Override
public void add(T node) {
public synchronized void add(T node) {
add(Collections.singletonList(node));
}
......@@ -85,7 +85,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
}
@Override
public void setComparator(Comparator<T> comparator) {
public synchronized void setComparator(Comparator<T> comparator) {
this.comparator = comparator;
// Sort all lists with the new comparator
Collections.sort(roots, comparator);
......@@ -95,7 +95,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
}
@Override
public Collection<T> depthFirstOrder() {
public synchronized Collection<T> depthFirstOrder() {
List<T> orderedList = new ArrayList<T>();
for (T root : roots) {
traverse(orderedList, root);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment