list position save and restore now implemented for threaded lists

This commit is contained in:
Ernir Erlingsson
2017-04-21 13:52:53 +02:00
parent d1a929da85
commit 044719432a
12 changed files with 193 additions and 16 deletions

View File

@@ -32,6 +32,7 @@ import org.briarproject.briar.api.android.ScreenFilterMonitor;
import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogPostFactory;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.feed.FeedManager; import org.briarproject.briar.api.feed.FeedManager;
import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumSharingManager; import org.briarproject.briar.api.forum.ForumSharingManager;
@@ -78,6 +79,8 @@ public interface AndroidComponent
@DatabaseExecutor @DatabaseExecutor
Executor databaseExecutor(); Executor databaseExecutor();
MessageTracker messageTracker();
LifecycleManager lifecycleManager(); LifecycleManager lifecycleManager();
IdentityManager identityManager(); IdentityManager identityManager();

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.threaded; package org.briarproject.briar.android.threaded;
import android.os.Handler;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
@@ -26,6 +27,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
protected final NestedTreeList<I> items = new NestedTreeList<>(); protected final NestedTreeList<I> items = new NestedTreeList<>();
private final ThreadItemListener<I> listener; private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager; private final LinearLayoutManager layoutManager;
private final Handler handler = new Handler();
private volatile int revision = 0; private volatile int revision = 0;
@@ -64,6 +66,31 @@ public class ThreadItemAdapter<I extends ThreadItem>
revision++; revision++;
} }
void setBottomItem(MessageId messageId) {
if (messageId != null) {
int pos = 0;
for (I item : items) {
if (item.getId().equals(messageId)) {
scrollToPosition(pos);
break;
}
pos++;
}
}
}
private void scrollToPosition(final int pos) {
// Post call ensures that the list scrolls AFTER it has been propagated
// and the layout has been calculated.
handler.post(new Runnable() {
@Override
public void run() {
layoutManager.scrollToPosition(pos);
}
});
}
public void setItems(Collection<I> items) { public void setItems(Collection<I> items) {
this.items.clear(); this.items.clear();
this.items.addAll(items); this.items.addAll(items);

View File

@@ -0,0 +1,15 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.List;
import javax.annotation.Nullable;
public interface ThreadItemList<I extends ThreadItem> extends List<I> {
@Nullable
MessageId getBottomVisibleItemId();
void setBottomVisibleItemId(@Nullable MessageId bottomVisibleItemId);
}

View File

@@ -0,0 +1,22 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.ArrayList;
import javax.annotation.Nullable;
public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
implements ThreadItemList<I> {
private MessageId bottomVisibleItemId;
@Override
public MessageId getBottomVisibleItemId() {
return bottomVisibleItemId;
}
public void setBottomVisibleItemId(@Nullable MessageId bottomVisibleItemId) {
this.bottomVisibleItemId = bottomVisibleItemId;
}
}

View File

@@ -26,6 +26,7 @@ import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingController.SharingListener; import org.briarproject.briar.android.controller.SharingController.SharingListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource;
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener; import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
@@ -51,7 +52,7 @@ import static org.briarproject.briar.android.threaded.ThreadItemAdapter.UnreadCo
public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadItemAdapter<I>, I extends ThreadItem, H extends PostHeader> public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadItemAdapter<I>, I extends ThreadItem, H extends PostHeader>
extends BriarActivity extends BriarActivity
implements ThreadListListener<H>, TextInputListener, SharingListener, implements ThreadListListener<H>, TextInputListener, SharingListener,
ThreadItemListener<I> { ThreadItemListener<I>, ThreadListDataSource {
protected static final String KEY_REPLY_ID = "replyId"; protected static final String KEY_REPLY_ID = "replyId";
@@ -68,6 +69,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
private MessageId replyId; private MessageId replyId;
protected abstract ThreadListController<G, I, H> getController(); protected abstract ThreadListController<G, I, H> getController();
@Inject @Inject
protected SharingController sharingController; protected SharingController sharingController;
@@ -104,6 +106,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
updateUnreadCount(); updateUnreadCount();
} }
} }
@Override @Override
public void onScrollStateChanged(RecyclerView recyclerView, public void onScrollStateChanged(RecyclerView recyclerView,
int newState) { int newState) {
@@ -144,6 +147,16 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
loadSharingContacts(); loadSharingContacts();
} }
@Override
public MessageId getBottomVisibleMessageId() {
if (layoutManager != null && adapter != null) {
int position =
layoutManager.findLastCompletelyVisibleItemPosition();
return adapter.getItemAt(position).getId();
}
return null;
}
protected abstract A createAdapter(LinearLayoutManager layoutManager); protected abstract A createAdapter(LinearLayoutManager layoutManager);
protected void loadNamedGroup() { protected void loadNamedGroup() {
@@ -167,15 +180,18 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI
protected void loadItems() { protected void loadItems() {
final int revision = adapter.getRevision(); final int revision = adapter.getRevision();
getController().loadItems( getController().loadItems(
new UiResultExceptionHandler<Collection<I>, DbException>(this) { new UiResultExceptionHandler<ThreadItemList<I>, DbException>(
this) {
@Override @Override
public void onResultUi(Collection<I> items) { public void onResultUi(ThreadItemList<I> items) {
if (revision == adapter.getRevision()) { if (revision == adapter.getRevision()) {
adapter.incrementRevision(); adapter.incrementRevision();
if (items.isEmpty()) { if (items.isEmpty()) {
list.showData(); list.showData();
} else { } else {
adapter.setItems(items); adapter.setItems(items);
adapter.setBottomItem(
items.getBottomVisibleItemId());
list.showData(); list.showData();
updateTextInput(replyId); updateTextInput(replyId);
} }

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.DestroyableContext; import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ExceptionHandler; import org.briarproject.briar.android.controller.handler.ExceptionHandler;
@@ -30,7 +31,7 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
void loadItem(H header, ResultExceptionHandler<I, DbException> handler); void loadItem(H header, ResultExceptionHandler<I, DbException> handler);
void loadItems(ResultExceptionHandler<Collection<I>, DbException> handler); void loadItems(ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
void markItemRead(I item); void markItemRead(I item);
@@ -52,4 +53,10 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
void onInvitationAccepted(ContactId c); void onInvitationAccepted(ContactId c);
} }
interface ThreadListDataSource {
@UiThread @Nullable
MessageId getBottomVisibleMessageId();
}
} }

View File

@@ -22,19 +22,20 @@ import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler; import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener; import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.NamedGroup; import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader; import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.client.ThreadedMessage; import org.briarproject.briar.api.client.ThreadedMessage;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
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;
@@ -56,6 +57,9 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
protected final Executor cryptoExecutor; protected final Executor cryptoExecutor;
protected final Clock clock; protected final Clock clock;
protected volatile L listener; protected volatile L listener;
@Inject
MessageTracker messageTracker;
private ThreadListDataSource source;
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor, protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager, LifecycleManager lifecycleManager, IdentityManager identityManager,
@@ -79,6 +83,13 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@Override @Override
public void onActivityCreate(Activity activity) { public void onActivityCreate(Activity activity) {
listener = (L) activity; listener = (L) activity;
if (activity instanceof ThreadListDataSource) {
source = (ThreadListDataSource) activity;
} else {
throw new ClassCastException(
"Activity " + activity.getClass().getSimpleName() +
" must implement ThreadListDataSource");
}
} }
@CallSuper @CallSuper
@@ -97,6 +108,13 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@Override @Override
public void onActivityDestroy() { public void onActivityDestroy() {
try {
messageTracker
.storeMessageId(groupId,
source.getBottomVisibleMessageId());
} catch (DbException e) {
e.printStackTrace();
}
} }
@CallSuper @CallSuper
@@ -144,7 +162,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@Override @Override
public void loadItems( public void loadItems(
final ResultExceptionHandler<Collection<I>, DbException> handler) { final ResultExceptionHandler<ThreadItemList<I>, DbException> handler) {
checkGroupId(); checkGroupId();
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
@Override @Override
@@ -293,11 +311,19 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
@DatabaseExecutor @DatabaseExecutor
protected abstract void deleteNamedGroup(G groupItem) throws DbException; protected abstract void deleteNamedGroup(G groupItem) throws DbException;
private List<I> buildItems(Collection<H> headers) { private ThreadItemList<I> buildItems(Collection<H> headers) {
List<I> items = new ArrayList<>(); ThreadItemList<I> items = new ThreadItemListImpl<>();
for (H h : headers) { for (H h : headers) {
items.add(buildItem(h, bodyCache.get(h.getId()))); items.add(buildItem(h, bodyCache.get(h.getId())));
} }
try {
MessageId msgId = messageTracker.loadStoredMessageId(groupId);
if (LOG.isLoggable(INFO))
LOG.info("Loaded last top visible message id " + msgId);
items.setBottomVisibleItemId(msgId);
} catch (DbException e) {
e.printStackTrace();
}
return items; return items;
} }

View File

@@ -13,6 +13,8 @@ import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.android.TestBriarApplication; import org.briarproject.briar.android.TestBriarApplication;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter; import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadItemList;
import org.briarproject.briar.android.threaded.ThreadItemListImpl;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -23,10 +25,7 @@ import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
@@ -81,7 +80,7 @@ public class ForumActivityTest {
private TestForumActivity forumActivity; private TestForumActivity forumActivity;
@Captor @Captor
private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumItem>, DbException>> private ArgumentCaptor<UiResultExceptionHandler<ThreadItemList<ForumItem>, DbException>>
rc; rc;
@Before @Before
@@ -93,7 +92,7 @@ public class ForumActivityTest {
.withIntent(intent).create().resume().get(); .withIntent(intent).create().resume().get();
} }
private List<ForumItem> getDummyData() { private ThreadItemList<ForumItem> getDummyData() {
ForumItem[] forumItems = new ForumItem[6]; ForumItem[] forumItems = new ForumItem[6];
for (int i = 0; i < forumItems.length; i++) { for (int i = 0; i < forumItems.length; i++) {
AuthorId authorId = new AuthorId(TestUtils.getRandomId()); AuthorId authorId = new AuthorId(TestUtils.getRandomId());
@@ -103,13 +102,15 @@ public class ForumActivityTest {
AUTHORS[i], System.currentTimeMillis(), author, UNKNOWN); AUTHORS[i], System.currentTimeMillis(), author, UNKNOWN);
forumItems[i].setLevel(LEVELS[i]); forumItems[i].setLevel(LEVELS[i]);
} }
return new ArrayList<>(Arrays.asList(forumItems)); ThreadItemList<ForumItem> list = new ThreadItemListImpl<>();
list.addAll(Arrays.asList(forumItems));
return list;
} }
@Test @Test
public void testNestedEntries() { public void testNestedEntries() {
ForumController mc = forumActivity.getController(); ForumController mc = forumActivity.getController();
List<ForumItem> dummyData = getDummyData(); ThreadItemList<ForumItem> dummyData = getDummyData();
verify(mc, times(1)).loadItems(rc.capture()); verify(mc, times(1)).loadItems(rc.capture());
rc.getValue().onResult(dummyData); rc.getValue().onResult(dummyData);
ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter(); ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import javax.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface MessageTracker { public interface MessageTracker {
@@ -38,6 +40,19 @@ public interface MessageTracker {
void trackMessage(Transaction txn, GroupId g, long timestamp, boolean read) void trackMessage(Transaction txn, GroupId g, long timestamp, boolean read)
throws DbException; throws DbException;
/**
* Loads the stored message id for the respective group id or returns null
* if none is available.
*/
@Nullable
MessageId loadStoredMessageId(GroupId g) throws DbException;
/**
* Stores the message id for the respective group id. Exactly one message id
* can be stored for any group id at any time, older values are overwritten.
*/
void storeMessageId(GroupId g, MessageId m) throws DbException;
/** /**
* Marks a message as read or unread and updates the group count. * Marks a message as read or unread and updates the group count.
*/ */

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.client;
public interface MessageTrackerConstants { public interface MessageTrackerConstants {
String GROUP_KEY_STORED_MESSAGE_ID = "storedMessageId";
String GROUP_KEY_MSG_COUNT = "messageCount"; String GROUP_KEY_MSG_COUNT = "messageCount";
String GROUP_KEY_UNREAD_COUNT = "unreadCount"; String GROUP_KEY_UNREAD_COUNT = "unreadCount";
String GROUP_KEY_LATEST_MSG = "latestMessageTime"; String GROUP_KEY_LATEST_MSG = "latestMessageTime";

View File

@@ -13,11 +13,13 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_LATEST_MSG; import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_LATEST_MSG;
import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_MSG_COUNT; import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_MSG_COUNT;
import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_STORED_MESSAGE_ID;
import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_UNREAD_COUNT; import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_UNREAD_COUNT;
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
@@ -57,6 +59,30 @@ class MessageTrackerImpl implements MessageTracker {
latestMsgTime)); latestMsgTime));
} }
@Nullable
@Override
public MessageId loadStoredMessageId(GroupId g) throws DbException {
try {
BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(g);
byte[] msgBytes = d.getOptionalRaw(GROUP_KEY_STORED_MESSAGE_ID);
return msgBytes != null? new MessageId(msgBytes) : null;
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override
public void storeMessageId(GroupId g, MessageId m) throws DbException {
BdfDictionary d = BdfDictionary.of(
new BdfEntry(GROUP_KEY_STORED_MESSAGE_ID, m)
);
try {
clientHelper.mergeGroupMetadata(g, d);
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override @Override
public GroupCount getGroupCount(GroupId g) throws DbException { public GroupCount getGroupCount(GroupId g) throws DbException {
GroupCount count; GroupCount count;

View File

@@ -1,7 +1,10 @@
package org.briarproject.briar.forum; package org.briarproject.briar.forum;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.test.TestDatabaseModule; import org.briarproject.bramble.test.TestDatabaseModule;
import org.briarproject.bramble.test.TestUtils;
import org.briarproject.briar.api.forum.Forum; import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumManager; import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumPost; import org.briarproject.briar.api.forum.ForumPost;
@@ -10,6 +13,7 @@ import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.test.BriarIntegrationTest; import org.briarproject.briar.test.BriarIntegrationTest;
import org.briarproject.briar.test.BriarIntegrationTestComponent; import org.briarproject.briar.test.BriarIntegrationTestComponent;
import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -223,4 +227,18 @@ public class ForumManagerTest
assertEquals(1, forumManager1.getPostHeaders(g1).size()); assertEquals(1, forumManager1.getPostHeaders(g1).size());
} }
@Test
public void testMessageStoreAndLoad() {
MessageId msgId = new MessageId(TestUtils.getRandomId());
MessageId loadedId = null;
try {
messageTracker0.storeMessageId(groupId0, msgId);
loadedId = messageTracker0.loadStoredMessageId(groupId0);
} catch (DbException e) {
e.printStackTrace();
}
Assert.assertNotNull(loadedId);
Assert.assertTrue(msgId.equals(loadedId));
}
} }