Migrate ConversationList to paging library and apply abstractions to conversation.

This commit is contained in:
Alex Hart
2020-06-12 15:11:36 -03:00
committed by Greyson Parrelli
parent ce940235b0
commit 49f75d7036
25 changed files with 1212 additions and 622 deletions

View File

@@ -479,7 +479,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
return headerView != null;
}
private boolean hasFooter() {
public boolean hasFooter() {
return footerView != null;
}

View File

@@ -12,6 +12,7 @@ final class ConversationData {
private final boolean isMessageRequestAccepted;
private final boolean hasPreMessageRequestMessages;
private final int jumpToPosition;
private final int threadSize;
ConversationData(long threadId,
long lastSeen,
@@ -20,7 +21,8 @@ final class ConversationData {
boolean hasSent,
boolean isMessageRequestAccepted,
boolean hasPreMessageRequestMessages,
int jumpToPosition)
int jumpToPosition,
int threadSize)
{
this.threadId = threadId;
this.lastSeen = lastSeen;
@@ -30,6 +32,7 @@ final class ConversationData {
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
this.jumpToPosition = jumpToPosition;
this.threadSize = threadSize;
}
public long getThreadId() {
@@ -71,4 +74,8 @@ final class ConversationData {
int getJumpToPosition() {
return jumpToPosition;
}
int getThreadSize() {
return threadSize;
}
}

View File

@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import java.util.ArrayList;
import java.util.List;
@@ -76,9 +78,9 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
if (!isInvalid()) {
SizeFixResult result = ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.messages, params.requestedStartPosition, result.total);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
}
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
@@ -103,54 +105,6 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}
private static @NonNull SizeFixResult ensureMultipleOfPageSize(@NonNull List<MessageRecord> records,
int startPosition,
int pageSize,
int total)
{
if (records.size() + startPosition == total || (records.size() != 0 && records.size() % pageSize == 0)) {
return new SizeFixResult(records, total);
}
if (records.size() < pageSize) {
Log.w(TAG, "Hit a miscalculation where we don't have the full dataset, but it's smaller than a page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
return new SizeFixResult(records, records.size() + startPosition);
}
Log.w(TAG, "Hit a miscalculation where our data size isn't a multiple of the page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
int overflow = records.size() % pageSize;
return new SizeFixResult(records.subList(0, records.size() - overflow), total);
}
private static class SizeFixResult {
final List<MessageRecord> messages;
final int total;
private SizeFixResult(@NonNull List<MessageRecord> messages, int total) {
this.messages = messages;
this.total = total;
}
}
interface DataUpdatedCallback {
void onDataUpdated();
}
static class Invalidator {
private Runnable callback;
synchronized void invalidate() {
if (callback != null) {
callback.run();
}
}
private synchronized void observe(@NonNull Runnable callback) {
this.callback = callback;
}
}
static class Factory extends DataSource.Factory<Integer, MessageRecord> {
private final Context context;

View File

@@ -21,7 +21,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -113,6 +112,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -163,8 +163,7 @@ public class ConversationFragment extends Fragment {
private ConversationBannerView emptyConversationBanner;
private MessageRequestViewModel messageRequestViewModel;
private ConversationViewModel conversationViewModel;
private Deferred deferred = new Deferred();
private SnapToTopDataObserver snapToTopDataObserver;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
@@ -198,6 +197,8 @@ public class ConversationFragment extends Fragment {
list.setLayoutManager(layoutManager);
list.setItemAnimator(null);
snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
if (FeatureFlags.messageRequests()) {
conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false);
}
@@ -226,7 +227,7 @@ public class ConversationFragment extends Fragment {
Log.i(TAG, "submitList skipped an invalid list");
}
});
conversationViewModel.getConversationMetadata().observe(this, data -> deferred.defer(() -> presentConversationMetadata(data)));
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
return view;
}
@@ -329,7 +330,7 @@ public class ConversationFragment extends Fragment {
}
int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition());
scrollToPosition(position);
snapToTopDataObserver.requestScrollPosition(position);
}
private void initializeMessageRequestViewModel() {
@@ -423,7 +424,7 @@ public class ConversationFragment extends Fragment {
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
deferred.setDeferred(true);
snapToTopDataObserver.requestScrollPosition(startingPosition);
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
@@ -442,7 +443,7 @@ public class ConversationFragment extends Fragment {
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(new DataObserver());
adapter.registerAdapterDataObserver(snapToTopDataObserver);
setLastSeen(conversationViewModel.getLastSeen());
@@ -563,7 +564,7 @@ public class ConversationFragment extends Fragment {
this.threadId = threadId;
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
deferred.setDeferred(true);
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(threadId, -1);
initializeListAdapter();
}
@@ -883,48 +884,52 @@ public class ConversationFragment extends Fragment {
return;
}
if (FeatureFlags.messageRequests()) {
adapter.setFooterView(conversationBanner);
} else {
adapter.setFooterView(null);
}
setLastSeen(conversation.getLastSeen());
if (FeatureFlags.messageRequests() && !conversation.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
Runnable afterScroll = () -> {
if (FeatureFlags.messageRequests()) {
adapter.setFooterView(conversationBanner);
if (!conversation.isMessageRequestAccepted()) {
snapToTopDataObserver.requestScrollPosition(adapter.getItemCount() - 1);
}
} else {
clearHeaderIfNotTyping(adapter);
adapter.setFooterView(null);
}
}
listener.onCursorChanged();
setLastSeen(conversation.getLastSeen());
if (FeatureFlags.messageRequests() && !conversation.hasPreMessageRequestMessages()) {
clearHeaderIfNotTyping(adapter);
} else {
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
adapter.setHeaderView(unknownSenderView);
} else {
clearHeaderIfNotTyping(adapter);
}
}
listener.onCursorChanged();
};
int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
int lastScrolledPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastScrolledPosition());
if (conversation.shouldJumpToMessage()) {
scrollToStartingPosition(conversation.getJumpToPosition());
if (conversation.getThreadSize() == 0) {
afterScroll.run();
} else if (conversation.shouldJumpToMessage()) {
snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition())
.withOnScrollRequestComplete(() -> {
afterScroll.run();
getListAdapter().pulseHighlightItem(conversation.getJumpToPosition());
})
.submit();
} else if (conversation.isMessageRequestAccepted()) {
scrollToPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition);
snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
.withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, list.getHeight()))
.withOnScrollRequestComplete(afterScroll)
.submit();
} else if (FeatureFlags.messageRequests()) {
list.post(() -> getListLayoutManager().scrollToPosition(adapter.getItemCount() - 1));
}
}
private void scrollToStartingPosition(int startingPosition) {
list.post(() -> {
list.getLayoutManager().scrollToPosition(startingPosition);
getListAdapter().pulseHighlightItem(startingPosition);
});
}
private void scrollToPosition(int position) {
if (position > 0) {
list.post(() -> getListLayoutManager().scrollToPositionWithOffset(position, list.getHeight()));
snapToTopDataObserver.buildScrollPosition(adapter.getItemCount() - 1)
.withOnScrollRequestComplete(afterScroll)
.submit();
}
}
@@ -959,22 +964,16 @@ public class ConversationFragment extends Fragment {
}
private void moveToMessagePosition(int position, @Nullable Runnable onMessageNotFound) {
int itemCount = getListAdapter() != null ? getListAdapter().getItemCount() : 0;
if (position >= 0 && position < itemCount) {
if (getListAdapter().getItem(position) == null) {
conversationViewModel.onConversationDataAvailable(threadId, position);
deferred.setDeferred(true);
deferred.defer(() -> moveToMessagePosition(position, onMessageNotFound));
} else {
scrollToStartingPosition(position);
}
} else {
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
if (onMessageNotFound != null) {
onMessageNotFound.run();
}
}
conversationViewModel.onConversationDataAvailable(threadId, position);
snapToTopDataObserver.buildScrollPosition(position)
.withOnScrollRequestComplete(() -> getListAdapter().pulseHighlightItem(position))
.withOnInvalidPosition(() -> {
if (onMessageNotFound != null) {
onMessageNotFound.run();
}
Log.w(TAG, "[moveToMessagePosition] Tried to navigate to message, but it wasn't found.");
})
.submit();
}
private void maybeShowSwipeToReplyTooltip() {
@@ -1074,44 +1073,6 @@ public class ConversationFragment extends Fragment {
}
}
private class DataObserver extends RecyclerView.AdapterDataObserver {
private final Rect rect = new Rect();
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (deferred.isDeferred()) {
deferred.setDeferred(false);
return;
}
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
return;
}
if (list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
int firstVisibleItem = getListLayoutManager().findFirstVisibleItemPosition();
if (firstVisibleItem == 0) {
View view = getListLayoutManager().findViewByPosition(0);
if (view == null) {
return;
}
view.getDrawingRect(rect);
list.offsetDescendantRectToMyCoords(view, rect);
int bottom = rect.bottom;
list.getDrawingRect(rect);
if (bottom <= rect.bottom) {
getListLayoutManager().scrollToPosition(0);
}
}
}
}
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
@@ -1319,6 +1280,52 @@ public class ConversationFragment extends Fragment {
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
private final class ConversationSnapToTopDataObserver extends SnapToTopDataObserver {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
@Nullable ScrollRequestValidator scrollRequestValidator)
{
super(recyclerView, scrollRequestValidator);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// Do nothing.
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
return;
}
super.onItemRangeInserted(positionStart, itemCount);
}
}
private final class ConversationScrollRequestValidator implements SnapToTopDataObserver.ScrollRequestValidator {
@Override
public boolean isPositionStillValid(int position) {
if (getListAdapter() == null) {
return position >= 0;
} else {
return position >= 0 && position < getListAdapter().getItemCount();
}
}
@Override
public boolean isItemAtPositionLoaded(int position) {
if (getListAdapter() == null) {
return false;
} else if (getListAdapter().hasFooter() && position == getListAdapter().getItemCount() - 1) {
return true;
} else {
return getListAdapter().getItem(position) != null;
}
}
}
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
private final MessageRecord messageRecord;
@@ -1465,33 +1472,4 @@ public class ConversationFragment extends Fragment {
}
}
private static class Deferred {
private Runnable deferred;
private boolean isDeferred;
public void defer(@Nullable Runnable deferred) {
this.deferred = deferred;
executeIfNecessary();
}
public void setDeferred(boolean isDeferred) {
this.isDeferred = isDeferred;
executeIfNecessary();
}
public boolean isDeferred() {
return isDeferred;
}
private void executeIfNecessary() {
if (deferred != null && !isDeferred) {
Runnable local = deferred;
deferred = null;
local.run();
}
}
}
}

View File

@@ -35,7 +35,8 @@ class ConversationRepository {
}
private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
long lastSeen = metadata.getLastSeen();
boolean hasSent = metadata.hasSent();
@@ -58,6 +59,6 @@ class ConversationRepository {
lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled);
}
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition);
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition, threadSize);
}
}

View File

@@ -3,11 +3,8 @@ package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
@@ -15,19 +12,15 @@ import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.thoughtcrime.securesms.conversation.ConversationDataSource.Invalidator;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.whispersystems.libsignal.util.Pair;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
class ConversationViewModel extends ViewModel {
@@ -70,10 +63,12 @@ class ConversationViewModel extends ViewModel {
final int startPosition;
if (data.shouldJumpToMessage()) {
startPosition = data.getJumpToPosition();
} else if (data.shouldScrollToLastSeen()) {
} else if (data.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
startPosition = data.getLastSeenPosition();
} else {
} else if (data.isMessageRequestAccepted()) {
startPosition = data.getLastScrolledPosition();
} else {
startPosition = data.getThreadSize();
}
Log.d(TAG, "Starting at position startPosition: " + startPosition + " jumpToPosition: " + jumpToPosition + " lastSeenPosition: " + data.getLastSeenPosition() + " lastScrolledPosition: " + data.getLastScrolledPosition());
@@ -86,7 +81,7 @@ class ConversationViewModel extends ViewModel {
this.messages = Transformations.map(messagesForThreadId, Pair::second);
LiveData<Long> distinctThread = Transformations.distinctUntilChanged(threadId);
LiveData<Long> distinctThread = Transformations.distinctUntilChanged(Transformations.map(messagesForThreadId, Pair::first));
conversationMetadata = Transformations.switchMap(distinctThread, thread -> metadata);
}