diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 9923043643e8983be4adc12d72877df2e5337b7b..9394f9d02ee249fc0bf9495e4f00df3fc6cb343b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -32,6 +32,7 @@ import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogPostFactory; 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.forum.ForumManager; import org.briarproject.briar.api.forum.ForumSharingManager; @@ -78,6 +79,8 @@ public interface AndroidComponent @DatabaseExecutor Executor databaseExecutor(); + MessageTracker messageTracker(); + LifecycleManager lifecycleManager(); IdentityManager identityManager(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java index 72b03a03fc133340218e0f45ddb801446c9349db..9e4bbe51e277e5c8097936b400377a9f28739ca1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemAdapter.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.threaded; +import android.os.Handler; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v7.widget.LinearLayoutManager; @@ -26,6 +27,7 @@ public class ThreadItemAdapter<I extends ThreadItem> protected final NestedTreeList<I> items = new NestedTreeList<>(); private final ThreadItemListener<I> listener; private final LinearLayoutManager layoutManager; + private final Handler handler = new Handler(); private volatile int revision = 0; @@ -64,6 +66,31 @@ public class ThreadItemAdapter<I extends ThreadItem> 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) { this.items.clear(); this.items.addAll(items); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java new file mode 100644 index 0000000000000000000000000000000000000000..53d6ed633a89704ed9351b6f6b5527128833e4ee --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemList.java @@ -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); +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..880d297391fc5d6936266fa65a3b58e5499ad4e1 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadItemListImpl.java @@ -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; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index 8106be7edb5fc6b1cd50a9bbd5a7257f4984f4e0..dc184365e56d727b2b8cf689707befc193c1572e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -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.handler.UiResultExceptionHandler; 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.view.BriarRecyclerView; 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> extends BriarActivity implements ThreadListListener<H>, TextInputListener, SharingListener, - ThreadItemListener<I> { + ThreadItemListener<I>, ThreadListDataSource { 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; protected abstract ThreadListController<G, I, H> getController(); + @Inject protected SharingController sharingController; @@ -104,6 +106,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI updateUnreadCount(); } } + @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { @@ -144,6 +147,16 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI 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 void loadNamedGroup() { @@ -167,15 +180,18 @@ public abstract class ThreadListActivity<G extends NamedGroup, A extends ThreadI protected void loadItems() { final int revision = adapter.getRevision(); getController().loadItems( - new UiResultExceptionHandler<Collection<I>, DbException>(this) { + new UiResultExceptionHandler<ThreadItemList<I>, DbException>( + this) { @Override - public void onResultUi(Collection<I> items) { + public void onResultUi(ThreadItemList<I> items) { if (revision == adapter.getRevision()) { adapter.incrementRevision(); if (items.isEmpty()) { list.showData(); } else { adapter.setItems(items); + adapter.setBottomItem( + items.getBottomVisibleItemId()); list.showData(); updateTextInput(replyId); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java index caebd1ec65d53dad3c590e0d1cf287703073e0d2..a062c8322ec4773b41eb1d1e476d4c7286779e26 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListController.java @@ -6,6 +6,7 @@ import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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.controller.ActivityLifecycleController; 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 loadItems(ResultExceptionHandler<Collection<I>, DbException> handler); + void loadItems(ResultExceptionHandler<ThreadItemList<I>, DbException> handler); void markItemRead(I item); @@ -52,4 +53,10 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem void onInvitationAccepted(ContactId c); } + interface ThreadListDataSource { + + @UiThread @Nullable + MessageId getBottomVisibleMessageId(); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java index a2db5242d9cd8bba435fa1cd50bf28d0e79e8a5e..94b880f9dcc058f1997f902282f5b9c54297269e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListControllerImpl.java @@ -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.threaded.ThreadListController.ThreadListListener; 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.PostHeader; import org.briarproject.briar.api.client.ThreadedMessage; -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.logging.Logger; +import javax.inject.Inject; + import static java.util.logging.Level.INFO; 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 Clock clock; protected volatile L listener; + @Inject + MessageTracker messageTracker; + private ThreadListDataSource source; protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor, LifecycleManager lifecycleManager, IdentityManager identityManager, @@ -79,6 +83,13 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T @Override public void onActivityCreate(Activity activity) { listener = (L) activity; + if (activity instanceof ThreadListDataSource) { + source = (ThreadListDataSource) activity; + } else { + throw new ClassCastException( + "Activity " + activity.getClass().getSimpleName() + + " must implement ThreadListDataSource"); + } } @CallSuper @@ -97,6 +108,13 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T @Override public void onActivityDestroy() { + try { + messageTracker + .storeMessageId(groupId, + source.getBottomVisibleMessageId()); + } catch (DbException e) { + e.printStackTrace(); + } } @CallSuper @@ -144,7 +162,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T @Override public void loadItems( - final ResultExceptionHandler<Collection<I>, DbException> handler) { + final ResultExceptionHandler<ThreadItemList<I>, DbException> handler) { checkGroupId(); runOnDbThread(new Runnable() { @Override @@ -293,11 +311,19 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T @DatabaseExecutor protected abstract void deleteNamedGroup(G groupItem) throws DbException; - private List<I> buildItems(Collection<H> headers) { - List<I> items = new ArrayList<>(); + private ThreadItemList<I> buildItems(Collection<H> headers) { + ThreadItemList<I> items = new ThreadItemListImpl<>(); for (H h : headers) { 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; } diff --git a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java index 8a3461c2eb1c4472cbf86a1d20db8ce08f06e8ff..96ffc88eb15de617846e5c5aba9d4c6242651f8d 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/forum/ForumActivityTest.java @@ -13,6 +13,8 @@ import org.briarproject.briar.BuildConfig; import org.briarproject.briar.android.TestBriarApplication; import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler; 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.Test; import org.junit.runner.RunWith; @@ -23,10 +25,7 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.List; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -81,7 +80,7 @@ public class ForumActivityTest { private TestForumActivity forumActivity; @Captor - private ArgumentCaptor<UiResultExceptionHandler<Collection<ForumItem>, DbException>> + private ArgumentCaptor<UiResultExceptionHandler<ThreadItemList<ForumItem>, DbException>> rc; @Before @@ -93,7 +92,7 @@ public class ForumActivityTest { .withIntent(intent).create().resume().get(); } - private List<ForumItem> getDummyData() { + private ThreadItemList<ForumItem> getDummyData() { ForumItem[] forumItems = new ForumItem[6]; for (int i = 0; i < forumItems.length; i++) { AuthorId authorId = new AuthorId(TestUtils.getRandomId()); @@ -103,13 +102,15 @@ public class ForumActivityTest { AUTHORS[i], System.currentTimeMillis(), author, UNKNOWN); forumItems[i].setLevel(LEVELS[i]); } - return new ArrayList<>(Arrays.asList(forumItems)); + ThreadItemList<ForumItem> list = new ThreadItemListImpl<>(); + list.addAll(Arrays.asList(forumItems)); + return list; } @Test public void testNestedEntries() { ForumController mc = forumActivity.getController(); - List<ForumItem> dummyData = getDummyData(); + ThreadItemList<ForumItem> dummyData = getDummyData(); verify(mc, times(1)).loadItems(rc.capture()); rc.getValue().onResult(dummyData); ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter(); diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java index 44c26fef4d583fc947e5e59e35954513c5ec3d98..5d6418f0aceaf17b46714509ec96c881187a31f1 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageTracker.java @@ -7,6 +7,8 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; +import javax.annotation.Nullable; + @NotNullByDefault public interface MessageTracker { @@ -38,6 +40,19 @@ public interface MessageTracker { void trackMessage(Transaction txn, GroupId g, long timestamp, boolean read) 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. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerConstants.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerConstants.java index ca689fc1ca2543ecb6ae80e922831233a9daf319..81a1d603bfedd505c1b10f3ec4d321d862c27067 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerConstants.java @@ -2,6 +2,7 @@ package org.briarproject.briar.client; public interface MessageTrackerConstants { + String GROUP_KEY_STORED_MESSAGE_ID = "storedMessageId"; String GROUP_KEY_MSG_COUNT = "messageCount"; String GROUP_KEY_UNREAD_COUNT = "unreadCount"; String GROUP_KEY_LATEST_MSG = "latestMessageTime"; diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java index bd230dca8ea0ab8efda10cc9f1c6744a0186bbda..716b1308eeb93c49fe6b81dc3627928882428a9f 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/MessageTrackerImpl.java @@ -13,11 +13,13 @@ import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.MessageTracker; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; 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_STORED_MESSAGE_ID; import static org.briarproject.briar.client.MessageTrackerConstants.GROUP_KEY_UNREAD_COUNT; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; @@ -57,6 +59,30 @@ class MessageTrackerImpl implements MessageTracker { 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 public GroupCount getGroupCount(GroupId g) throws DbException { GroupCount count; diff --git a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java index 30567fdf225431c37ef54d27fc1908c2d2e7c9e1..09805ce44b1cb4d07cc45796ed5b830abab7e7a7 100644 --- a/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/forum/ForumManagerTest.java @@ -1,7 +1,10 @@ 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.MessageId; 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.ForumManager; 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.BriarIntegrationTestComponent; import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -223,4 +227,18 @@ public class ForumManagerTest 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)); + } + }