Refactor Forum Controller, so it can be used by private groups

This commit is contained in:
Torsten Grote
2016-10-11 10:17:51 -03:00
parent 9ce95d6de7
commit 65b47bb5d2
10 changed files with 569 additions and 441 deletions

View File

@@ -14,36 +14,44 @@ import android.view.View;
import org.briarproject.R;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.android.view.BriarRecyclerView;
import org.briarproject.android.view.TextInputView;
import org.briarproject.android.view.TextInputView.TextInputListener;
import org.briarproject.api.clients.BaseGroup;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static android.support.design.widget.Snackbar.make;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
public abstract class ThreadListActivity<G extends BaseGroup, I extends ThreadItem, H extends PostHeader, A extends ThreadItemAdapter<I>>
extends BriarActivity
implements TextInputListener, ThreadItemListener<I> {
implements ThreadListListener<H>, TextInputListener,
ThreadItemListener<I> {
protected static final String KEY_INPUT_VISIBILITY = "inputVisibility";
protected static final String KEY_REPLY_ID = "replyId";
@Inject
protected AndroidNotificationManager notificationManager;
protected A adapter;
protected BriarRecyclerView list;
protected TextInputView textInput;
protected GroupId groupId;
private byte[] replyId;
protected abstract ThreadListController<G, I, H> getController();
@CallSuper
@Override
@SuppressWarnings("ConstantConditions")
public void onCreate(final Bundle state) {
super.onCreate(state);
@@ -53,6 +61,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No GroupId in intent.");
groupId = new GroupId(b);
getController().setGroupId(groupId);
String groupName = i.getStringExtra(GROUP_NAME);
if (groupName != null) setTitle(groupName);
else loadAndSetTitle();
textInput = (TextInputView) findViewById(R.id.text_input_container);
textInput.setVisibility(GONE);
@@ -62,17 +74,64 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
list.setLayoutManager(linearLayoutManager);
adapter = createAdapter(linearLayoutManager);
list.setAdapter(adapter);
if (state != null) {
replyId = state.getByteArray(KEY_REPLY_ID);
}
loadItems();
}
protected abstract @LayoutRes int getLayout();
protected abstract A createAdapter(LinearLayoutManager layoutManager);
private void loadAndSetTitle() {
getController().loadGroupItem(
new UiResultExceptionHandler<G, DbException>(this) {
@Override
public void onResultUi(G forum) {
setTitle(forum.getName());
}
@Override
public void onExceptionUi(DbException exception) {
// TODO Proper error handling
finish();
}
});
}
private void loadItems() {
getController().loadItems(
new UiResultExceptionHandler<Collection<I>, DbException>(
this) {
@Override
public void onResultUi(Collection<I> result) {
// FIXME What's the benefit of copying the collection?
List<I> items = new ArrayList<>(result);
if (items.isEmpty()) {
list.showData();
} else {
adapter.setItems(items);
list.showData();
if (replyId != null)
adapter.setReplyItemById(replyId);
}
}
@Override
public void onExceptionUi(DbException exception) {
// TODO Proper error handling
finish();
}
});
}
@CallSuper
@Override
public void onResume() {
super.onResume();
notificationManager.blockNotification(groupId);
list.startPeriodicUpdate();
}
@@ -80,7 +139,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
@Override
public void onPause() {
super.onPause();
notificationManager.unblockNotification(groupId);
list.stopPeriodicUpdate();
}
@@ -130,12 +188,10 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
public void onItemVisible(I item) {
if (!item.isRead()) {
item.setRead(true);
markItemRead(item);
getController().markItemRead(item);
}
}
protected abstract void markItemRead(I item);
@Override
public void onReplyClick(I item) {
showTextInput(item);
@@ -167,13 +223,50 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
if (text.trim().length() == 0)
return;
I replyItem = adapter.getReplyItem();
sendItem(text, replyItem);
UiResultExceptionHandler<I, DbException> handler =
new UiResultExceptionHandler<I, DbException>(this) {
@Override
public void onResultUi(I result) {
addItem(result, true);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO add proper exception handling
finish();
}
};
if (replyItem == null) {
// root post
getController().send(text, handler);
} else {
getController().send(text, replyItem.getId(), handler);
}
textInput.hideSoftKeyboard();
textInput.setVisibility(GONE);
adapter.setReplyItem(null);
}
protected abstract void sendItem(String text, I replyToItem);
@Override
public void onHeaderReceived(H header) {
getController().loadItem(header,
new UiResultExceptionHandler<I, DbException>(this) {
@Override
public void onResultUi(final I result) {
addItem(result, false);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO add proper exception handling
}
});
}
@Override
public void onGroupRemoved() {
supportFinishAfterTransition();
}
protected void addItem(final I item, boolean isLocal) {
adapter.add(item);

View File

@@ -0,0 +1,48 @@
package org.briarproject.android.threaded;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.android.DestroyableContext;
import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.clients.BaseGroup;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.forum.ForumPostHeader;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.Collection;
public interface ThreadListController<G extends BaseGroup, I extends ThreadItem, H extends PostHeader>
extends ActivityLifecycleController {
void setGroupId(GroupId groupId);
void loadGroupItem(ResultExceptionHandler<G, DbException> handler);
void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
void loadItems(ResultExceptionHandler<Collection<I>, DbException> handler);
void markItemRead(I item);
void markItemsRead(Collection<I> items);
void send(String body, ResultExceptionHandler<I, DbException> handler);
void send(String body, @Nullable MessageId parentId,
ResultExceptionHandler<I, DbException> handler);
void deleteGroupItem(ResultExceptionHandler<Void, DbException> handler);
interface ThreadListListener<H> extends DestroyableContext {
@UiThread
void onHeaderReceived(H header);
@UiThread
void onGroupRemoved();
}
}

View File

@@ -0,0 +1,327 @@
package org.briarproject.android.threaded;
import android.app.Activity;
import android.support.annotation.CallSuper;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.clients.BaseGroup;
import org.briarproject.api.clients.PostHeader;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.CryptoExecutor;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.GroupRemovedEvent;
import org.briarproject.api.forum.ForumManager;
import org.briarproject.api.forum.ForumPostFactory;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public abstract class ThreadListControllerImpl<G extends BaseGroup, I extends ThreadItem, H extends PostHeader>
extends DbControllerImpl
implements ThreadListController<G, I, H>, EventListener {
private static final Logger LOG =
Logger.getLogger(ThreadListControllerImpl.class.getName());
protected final Executor cryptoExecutor;
protected final CryptoComponent crypto;
protected final EventBus eventBus;
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final Map<MessageId, String> bodyCache =
new ConcurrentHashMap<>();
protected final AtomicLong newestTimeStamp = new AtomicLong();
protected volatile GroupId groupId;
protected ThreadListListener<H> listener;
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
@CryptoExecutor Executor cryptoExecutor, CryptoComponent crypto,
EventBus eventBus, IdentityManager identityManager,
AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager);
this.cryptoExecutor = cryptoExecutor;
this.crypto = crypto;
this.eventBus = eventBus;
this.identityManager = identityManager;
this.notificationManager = notificationManager;
}
@Override
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
}
@CallSuper
@SuppressWarnings("unchecked")
@Override
public void onActivityCreate(Activity activity) {
listener = (ThreadListListener<H>) activity;
}
@CallSuper
@Override
public void onActivityResume() {
checkGroupId();
notificationManager.blockNotification(groupId);
eventBus.addListener(this);
}
@CallSuper
@Override
public void onActivityPause() {
notificationManager.unblockNotification(groupId);
eventBus.removeListener(this);
}
@Override
public void onActivityDestroy() {
}
@CallSuper
@Override
public void eventOccurred(Event e) {
if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e;
if (s.getGroup().getId().equals(groupId)) {
LOG.info("Group removed");
listener.runOnUiThreadUnlessDestroyed(new Runnable() {
@Override
public void run() {
listener.onGroupRemoved();
}
});
}
}
}
@Override
public void loadGroupItem(
final ResultExceptionHandler<G, DbException> handler) {
checkGroupId();
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
G groupItem = loadGroupItem();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading forum took " + duration + " ms");
handler.onResult(groupItem);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
protected abstract G loadGroupItem() throws DbException;
@Override
public void loadItems(
final ResultExceptionHandler<Collection<I>, DbException> handler) {
checkGroupId();
runOnDbThread(new Runnable() {
@Override
public void run() {
LOG.info("Loading items...");
try {
// Load headers
long now = System.currentTimeMillis();
Collection<H> headers = loadHeaders();
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading headers took " + duration + " ms");
// Update timestamp of newest item
updateNewestTimeStamp(headers);
// Load bodies
now = System.currentTimeMillis();
loadBodies(headers);
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading bodies took " + duration + " ms");
// Build and hand over items
handler.onResult(buildItems(headers));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
protected abstract Collection<H> loadHeaders() throws DbException;
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
protected abstract void loadBodies(Collection<H> headers)
throws DbException;
@Override
public void loadItem(final H header,
final ResultExceptionHandler<I, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
LOG.info("Loading item...");
try {
loadBodies(Collections.singletonList(header));
I item = buildItem(header);
handler.onResult(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
public void markItemRead(I item) {
markItemsRead(Collections.singletonList(item));
}
@Override
public void markItemsRead(final Collection<I> items) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
for (I i : items) {
markRead(i.getId());
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Marking read took " + duration + " ms");
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
protected abstract void markRead(MessageId id) throws DbException;
@Override
public void send(String body,
ResultExceptionHandler<I, DbException> resultHandler) {
send(body, null, resultHandler);
}
@Override
public void deleteGroupItem(
final ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
G groupItem = loadGroupItem();
deleteGroupItem(groupItem);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Removing group took " + duration + " ms");
//noinspection ConstantConditions
handler.onResult(null);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
/**
* This should only be run from the DbThread.
*
* @throws DbException
*/
protected abstract void deleteGroupItem(G groupItem) throws DbException;
private List<I> buildItems(Collection<H> headers) {
List<I> entries = new ArrayList<>();
for (H h : headers) {
entries.add(buildItem(h));
}
return entries;
}
/**
* When building the item, the body can be assumed to be cached
*/
protected abstract I buildItem(H header);
private void updateNewestTimeStamp(Collection<H> headers) {
for (H h : headers) {
updateNewestTimestamp(h.getTimestamp());
}
}
protected void updateNewestTimestamp(long update) {
long newest = newestTimeStamp.get();
while (newest < update) {
if (newestTimeStamp.compareAndSet(newest, update)) return;
newest = newestTimeStamp.get();
}
}
private void checkGroupId() {
if (groupId == null) {
throw new IllegalStateException(
"You must set the GroupId before the controller is started.");
}
}
}