mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-18 05:39:53 +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.controller.TransportStateListener;
|
||||||
import org.briarproject.android.forum.ForumController;
|
import org.briarproject.android.forum.ForumController;
|
||||||
import org.briarproject.android.forum.ForumControllerImpl;
|
import org.briarproject.android.forum.ForumControllerImpl;
|
||||||
import org.briarproject.android.forum.ForumTestControllerImpl;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
@@ -103,14 +100,6 @@ public class ActivityModule {
|
|||||||
return forumController;
|
return forumController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Named("ForumTestController")
|
|
||||||
@ActivityScope
|
|
||||||
@Provides
|
|
||||||
protected ForumController provideForumTestController(
|
|
||||||
ForumTestControllerImpl forumController) {
|
|
||||||
return forumController;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Provides
|
@Provides
|
||||||
BlogController provideBlogController(BlogControllerImpl blogController) {
|
BlogController provideBlogController(BlogControllerImpl blogController) {
|
||||||
|
|||||||
@@ -1,85 +1,70 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.ArgbEvaluator;
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.ActivityComponent;
|
import org.briarproject.android.ActivityComponent;
|
||||||
import org.briarproject.android.BriarActivity;
|
import org.briarproject.android.BriarActivity;
|
||||||
import org.briarproject.android.api.AndroidNotificationManager;
|
import org.briarproject.android.api.AndroidNotificationManager;
|
||||||
|
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||||
import org.briarproject.android.forum.ForumController.ForumPostListener;
|
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.ShareForumActivity;
|
||||||
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
import org.briarproject.android.sharing.SharingStatusForumActivity;
|
||||||
import org.briarproject.android.view.AuthorView;
|
|
||||||
import org.briarproject.android.view.BriarRecyclerView;
|
import org.briarproject.android.view.BriarRecyclerView;
|
||||||
import org.briarproject.android.view.TextInputView;
|
import org.briarproject.android.view.TextInputView;
|
||||||
import org.briarproject.android.view.TextInputView.TextInputListener;
|
import org.briarproject.android.view.TextInputView.TextInputListener;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_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.GONE;
|
||||||
import static android.view.View.INVISIBLE;
|
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
|
|
||||||
public class ForumActivity extends BriarActivity implements
|
public class ForumActivity extends BriarActivity implements
|
||||||
ForumPostListener, TextInputListener {
|
ForumPostListener, TextInputListener, OnNestedForumListener {
|
||||||
|
|
||||||
static final String FORUM_NAME = "briar.FORUM_NAME";
|
static final String FORUM_NAME = "briar.FORUM_NAME";
|
||||||
|
|
||||||
private static final int REQUEST_FORUM_SHARED = 3;
|
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_INPUT_VISIBILITY = "inputVisibility";
|
||||||
private static final String KEY_REPLY_ID = "replyId";
|
private static final String KEY_REPLY_ID = "replyId";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AndroidNotificationManager notificationManager;
|
AndroidNotificationManager notificationManager;
|
||||||
|
|
||||||
// uncomment the next line for a test component with dummy data
|
|
||||||
// @Named("ForumTestController")
|
|
||||||
@Inject
|
@Inject
|
||||||
protected ForumController forumController;
|
protected ForumController forumController;
|
||||||
|
|
||||||
// Protected access for testing
|
// Protected access for testing
|
||||||
protected ForumAdapter forumAdapter;
|
protected NestedForumAdapter forumAdapter;
|
||||||
|
|
||||||
private BriarRecyclerView recyclerView;
|
private BriarRecyclerView recyclerView;
|
||||||
private TextInputView textInput;
|
private TextInputView textInput;
|
||||||
private LinearLayoutManager linearLayoutManager;
|
|
||||||
|
|
||||||
private volatile GroupId groupId = null;
|
private volatile GroupId groupId = null;
|
||||||
|
|
||||||
@@ -96,42 +81,44 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
String forumName = i.getStringExtra(FORUM_NAME);
|
String forumName = i.getStringExtra(FORUM_NAME);
|
||||||
if (forumName != null) setTitle(forumName);
|
if (forumName != null) setTitle(forumName);
|
||||||
|
|
||||||
forumAdapter = new ForumAdapter();
|
|
||||||
|
|
||||||
textInput = (TextInputView) findViewById(R.id.text_input_container);
|
textInput = (TextInputView) findViewById(R.id.text_input_container);
|
||||||
textInput.setVisibility(GONE);
|
textInput.setVisibility(GONE);
|
||||||
textInput.setListener(this);
|
textInput.setListener(this);
|
||||||
recyclerView =
|
recyclerView =
|
||||||
(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
|
(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
|
||||||
recyclerView.setAdapter(forumAdapter);
|
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
|
||||||
linearLayoutManager = new LinearLayoutManager(this);
|
|
||||||
recyclerView.setLayoutManager(linearLayoutManager);
|
recyclerView.setLayoutManager(linearLayoutManager);
|
||||||
|
forumAdapter = new NestedForumAdapter(this, this, linearLayoutManager);
|
||||||
|
recyclerView.setAdapter(forumAdapter);
|
||||||
recyclerView.setEmptyText(R.string.no_forum_posts);
|
recyclerView.setEmptyText(R.string.no_forum_posts);
|
||||||
|
|
||||||
forumController.loadForum(groupId, new UiResultHandler<Boolean>(this) {
|
forumController.loadForum(groupId,
|
||||||
@Override
|
new UiResultExceptionHandler<List<ForumEntry>, DbException>(
|
||||||
public void onResultUi(Boolean result) {
|
this) {
|
||||||
if (result) {
|
@Override
|
||||||
Forum forum = forumController.getForum();
|
public void onResultUi(List<ForumEntry> result) {
|
||||||
if (forum != null) setTitle(forum.getName());
|
Forum forum = forumController.getForum();
|
||||||
List<ForumEntry> entries =
|
if (forum != null) setTitle(forum.getName());
|
||||||
forumController.getForumEntries();
|
List<ForumEntry> entries = new ArrayList<>(result);
|
||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
recyclerView.showData();
|
recyclerView.showData();
|
||||||
} else {
|
} else {
|
||||||
forumAdapter.setEntries(entries);
|
forumAdapter.setEntries(entries);
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
byte[] replyId = state.getByteArray(KEY_REPLY_ID);
|
byte[] replyId =
|
||||||
if (replyId != null)
|
state.getByteArray(KEY_REPLY_ID);
|
||||||
forumAdapter.setReplyEntryById(replyId);
|
if (replyId != null)
|
||||||
|
forumAdapter.setReplyEntryById(replyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO Maybe an error dialog ?
|
@Override
|
||||||
finish();
|
public void onExceptionUi(DbException exception) {
|
||||||
}
|
// TODO Improve UX ?
|
||||||
}
|
finish();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -263,14 +250,28 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
public void onSendClick(String text) {
|
public void onSendClick(String text) {
|
||||||
if (text.trim().length() == 0)
|
if (text.trim().length() == 0)
|
||||||
return;
|
return;
|
||||||
if (forumController.getForum() == null) return;
|
|
||||||
ForumEntry replyEntry = forumAdapter.getReplyEntry();
|
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) {
|
if (replyEntry == null) {
|
||||||
// root post
|
// root post
|
||||||
forumController.createPost(StringUtils.toUtf8(text));
|
forumController.createPost(StringUtils.toUtf8(text), resultHandler);
|
||||||
} else {
|
} else {
|
||||||
forumController.createPost(StringUtils.toUtf8(text),
|
forumController
|
||||||
replyEntry.getMessageId());
|
.createPost(StringUtils.toUtf8(text), replyEntry.getId(),
|
||||||
|
resultHandler);
|
||||||
}
|
}
|
||||||
hideSoftKeyboard(textInput);
|
hideSoftKeyboard(textInput);
|
||||||
textInput.setVisibility(GONE);
|
textInput.setVisibility(GONE);
|
||||||
@@ -308,412 +309,56 @@ public class ForumActivity extends BriarActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addLocalEntry(int index, ForumEntry entry) {
|
public void onEntryVisible(ForumEntry forumEntry) {
|
||||||
forumAdapter.addEntry(index, entry, true);
|
if (!forumEntry.isRead()) {
|
||||||
displaySnackbarShort(R.string.forum_new_entry_posted);
|
forumEntry.setRead(true);
|
||||||
|
forumController.entryRead(forumEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addForeignEntry(final int index, final ForumEntry entry) {
|
public void onReplyClick(ForumEntry forumEntry) {
|
||||||
forumAdapter.addEntry(index, entry, false);
|
showTextInput(forumEntry);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ForumViewHolder extends RecyclerView.ViewHolder {
|
private void onForumEntryAdded(final ForumEntry entry, boolean isLocal) {
|
||||||
|
forumAdapter.addEntry(entry);
|
||||||
final TextView textView, lvlText, repliesText;
|
if (isLocal && forumAdapter.isVisible(entry)) {
|
||||||
final AuthorView author;
|
displaySnackbarShort(R.string.forum_new_entry_posted);
|
||||||
final View[] lvls;
|
} else {
|
||||||
final View chevron, replyButton;
|
Snackbar snackbar = Snackbar.make(recyclerView,
|
||||||
final ViewGroup cell;
|
isLocal ? R.string.forum_new_entry_posted :
|
||||||
final View topDivider;
|
R.string.forum_new_entry_received,
|
||||||
|
Snackbar.LENGTH_LONG);
|
||||||
ForumViewHolder(View v) {
|
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||||
super(v);
|
snackbar.setActionTextColor(ContextCompat
|
||||||
|
|
||||||
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
|
|
||||||
.getColor(ForumActivity.this,
|
.getColor(ForumActivity.this,
|
||||||
R.color.window_background));
|
R.color.briar_button_positive));
|
||||||
anim.setEvaluator(new ArgbEvaluator());
|
snackbar.setAction(R.string.show, new View.OnClickListener() {
|
||||||
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() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
showTextInput(data);
|
forumAdapter.scrollToEntry(entry);
|
||||||
linearLayoutManager
|
|
||||||
.scrollToPositionWithOffset(getVisiblePos(data), 0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
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++;
|
|
||||||
}
|
|
||||||
return sEntry == null ? visibleCounter : NO_POSITION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return getVisiblePos(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.UiThread;
|
||||||
|
|
||||||
import org.briarproject.android.controller.ActivityLifecycleController;
|
import org.briarproject.android.controller.ActivityLifecycleController;
|
||||||
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.android.controller.handler.ResultHandler;
|
import org.briarproject.android.controller.handler.ResultHandler;
|
||||||
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.forum.Forum;
|
import org.briarproject.api.forum.Forum;
|
||||||
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
@@ -13,12 +17,14 @@ import java.util.List;
|
|||||||
|
|
||||||
public interface ForumController extends ActivityLifecycleController {
|
public interface ForumController extends ActivityLifecycleController {
|
||||||
|
|
||||||
void loadForum(GroupId groupId, ResultHandler<Boolean> resultHandler);
|
void loadForum(GroupId groupId,
|
||||||
|
ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
Forum getForum();
|
Forum getForum();
|
||||||
|
|
||||||
List<ForumEntry> getForumEntries();
|
void loadPost(ForumPostHeader header,
|
||||||
|
ResultExceptionHandler<ForumEntry, DbException> resultHandler);
|
||||||
|
|
||||||
void unsubscribe(ResultHandler<Boolean> resultHandler);
|
void unsubscribe(ResultHandler<Boolean> resultHandler);
|
||||||
|
|
||||||
@@ -26,14 +32,15 @@ public interface ForumController extends ActivityLifecycleController {
|
|||||||
|
|
||||||
void entriesRead(Collection<ForumEntry> messageIds);
|
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 {
|
interface ForumPostListener {
|
||||||
void addLocalEntry(int index, ForumEntry entry);
|
@UiThread
|
||||||
|
void onExternalEntryAdded(ForumPostHeader header);
|
||||||
void addForeignEntry(int index, ForumEntry entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.app.Activity;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.android.controller.DbControllerImpl;
|
import org.briarproject.android.controller.DbControllerImpl;
|
||||||
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.android.controller.handler.ResultHandler;
|
import org.briarproject.android.controller.handler.ResultHandler;
|
||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.clients.MessageTree;
|
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
import org.briarproject.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.api.crypto.CryptoExecutor;
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.crypto.KeyParser;
|
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.identity.LocalAuthor;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.briarproject.clients.MessageTreeImpl;
|
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
@@ -35,16 +34,16 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Stack;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
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
|
public class ForumControllerImpl extends DbControllerImpl
|
||||||
implements ForumController, EventListener {
|
implements ForumController, EventListener {
|
||||||
@@ -69,12 +68,10 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
protected volatile IdentityManager identityManager;
|
protected volatile IdentityManager identityManager;
|
||||||
|
|
||||||
private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
|
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 LocalAuthor localAuthor = null;
|
||||||
private volatile Forum forum = 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;
|
private ForumPostListener listener;
|
||||||
|
|
||||||
@@ -111,13 +108,18 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
@Override
|
@Override
|
||||||
public void eventOccurred(Event e) {
|
public void eventOccurred(Event e) {
|
||||||
if (forum == null) return;
|
if (forum == null) return;
|
||||||
|
|
||||||
if (e instanceof ForumPostReceivedEvent) {
|
if (e instanceof ForumPostReceivedEvent) {
|
||||||
ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
|
final ForumPostReceivedEvent pe = (ForumPostReceivedEvent) e;
|
||||||
if (pe.getGroupId().equals(forum.getId())) {
|
if (pe.getGroupId().equals(forum.getId())) {
|
||||||
LOG.info("Forum Post received, adding...");
|
LOG.info("Forum post received, adding...");
|
||||||
// FIXME: Don't make blocking calls in event handlers
|
final ForumPostHeader fph = pe.getForumPostHeader();
|
||||||
addNewPost(pe.getForumPostHeader());
|
updateNewestTimestamp(fph.getTimestamp());
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
listener.onExternalEntryAdded(fph);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (e instanceof GroupRemovedEvent) {
|
} else if (e instanceof GroupRemovedEvent) {
|
||||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||||
@@ -133,46 +135,33 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addNewPost(final ForumPostHeader h) {
|
/**
|
||||||
if (forum == null) return;
|
* This should only be run from the DbThread.
|
||||||
runOnDbThread(new Runnable() {
|
*
|
||||||
@Override
|
* @throws DbException
|
||||||
public void run() {
|
*/
|
||||||
if (!bodyCache.containsKey(h.getId())) {
|
private void loadForum(GroupId groupId) throws DbException {
|
||||||
try {
|
// Get Forum
|
||||||
byte[] body = forumManager.getPostBody(h.getId());
|
long now = System.currentTimeMillis();
|
||||||
bodyCache.put(h.getId(), body);
|
forum = forumManager.getForum(groupId);
|
||||||
} catch (DbException e) {
|
long duration = System.currentTimeMillis() - now;
|
||||||
if (LOG.isLoggable(WARNING))
|
if (LOG.isLoggable(INFO))
|
||||||
LOG.log(WARNING, e.toString(), e);
|
LOG.info("Loading forum took " + duration + " ms");
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tree.add(h);
|
// Get First Identity
|
||||||
forumEntries = null;
|
now = System.currentTimeMillis();
|
||||||
// FIXME we should not need to calculate the index here
|
localAuthor = identityManager.getLocalAuthor();
|
||||||
// the index is essentially stored in two different locations
|
duration = System.currentTimeMillis() - now;
|
||||||
int i = 0;
|
if (LOG.isLoggable(INFO))
|
||||||
for (ForumEntry entry : getForumEntries()) {
|
LOG.info("Loading author took " + duration + " ms");
|
||||||
if (entry.getMessageId().equals(h.getId())) {
|
|
||||||
if (localAuthor != null && localAuthor.equals(h.getAuthor())) {
|
|
||||||
addLocalEntry(i, entry);
|
|
||||||
} else {
|
|
||||||
addForeignEntry(i, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should only be run from the DbThread.
|
* This should only be run from the DbThread.
|
||||||
|
*
|
||||||
* @throws DbException
|
* @throws DbException
|
||||||
*/
|
*/
|
||||||
private void loadPosts() throws DbException {
|
private Collection<ForumPostHeader> loadHeaders() throws DbException {
|
||||||
if (forum == null)
|
if (forum == null)
|
||||||
throw new RuntimeException("Forum has not been initialized");
|
throw new RuntimeException("Forum has not been initialized");
|
||||||
|
|
||||||
@@ -180,59 +169,68 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
Collection<ForumPostHeader> headers =
|
Collection<ForumPostHeader> headers =
|
||||||
forumManager.getPostHeaders(forum.getId());
|
forumManager.getPostHeaders(forum.getId());
|
||||||
tree.add(headers);
|
|
||||||
long duration = System.currentTimeMillis() - now;
|
long duration = System.currentTimeMillis() - now;
|
||||||
if (LOG.isLoggable(INFO))
|
if (LOG.isLoggable(INFO))
|
||||||
LOG.info("Loading headers took " + duration + " ms");
|
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
|
// Get Bodies
|
||||||
now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
for (ForumPostHeader header : headers) {
|
for (ForumPostHeader header : headers) {
|
||||||
if (!bodyCache.containsKey(header.getId())) {
|
if (!bodyCache.containsKey(header.getId())) {
|
||||||
byte[] body = forumManager.getPostBody(header.getId());
|
byte[] body = forumManager.getPostBody(header.getId());
|
||||||
bodyCache.put(header.getId(), body);
|
bodyCache.put(header.getId(), body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
duration = System.currentTimeMillis() - now;
|
long duration = System.currentTimeMillis() - now;
|
||||||
if (LOG.isLoggable(INFO))
|
if (LOG.isLoggable(INFO))
|
||||||
LOG.info("Loading bodies took " + duration + " ms");
|
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
|
@Override
|
||||||
public void loadForum(final GroupId groupId,
|
public void loadForum(final GroupId groupId,
|
||||||
final ResultHandler<Boolean> resultHandler) {
|
final ResultExceptionHandler<List<ForumEntry>, DbException> resultHandler) {
|
||||||
runOnDbThread(new Runnable() {
|
runOnDbThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
LOG.info("Loading forum...");
|
LOG.info("Loading forum...");
|
||||||
try {
|
try {
|
||||||
if (forum == null) {
|
if (forum == null) {
|
||||||
// Get Forum
|
loadForum(groupId);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
resultHandler.onResult(true);
|
// Get Forum Posts and Bodies
|
||||||
|
Collection<ForumPostHeader> headers = loadHeaders();
|
||||||
|
updateNewestTimeStamp(headers);
|
||||||
|
loadBodies(headers);
|
||||||
|
resultHandler.onResult(buildForumEntries(headers));
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
if (LOG.isLoggable(WARNING))
|
if (LOG.isLoggable(WARNING))
|
||||||
LOG.log(WARNING, e.toString(), e);
|
LOG.log(WARNING, e.toString(), e);
|
||||||
resultHandler.onResult(false);
|
resultHandler.onException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -245,31 +243,23 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ForumEntry> getForumEntries() {
|
public void loadPost(final ForumPostHeader header,
|
||||||
if (forumEntries != null) {
|
final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||||
return forumEntries;
|
runOnDbThread(new Runnable() {
|
||||||
}
|
@Override
|
||||||
Collection<ForumPostHeader> headers = getHeaders();
|
public void run() {
|
||||||
List<ForumEntry> entries = new ArrayList<>();
|
LOG.info("Loading post...");
|
||||||
Stack<MessageId> idStack = new Stack<>();
|
try {
|
||||||
|
loadBodies(Collections.singletonList(header));
|
||||||
for (ForumPostHeader h : headers) {
|
resultHandler.onResult(new ForumEntry(header, StringUtils
|
||||||
if (h.getParentId() == null) {
|
.fromUtf8(bodyCache.get(header.getId()))));
|
||||||
idStack.clear();
|
} catch (DbException e) {
|
||||||
} else if (idStack.isEmpty() ||
|
if (LOG.isLoggable(WARNING))
|
||||||
!idStack.contains(h.getParentId())) {
|
LOG.log(WARNING, e.toString(), e);
|
||||||
idStack.push(h.getParentId());
|
resultHandler.onException(e);
|
||||||
} else if (!h.getParentId().equals(idStack.peek())) {
|
}
|
||||||
do {
|
|
||||||
idStack.pop();
|
|
||||||
} while (!h.getParentId().equals(idStack.peek()));
|
|
||||||
}
|
}
|
||||||
byte[] body = bodyCache.get(h.getId());
|
});
|
||||||
entries.add(new ForumEntry(h, StringUtils.fromUtf8(body),
|
|
||||||
idStack.size()));
|
|
||||||
}
|
|
||||||
forumEntries = entries;
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -307,7 +297,7 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
try {
|
try {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
for (ForumEntry fe : forumEntries) {
|
for (ForumEntry fe : forumEntries) {
|
||||||
forumManager.setReadFlag(fe.getMessageId(), true);
|
forumManager.setReadFlag(fe.getId(), true);
|
||||||
}
|
}
|
||||||
long duration = System.currentTimeMillis() - now;
|
long duration = System.currentTimeMillis() - now;
|
||||||
if (LOG.isLoggable(INFO))
|
if (LOG.isLoggable(INFO))
|
||||||
@@ -321,95 +311,73 @@ public class ForumControllerImpl extends DbControllerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createPost(byte[] body) {
|
public void createPost(byte[] body,
|
||||||
createPost(body, null);
|
ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||||
|
createPost(body, null, resultHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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() {
|
cryptoExecutor.execute(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
LOG.info("Create post...");
|
||||||
long timestamp = System.currentTimeMillis();
|
long timestamp = System.currentTimeMillis();
|
||||||
long newestTimeStamp = 0;
|
timestamp = Math.max(timestamp, newestTimeStamp.get());
|
||||||
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;
|
|
||||||
}
|
|
||||||
ForumPost p;
|
ForumPost p;
|
||||||
try {
|
try {
|
||||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||||
byte[] b = localAuthor.getPrivateKey();
|
byte[] b = localAuthor.getPrivateKey();
|
||||||
PrivateKey authorKey = keyParser.parsePrivateKey(b);
|
PrivateKey authorKey = keyParser.parsePrivateKey(b);
|
||||||
p = forumPostFactory.createPseudonymousPost(
|
p = forumPostFactory.createPseudonymousPost(
|
||||||
forum.getId(), timestamp, parentId,
|
forum.getId(), timestamp, parentId, localAuthor,
|
||||||
localAuthor, "text/plain", body,
|
"text/plain", body, authorKey);
|
||||||
authorKey);
|
|
||||||
} catch (GeneralSecurityException | FormatException e) {
|
} catch (GeneralSecurityException | FormatException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
bodyCache.put(p.getMessage().getId(), body);
|
bodyCache.put(p.getMessage().getId(), body);
|
||||||
storePost(p);
|
storePost(p, resultHandler);
|
||||||
// FIXME: Don't make DB calls on the crypto executor
|
|
||||||
addNewPost(p);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLocalEntry(final int index, final ForumEntry entry) {
|
private void storePost(final ForumPost p,
|
||||||
activity.runOnUiThread(new Runnable() {
|
final ResultExceptionHandler<ForumEntry, DbException> resultHandler) {
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
listener.addLocalEntry(index, entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addForeignEntry(final int index, final ForumEntry entry) {
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
listener.addForeignEntry(index, entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void storePost(final ForumPost p) {
|
|
||||||
runOnDbThread(new Runnable() {
|
runOnDbThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
LOG.info("Store post...");
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
forumManager.addLocalPost(p);
|
forumManager.addLocalPost(p);
|
||||||
long duration = System.currentTimeMillis() - now;
|
long duration = System.currentTimeMillis() - now;
|
||||||
if (LOG.isLoggable(INFO))
|
if (LOG.isLoggable(INFO))
|
||||||
LOG.info(
|
LOG.info("Storing message took " + duration + " ms");
|
||||||
"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) {
|
} catch (DbException e) {
|
||||||
if (LOG.isLoggable(WARNING))
|
if (LOG.isLoggable(WARNING))
|
||||||
LOG.log(WARNING, e.toString(), e);
|
LOG.log(WARNING, e.toString(), e);
|
||||||
|
resultHandler.onException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addNewPost(final ForumPost p) {
|
private void updateNewestTimestamp(long update) {
|
||||||
ForumPostHeader h =
|
long newest = newestTimeStamp.get();
|
||||||
new ForumPostHeader(p.getMessage().getId(), p.getParent(),
|
while (newest < update) {
|
||||||
p.getMessage().getTimestamp(), p.getAuthor(), VERIFIED,
|
if (newestTimeStamp.compareAndSet(newest, update)) return;
|
||||||
false);
|
newest = newestTimeStamp.get();
|
||||||
addNewPost(h);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<ForumPostHeader> getHeaders() {
|
|
||||||
return tree.depthFirstOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
package org.briarproject.android.forum;
|
package org.briarproject.android.forum;
|
||||||
|
|
||||||
|
import org.briarproject.api.clients.MessageTree;
|
||||||
import org.briarproject.api.forum.ForumPostHeader;
|
import org.briarproject.api.forum.ForumPostHeader;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
import org.briarproject.api.identity.AuthorId;
|
|
||||||
import org.briarproject.api.sync.MessageId;
|
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 messageId;
|
||||||
|
private final MessageId parentId;
|
||||||
private final String text;
|
private final String text;
|
||||||
private final int level;
|
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final Author author;
|
private final Author author;
|
||||||
private Status status;
|
private Status status;
|
||||||
|
private int level = LEVEL_UNDEFINED;
|
||||||
private boolean isShowingDescendants = true;
|
private boolean isShowingDescendants = true;
|
||||||
|
private int descendantCount = 0;
|
||||||
private boolean isRead = true;
|
private boolean isRead = true;
|
||||||
|
|
||||||
ForumEntry(ForumPostHeader h, String text, int level) {
|
ForumEntry(ForumPostHeader h, String text) {
|
||||||
this(h.getId(), text, level, h.getTimestamp(), h.getAuthor(),
|
this(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
||||||
h.getAuthorStatus());
|
h.getAuthorStatus());
|
||||||
this.isRead = h.isRead();
|
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) {
|
long timestamp, Author author, Status status) {
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
|
this.parentId = parentId;
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.level = level;
|
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
@@ -41,6 +46,16 @@ public class ForumEntry {
|
|||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MessageId getId() {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MessageId getParentId() {
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
@@ -57,6 +72,10 @@ public class ForumEntry {
|
|||||||
return isShowingDescendants;
|
return isShowingDescendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setLevel(int level) {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
void setShowingDescendants(boolean showingDescendants) {
|
void setShowingDescendants(boolean showingDescendants) {
|
||||||
this.isShowingDescendants = showingDescendants;
|
this.isShowingDescendants = showingDescendants;
|
||||||
}
|
}
|
||||||
@@ -72,4 +91,12 @@ public class ForumEntry {
|
|||||||
void setRead(boolean read) {
|
void setRead(boolean read) {
|
||||||
isRead = 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.BuildConfig;
|
||||||
import org.briarproject.TestUtils;
|
import org.briarproject.TestUtils;
|
||||||
import org.briarproject.android.TestBriarApplication;
|
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.Author;
|
||||||
import org.briarproject.api.identity.AuthorId;
|
import org.briarproject.api.identity.AuthorId;
|
||||||
import org.briarproject.api.sync.GroupId;
|
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
|
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
|
1
|
||||||
-> 2
|
-> 2
|
||||||
@@ -64,7 +81,8 @@ public class ForumActivityTest {
|
|||||||
|
|
||||||
private TestForumActivity forumActivity;
|
private TestForumActivity forumActivity;
|
||||||
@Captor
|
@Captor
|
||||||
private ArgumentCaptor<UiResultHandler<Boolean>> rc;
|
private ArgumentCaptor<UiResultExceptionHandler<List<ForumEntry>, DbException>>
|
||||||
|
rc;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
@@ -75,16 +93,17 @@ public class ForumActivityTest {
|
|||||||
.withIntent(intent).create().resume().get();
|
.withIntent(intent).create().resume().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private List<ForumEntry> getDummyData() {
|
private List<ForumEntry> getDummyData() {
|
||||||
ForumEntry[] forumEntries = new ForumEntry[6];
|
ForumEntry[] forumEntries = new ForumEntry[6];
|
||||||
for (int i = 0; i < forumEntries.length; i++) {
|
for (int i = 0; i < forumEntries.length; i++) {
|
||||||
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
|
AuthorId authorId = new AuthorId(TestUtils.getRandomId());
|
||||||
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
|
byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
|
||||||
Author author = new Author(authorId, AUTHORS[i], publicKey);
|
Author author = new Author(authorId, AUTHORS[i], publicKey);
|
||||||
forumEntries[i] = new ForumEntry(
|
forumEntries[i] =
|
||||||
new MessageId(TestUtils.getRandomId()), AUTHORS[i],
|
new ForumEntry(AUTHOR_IDS[i], PARENT_AUTHOR_IDS[i],
|
||||||
LEVELS[i], System.currentTimeMillis(), author, UNKNOWN);
|
AUTHORS[i], System.currentTimeMillis(), author,
|
||||||
|
UNKNOWN);
|
||||||
|
forumEntries[i].setLevel(LEVELS[i]);
|
||||||
}
|
}
|
||||||
return new ArrayList<>(Arrays.asList(forumEntries));
|
return new ArrayList<>(Arrays.asList(forumEntries));
|
||||||
}
|
}
|
||||||
@@ -93,13 +112,10 @@ public class ForumActivityTest {
|
|||||||
public void testNestedEntries() {
|
public void testNestedEntries() {
|
||||||
ForumController mc = forumActivity.getController();
|
ForumController mc = forumActivity.getController();
|
||||||
List<ForumEntry> dummyData = getDummyData();
|
List<ForumEntry> dummyData = getDummyData();
|
||||||
Mockito.when(mc.getForumEntries()).thenReturn(dummyData);
|
|
||||||
// Verify that the forum load is called once
|
|
||||||
verify(mc, times(1))
|
verify(mc, times(1))
|
||||||
.loadForum(Mockito.any(GroupId.class), rc.capture());
|
.loadForum(Mockito.any(GroupId.class), rc.capture());
|
||||||
rc.getValue().onResult(true);
|
rc.getValue().onResult(dummyData);
|
||||||
verify(mc, times(1)).getForumEntries();
|
NestedForumAdapter adapter = forumActivity.getAdapter();
|
||||||
ForumActivity.ForumAdapter adapter = forumActivity.getAdapter();
|
|
||||||
Assert.assertNotNull(adapter);
|
Assert.assertNotNull(adapter);
|
||||||
// Cascade close
|
// Cascade close
|
||||||
assertEquals(6, adapter.getItemCount());
|
assertEquals(6, adapter.getItemCount());
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class TestForumActivity extends ForumActivity {
|
|||||||
return forumController;
|
return forumController;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ForumAdapter getAdapter() {
|
public NestedForumAdapter getAdapter() {
|
||||||
return forumAdapter;
|
return forumAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
|
|||||||
interface MessageNode {
|
interface MessageNode {
|
||||||
MessageId getId();
|
MessageId getId();
|
||||||
MessageId getParentId();
|
MessageId getParentId();
|
||||||
|
void setLevel(int level);
|
||||||
|
void setDescendantCount(int descendantCount);
|
||||||
long getTimestamp();
|
long getTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package org.briarproject.api.forum;
|
package org.briarproject.api.forum;
|
||||||
|
|
||||||
import org.briarproject.api.clients.MessageTree;
|
|
||||||
import org.briarproject.api.clients.PostHeader;
|
import org.briarproject.api.clients.PostHeader;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
public class ForumPostHeader extends PostHeader
|
public class ForumPostHeader extends PostHeader {
|
||||||
implements MessageTree.MessageNode {
|
|
||||||
|
|
||||||
public ForumPostHeader(MessageId id, MessageId parentId, long timestamp,
|
public ForumPostHeader(MessageId id, MessageId parentId, long timestamp,
|
||||||
Author author, Author.Status authorStatus, boolean read) {
|
Author author, Author.Status authorStatus, boolean read) {
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public synchronized void clear() {
|
||||||
roots.clear();
|
roots.clear();
|
||||||
nodeMap.clear();
|
nodeMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void add(Collection<T> nodes) {
|
public synchronized void add(Collection<T> nodes) {
|
||||||
// add all nodes to the node map
|
// add all nodes to the node map
|
||||||
for (T node : nodes) {
|
for (T node : nodes) {
|
||||||
nodeMap.put(node.getId(), new ArrayList<T>());
|
nodeMap.put(node.getId(), new ArrayList<T>());
|
||||||
@@ -45,7 +45,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void add(T node) {
|
public synchronized void add(T node) {
|
||||||
add(Collections.singletonList(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);
|
list.add(node);
|
||||||
for (T child : nodeMap.get(node.getId())) {
|
List<T> children = nodeMap.get(node.getId());
|
||||||
traverse(list, child);
|
node.setLevel(level);
|
||||||
|
node.setDescendantCount(children.size());
|
||||||
|
for (T child : children) {
|
||||||
|
traverse(list, child, level+1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setComparator(Comparator<T> comparator) {
|
public synchronized void setComparator(Comparator<T> comparator) {
|
||||||
this.comparator = comparator;
|
this.comparator = comparator;
|
||||||
// Sort all lists with the new comparator
|
// Sort all lists with the new comparator
|
||||||
Collections.sort(roots, comparator);
|
Collections.sort(roots, comparator);
|
||||||
@@ -95,10 +98,10 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<T> depthFirstOrder() {
|
public synchronized Collection<T> depthFirstOrder() {
|
||||||
List<T> orderedList = new ArrayList<T>();
|
List<T> orderedList = new ArrayList<T>();
|
||||||
for (T root : roots) {
|
for (T root : roots) {
|
||||||
traverse(orderedList, root);
|
traverse(orderedList, root, 0);
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableList(orderedList);
|
return Collections.unmodifiableList(orderedList);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ public class MessageTreeTest {
|
|||||||
return parentId;
|
return parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLevel(int level) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDescendantCount(int descendantCount) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
|
|||||||
Reference in New Issue
Block a user