mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Merge branch '556-thread-safety-blocking-issues' into 'master'
Forum controller thread safety and tree safety This branch solves the concurrent forum issues by code restructure and refactoring. Closes #556 Closes #552 See merge request !262
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -1,85 +1,70 @@
|
||||
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;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.android.forum.ForumController.ForumPostListener;
|
||||
import org.briarproject.android.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.db.DbException;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import 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;
|
||||
private LinearLayoutManager linearLayoutManager;
|
||||
|
||||
private volatile GroupId groupId = null;
|
||||
|
||||
@@ -96,42 +81,44 @@ 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 UiResultExceptionHandler<List<ForumEntry>, DbException>(
|
||||
this) {
|
||||
@Override
|
||||
public void onResultUi(List<ForumEntry> result) {
|
||||
Forum forum = forumController.getForum();
|
||||
if (forum != null) setTitle(forum.getName());
|
||||
List<ForumEntry> entries = new ArrayList<>(result);
|
||||
if (entries.isEmpty()) {
|
||||
recyclerView.showData();
|
||||
} else {
|
||||
forumAdapter.setEntries(entries);
|
||||
if (state != null) {
|
||||
byte[] replyId =
|
||||
state.getByteArray(KEY_REPLY_ID);
|
||||
if (replyId != null)
|
||||
forumAdapter.setReplyEntryById(replyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO Maybe an error dialog ?
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO Improve UX ?
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,14 +250,28 @@ public class ForumActivity extends BriarActivity implements
|
||||
public void onSendClick(String text) {
|
||||
if (text.trim().length() == 0)
|
||||
return;
|
||||
if (forumController.getForum() == null) return;
|
||||
ForumEntry replyEntry = forumAdapter.getReplyEntry();
|
||||
UiResultExceptionHandler<ForumEntry, DbException> resultHandler =
|
||||
new UiResultExceptionHandler<ForumEntry, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(ForumEntry result) {
|
||||
onForumEntryAdded(result, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO Improve UX ?
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
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 +309,56 @@ 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 && forumAdapter.isVisible(entry)) {
|
||||
displaySnackbarShort(R.string.forum_new_entry_posted);
|
||||
} else {
|
||||
Snackbar snackbar = Snackbar.make(recyclerView,
|
||||
isLocal ? R.string.forum_new_entry_posted :
|
||||
R.string.forum_new_entry_received,
|
||||
Snackbar.LENGTH_LONG);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
snackbar.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExternalEntryAdded(ForumPostHeader header) {
|
||||
forumController.loadPost(header,
|
||||
new UiResultExceptionHandler<ForumEntry, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(final ForumEntry result) {
|
||||
onForumEntryAdded(result, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
// TODO add proper exception handling
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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.ResultExceptionHandler;
|
||||
import org.briarproject.android.controller.handler.ResultHandler;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
@@ -13,12 +17,14 @@ import java.util.List;
|
||||
|
||||
public interface ForumController extends ActivityLifecycleController {
|
||||
|
||||
void loadForum(GroupId groupId, ResultHandler<Boolean> resultHandler);
|
||||
void loadForum(GroupId groupId,
|
||||
ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler);
|
||||
|
||||
@Nullable
|
||||
Forum getForum();
|
||||
|
||||
List<ForumEntry> getForumEntries();
|
||||
void loadPost(ForumPostHeader header,
|
||||
ResultExceptionHandler<ForumEntry, DbException> resultHandler);
|
||||
|
||||
void unsubscribe(ResultHandler<Boolean> resultHandler);
|
||||
|
||||
@@ -26,14 +32,15 @@ public interface ForumController extends ActivityLifecycleController {
|
||||
|
||||
void entriesRead(Collection<ForumEntry> messageIds);
|
||||
|
||||
void createPost(byte[] body);
|
||||
void createPost(byte[] body,
|
||||
ResultExceptionHandler<ForumEntry, DbException> resultHandler);
|
||||
|
||||
void createPost(byte[] body, MessageId parentId);
|
||||
void createPost(byte[] body, MessageId parentId,
|
||||
ResultExceptionHandler<ForumEntry, DbException> resultHandler);
|
||||
|
||||
interface ForumPostListener {
|
||||
void addLocalEntry(int index, ForumEntry entry);
|
||||
|
||||
void addForeignEntry(int index, ForumEntry entry);
|
||||
@UiThread
|
||||
void onExternalEntryAdded(ForumPostHeader header);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.app.Activity;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
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 +26,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,16 +34,16 @@ 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;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.identity.Author.Status.VERIFIED;
|
||||
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
||||
|
||||
public class ForumControllerImpl extends DbControllerImpl
|
||||
implements ForumController, EventListener {
|
||||
@@ -69,12 +68,10 @@ 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 final 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 +108,18 @@ 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());
|
||||
LOG.info("Forum post received, adding...");
|
||||
final ForumPostHeader fph = pe.getForumPostHeader();
|
||||
updateNewestTimestamp(fph.getTimestamp());
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onExternalEntryAdded(fph);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||
@@ -133,46 +135,33 @@ 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.getLocalAuthor();
|
||||
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 +169,68 @@ 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 void updateNewestTimeStamp(Collection<ForumPostHeader> headers) {
|
||||
for (ForumPostHeader h : headers) {
|
||||
updateNewestTimestamp(h.getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadForum(final GroupId groupId,
|
||||
final ResultHandler<Boolean> resultHandler) {
|
||||
final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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();
|
||||
updateNewestTimeStamp(headers);
|
||||
loadBodies(headers);
|
||||
resultHandler.onResult(buildForumEntries(headers));
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
resultHandler.onResult(false);
|
||||
resultHandler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -245,31 +243,23 @@ 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 ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Loading post...");
|
||||
try {
|
||||
loadBodies(Collections.singletonList(header));
|
||||
resultHandler.onResult(new ForumEntry(header, StringUtils
|
||||
.fromUtf8(bodyCache.get(header.getId()))));
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
resultHandler.onException(e);
|
||||
}
|
||||
}
|
||||
byte[] body = bodyCache.get(h.getId());
|
||||
entries.add(new ForumEntry(h, StringUtils.fromUtf8(body),
|
||||
idStack.size()));
|
||||
}
|
||||
forumEntries = entries;
|
||||
return entries;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -307,7 +297,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,95 +311,73 @@ public class ForumControllerImpl extends DbControllerImpl
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPost(byte[] body) {
|
||||
createPost(body, null);
|
||||
public void createPost(byte[] body,
|
||||
ResultExceptionHandler<ForumEntry, DbException> 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 ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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;
|
||||
}
|
||||
timestamp = Math.max(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);
|
||||
storePost(p, resultHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
private void storePost(final ForumPost p,
|
||||
final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
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");
|
||||
LOG.info("Storing message took " + duration + " ms");
|
||||
|
||||
ForumPostHeader h =
|
||||
new ForumPostHeader(p.getMessage().getId(),
|
||||
p.getParent(),
|
||||
p.getMessage().getTimestamp(),
|
||||
p.getAuthor(), OURSELVES, 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);
|
||||
resultHandler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 void updateNewestTimestamp(long update) {
|
||||
long newest = newestTimeStamp.get();
|
||||
while (newest < update) {
|
||||
if (newestTimeStamp.compareAndSet(newest, update)) return;
|
||||
newest = newestTimeStamp.get();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<ForumPostHeader> getHeaders() {
|
||||
return tree.depthFirstOrder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
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 {
|
||||
/* This class is not thread safe */
|
||||
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 int descendantCount = 0;
|
||||
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 +46,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 +72,10 @@ public class ForumEntry {
|
||||
return isShowingDescendants;
|
||||
}
|
||||
|
||||
public void setLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
void setShowingDescendants(boolean showingDescendants) {
|
||||
this.isShowingDescendants = showingDescendants;
|
||||
}
|
||||
@@ -72,4 +91,12 @@ public class ForumEntry {
|
||||
void setRead(boolean read) {
|
||||
isRead = read;
|
||||
}
|
||||
|
||||
public boolean hasDescendants() {
|
||||
return descendantCount > 0;
|
||||
}
|
||||
|
||||
public void setDescendantCount(int descendantCount) {
|
||||
this.descendantCount = descendantCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
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 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;
|
||||
}
|
||||
|
||||
void setEntries(List<ForumEntry> entries) {
|
||||
forumEntries.clear();
|
||||
forumEntries.addAll(entries);
|
||||
notifyItemRangeInserted(0, entries.size());
|
||||
}
|
||||
|
||||
void addEntry(ForumEntry entry) {
|
||||
forumEntries.add(entry);
|
||||
addedEntry = entry;
|
||||
if (entry.getParentId() == null) {
|
||||
notifyItemInserted(getVisiblePos(entry));
|
||||
} else {
|
||||
// Try to find the entry's parent and perform the proper ui update if
|
||||
// it's present and 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()) {
|
||||
int parentVisiblePos = getVisiblePos(higherEntry);
|
||||
if (parentVisiblePos != NO_POSITION) {
|
||||
// parent is visible, we need to update its ui
|
||||
notifyItemChanged(parentVisiblePos);
|
||||
// new entry insert ui
|
||||
int visiblePos = getVisiblePos(entry);
|
||||
notifyItemInserted(visiblePos);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// do not show the new entry if its parent is not showing
|
||||
// descendants (this can be overridden by the user by
|
||||
// pressing the snack bar)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToEntry(ForumEntry entry) {
|
||||
int visiblePos = getVisiblePos(entry);
|
||||
if (visiblePos == NO_POSITION && entry.getParentId() != null) {
|
||||
// The entry is not visible due to being hidden by its parent entry.
|
||||
// Find the parent and make it visible and traverse up the parent
|
||||
// chain if necessary to make the entry visible
|
||||
MessageId parentId = entry.getParentId();
|
||||
for (int i = forumEntries.indexOf(entry) - 1; i >= 0; i--) {
|
||||
ForumEntry higherEntry = forumEntries.get(i);
|
||||
if (higherEntry.getId().equals(parentId)) {
|
||||
// parent found
|
||||
showDescendants(higherEntry);
|
||||
int parentPos = getVisiblePos(higherEntry);
|
||||
if (parentPos != NO_POSITION) {
|
||||
// parent or ancestor is visible, entry's visibility
|
||||
// is ensured
|
||||
notifyItemChanged(parentPos);
|
||||
visiblePos = parentPos;
|
||||
break;
|
||||
}
|
||||
// parent or ancestor is hidden, we need to continue up the
|
||||
// dependency chain
|
||||
parentId = higherEntry.getParentId();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (visiblePos != NO_POSITION)
|
||||
layoutManager.scrollToPositionWithOffset(visiblePos, 0);
|
||||
}
|
||||
|
||||
private int getReplyCount(ForumEntry entry) {
|
||||
int counter = 0;
|
||||
int pos = forumEntries.indexOf(entry);
|
||||
if (pos >= 0) {
|
||||
int ancestorLvl = entry.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()) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param position is visible entry index
|
||||
* @return the visible entry at index position from an ordered list of visible
|
||||
* entries, or null if position is larger then the number of visible entries.
|
||||
*/
|
||||
@Nullable
|
||||
ForumEntry getVisibleEntry(int position) {
|
||||
int levelLimit = UNDEFINED;
|
||||
for (ForumEntry forumEntry : forumEntries) {
|
||||
if (levelLimit >= 0) {
|
||||
// skip hidden entries that their parent is hiding
|
||||
if (forumEntry.getLevel() > levelLimit) {
|
||||
continue;
|
||||
}
|
||||
levelLimit = UNDEFINED;
|
||||
}
|
||||
if (!forumEntry.isShowingDescendants()) {
|
||||
levelLimit = forumEntry.getLevel();
|
||||
}
|
||||
if (position-- == 0) {
|
||||
return forumEntry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (entry.hasDescendants()) {
|
||||
ui.chevron.setVisibility(VISIBLE);
|
||||
if (entry.isShowingDescendants()) {
|
||||
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));
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isVisible(ForumEntry entry) {
|
||||
return getVisiblePos(entry) != NO_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sEntry the ForumEntry to find the visible positoin of, or null to
|
||||
* return the total cound of visible elements
|
||||
* @return the visible position of sEntry, or the total number of visible
|
||||
* elements if sEntry is null. If sEntry is not visible a NO_POSITION is
|
||||
* returned.
|
||||
*/
|
||||
private int getVisiblePos(ForumEntry sEntry) {
|
||||
int visibleCounter = 0;
|
||||
int levelLimit = UNDEFINED;
|
||||
for (ForumEntry fEntry : forumEntries) {
|
||||
if (levelLimit >= 0) {
|
||||
if (fEntry.getLevel() > levelLimit) {
|
||||
// skip all the entries below a non visible branch
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
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;
|
||||
|
||||
@UiThread
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import junit.framework.Assert;
|
||||
import org.briarproject.BuildConfig;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.android.TestBriarApplication;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
@@ -50,6 +51,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 +81,8 @@ public class ForumActivityTest {
|
||||
|
||||
private TestForumActivity forumActivity;
|
||||
@Captor
|
||||
private ArgumentCaptor<UiResultHandler<Boolean>> rc;
|
||||
private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>>
|
||||
rc;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -75,16 +93,17 @@ public class ForumActivityTest {
|
||||
.withIntent(intent).create().resume().get();
|
||||
}
|
||||
|
||||
|
||||
private List<ForumEntry> getDummyData() {
|
||||
ForumEntry[] forumEntries = new ForumEntry[6];
|
||||
for (int i = 0; i < forumEntries.length; i++) {
|
||||
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 +112,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
|
||||
interface MessageNode {
|
||||
MessageId getId();
|
||||
MessageId getParentId();
|
||||
void setLevel(int level);
|
||||
void setDescendantCount(int descendantCount);
|
||||
long getTimestamp();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -77,15 +77,18 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
}
|
||||
|
||||
|
||||
private void traverse(List<T> list, T node) {
|
||||
private void traverse(List<T> list, T node, int level) {
|
||||
list.add(node);
|
||||
for (T child : nodeMap.get(node.getId())) {
|
||||
traverse(list, child);
|
||||
List<T> children = nodeMap.get(node.getId());
|
||||
node.setLevel(level);
|
||||
node.setDescendantCount(children.size());
|
||||
for (T child : children) {
|
||||
traverse(list, child, level+1);
|
||||
}
|
||||
}
|
||||
|
||||
@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,10 +98,10 @@ 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);
|
||||
traverse(orderedList, root, 0);
|
||||
}
|
||||
return Collections.unmodifiableList(orderedList);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,16 @@ public class MessageTreeTest {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLevel(int level) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescendantCount(int descendantCount) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
|
||||
Reference in New Issue
Block a user