Use our own homemade paging library for conversation paging.

I made the lib, and Alan made the build actually work.

Co-authored-by: Alan Evans <alan@signal.org>
This commit is contained in:
Greyson Parrelli
2020-12-03 13:19:03 -05:00
parent ac41f3d662
commit 31960b53a0
49 changed files with 1596 additions and 243 deletions

View File

@@ -2,26 +2,6 @@ import org.signal.signing.ApkSignerUtil
import java.security.MessageDigest
buildscript {
repositories {
google()
maven {
url "https://repo1.maven.org/maven2"
}
jcenter {
content {
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
includeGroupByRegex "com\\.archinamon.*"
}
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs'
@@ -94,9 +74,10 @@ def abiPostFix = ['universal' : 0,
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
android {
buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK
flavorDimensions 'distribution', 'environment'
compileSdkVersion 30
buildToolsVersion '30.0.2'
useLibrary 'org.apache.http.legacy'
dexOptions {
@@ -118,8 +99,9 @@ android {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
minSdkVersion 19
targetSdkVersion 30
minSdkVersion MINIMUM_SDK
targetSdkVersion TARGET_SDK
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -167,8 +149,8 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
packagingOptions {
@@ -218,6 +200,7 @@ android {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
}
release {
minifyEnabled true
@@ -350,6 +333,7 @@ dependencies {
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation project(':libsignal-service')
implementation project(':paging')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.5'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'

View File

@@ -31,8 +31,10 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -66,7 +68,7 @@ import java.util.Set;
* the "footer" is at the top, and we refer to the "next" record as having a lower index.
*/
public class ConversationAdapter
extends PagedListAdapter<ConversationMessage, RecyclerView.ViewHolder>
extends ListAdapter<ConversationMessage, RecyclerView.ViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
{
@@ -100,6 +102,7 @@ public class ConversationAdapter
private ConversationMessage recordToPulse;
private View headerView;
private View footerView;
private PagingController pagingController;
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@@ -107,7 +110,18 @@ public class ConversationAdapter
@Nullable ItemClickListener clickListener,
@NonNull Recipient recipient)
{
super(new DiffCallback());
super(new DiffUtil.ItemCallback<ConversationMessage>() {
@Override
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
return oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
}
@Override
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
return false;
}
});
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
@@ -244,26 +258,6 @@ public class ConversationAdapter
}
}
@Override
public void submitList(@Nullable PagedList<ConversationMessage> pagedList) {
cleanFastRecords();
super.submitList(pagedList);
}
@Override
protected @Nullable ConversationMessage getItem(int position) {
position = hasHeader() ? position - 1 : position;
if (position == -1) {
return null;
} else if (position < fastRecords.size()) {
return fastRecords.get(position);
} else {
int correctedPosition = position - fastRecords.size();
return super.getItem(correctedPosition);
}
}
@Override
public int getItemCount() {
boolean hasHeader = headerView != null;
@@ -306,12 +300,37 @@ public class ConversationAdapter
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
}
public @Nullable ConversationMessage getItem(int position) {
position = hasHeader() ? position - 1 : position;
if (position == -1) {
return null;
} else if (position < fastRecords.size()) {
return fastRecords.get(position);
} else {
int correctedPosition = position - fastRecords.size();
if (pagingController != null) {
pagingController.onDataNeededAroundIndex(correctedPosition);
}
return super.getItem(correctedPosition);
}
}
public void submitList(@Nullable List<ConversationMessage> pagedList) {
cleanFastRecords();
super.submitList(pagedList);
}
public void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
}
boolean hasNoConversationMessages() {
return super.getItemCount() + fastRecords.size() == 0;
return getItemCount() + fastRecords.size() == 0;
}
/**
@@ -576,19 +595,6 @@ public class ConversationAdapter
}
}
private static class DiffCallback extends DiffUtil.ItemCallback<ConversationMessage> {
@Override
public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
return oldItem.getMessageRecord().isMms() == newItem.getMessageRecord().isMms() && oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId();
}
@Override
public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) {
// Corner rounding is not part of the model, so we can't use this yet
return false;
}
}
interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(ConversationMessage item);
void onItemLongClick(View maskTarget, ConversationMessage item);

View File

@@ -1,135 +1,85 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.database.ContentObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import com.annimon.stream.Stream;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Core data source for loading an individual conversation.
*/
@Trace
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
class ConversationDataSource implements PagedDataSource<ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation", 1, 1);
private final Context context;
private final long threadId;
private ConversationDataSource(@NonNull Context context,
long threadId,
@NonNull Invalidator invalidator)
{
this.context = context;
this.threadId = threadId;
ContentObserver contentObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
invalidate();
context.getContentResolver().unregisterContentObserver(this);
}
};
invalidator.observe(() -> {
invalidate();
context.getContentResolver().unregisterContentObserver(contentObserver);
});
context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadId), true, contentObserver);
ConversationDataSource(@NonNull Context context, long threadId) {
this.context = context;
this.threadId = threadId;
}
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
public int size() {
long startTime = System.currentTimeMillis();
int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.requestedLoadSize);
int totalCount = db.getConversationCount(threadId);
int effectiveCount = params.requestedStartPosition;
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
MessageRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
records.add(record);
mentionHelper.add(record);
effectiveCount++;
}
}
long mentionStart = System.currentTimeMillis();
mentionHelper.fetchMentions(context);
if (!isInvalid()) {
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
List<ConversationMessage> items = Stream.of(result.getItems())
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items, params.requestedStartPosition, result.getTotal());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms (mentions: " + (System.currentTimeMillis() - mentionStart) + " ms) | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal());
} else {
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + " -- invalidated");
}
return size;
}
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
public @NonNull List<ConversationMessage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
List<MessageRecord> records = new ArrayList<>(params.loadSize);
List<MessageRecord> records = new ArrayList<>(length);
MentionHelper mentionHelper = new MentionHelper();
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, start, length))) {
MessageRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
records.add(record);
mentionHelper.add(record);
}
}
long mentionStart = System.currentTimeMillis();
stopwatch.split("messages");
mentionHelper.fetchMentions(context);
List<ConversationMessage> items = Stream.of(records)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
callback.onResult(items);
stopwatch.split("mentions");
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms (mentions: " + (System.currentTimeMillis() - mentionStart) + " ms) | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
List<ConversationMessage> messages = Stream.of(records)
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
.toList();
stopwatch.split("conversion");
stopwatch.stop(TAG);
return messages;
}
private static class MentionHelper {
@@ -151,22 +101,4 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
return messageIdToMentions.get(id);
}
}
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
private final Context context;
private final long threadId;
private final Invalidator invalidator;
Factory(Context context, long threadId, @NonNull Invalidator invalidator) {
this.context = context;
this.threadId = threadId;
this.invalidator = invalidator;
}
@Override
public @NonNull DataSource<Integer, ConversationMessage> create() {
return new ConversationDataSource(context, threadId, invalidator);
}
}
}

View File

@@ -250,13 +250,9 @@ public class ConversationFragment extends LoggingFragment {
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
conversationViewModel.getMessages().observe(this, list -> {
if (getListAdapter() != null && !list.getDataSource().isInvalid()) {
Log.i(TAG, "submitList");
getListAdapter().submitList(list);
} else if (list.getDataSource().isInvalid()) {
Log.i(TAG, "submitList skipped an invalid list");
}
getListAdapter().submitList(list);
});
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
conversationViewModel.getShowMentionsButton().observe(this, shouldShow -> {
@@ -506,6 +502,7 @@ public class ConversationFragment extends LoggingFragment {
if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
adapter.setPagingController(conversationViewModel.getPagingController());
list.setAdapter(adapter);
setStickyHeaderDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool());
@@ -1006,6 +1003,7 @@ public class ConversationFragment extends LoggingFragment {
}
private void moveToPosition(int position, @Nullable Runnable onMessageNotFound) {
Log.d(TAG, "moveToPosition(" + position + ")");
conversationViewModel.onConversationDataAvailable(threadId, position);
snapToTopDataObserver.buildScrollPosition(position)
.withOnPerformScroll(((layoutManager, p) ->

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.database.ContentObserver;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
@@ -10,16 +11,17 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.signal.paging.ProxyPagingController;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
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.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.whispersystems.libsignal.util.Pair;
import java.util.List;
@@ -29,20 +31,22 @@ class ConversationViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationViewModel.class);
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final MutableLiveData<Long> threadId;
private final LiveData<PagedList<ConversationMessage>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final Invalidator invalidator;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final LiveData<Boolean> canShowAsBubble;
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final MutableLiveData<Long> threadId;
private final LiveData<List<ConversationMessage>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final LiveData<Boolean> canShowAsBubble;
private final ProxyPagingController pagingController;
private final ContentObserver messageObserver;
private ConversationIntents.Args args;
private int jumpToPosition;
private int jumpToPosition;
private boolean hasRegisteredObserver;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
@@ -50,9 +54,15 @@ class ConversationViewModel extends ViewModel {
this.conversationRepository = new ConversationRepository();
this.recentMedia = new MutableLiveData<>();
this.threadId = new MutableLiveData<>();
this.invalidator = new Invalidator();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.pagingController = new ProxyPagingController();
this.messageObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
pagingController.onDataInvalidated();
}
};
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
@@ -62,13 +72,7 @@ class ConversationViewModel extends ViewModel {
return conversationData;
});
LiveData<Pair<Long, PagedList<ConversationMessage>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
DataSource.Factory<Integer, ConversationMessage> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25)
.build();
LiveData<Pair<Long, PagedData<ConversationMessage>>> pagedDataForThreadId = Transformations.map(metadata, data -> {
final int startPosition;
if (data.shouldJumpToMessage()) {
startPosition = data.getJumpToPosition();
@@ -80,23 +84,31 @@ class ConversationViewModel extends ViewModel {
startPosition = data.getThreadSize();
}
Log.d(TAG, "Starting at position startPosition: " + startPosition + " jumpToPosition: " + jumpToPosition + " lastSeenPosition: " + data.getLastSeenPosition() + " lastScrolledPosition: " + data.getLastScrolledPosition());
if (hasRegisteredObserver) {
context.getContentResolver().unregisterContentObserver(messageObserver);
}
return Transformations.map(new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationDataSource.EXECUTOR)
.setInitialLoadKey(Math.max(startPosition, 0))
.build(),
input -> new Pair<>(data.getThreadId(), input));
context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(data.getThreadId()), true, messageObserver);
hasRegisteredObserver = true;
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId());
PagingConfig config = new PagingConfig.Builder()
.setPageSize(25)
.setBufferPages(1)
.setStartIndex(Math.max(startPosition, 0))
.build();
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
return new Pair<>(data.getThreadId(), PagedData.create(dataSource, config));
});
this.messages = Transformations.map(messagesForThreadId, Pair::second);
this.messages = Transformations.switchMap(pagedDataForThreadId, pair -> {
pagingController.set(pair.second().getController());
return pair.second().getData();
});
LiveData<DistinctConversationDataByThreadId> distinctData = LiveDataUtil.combineLatest(messagesForThreadId,
metadata,
(m, data) -> new DistinctConversationDataByThreadId(data));
conversationMetadata = Transformations.map(Transformations.distinctUntilChanged(distinctData), DistinctConversationDataByThreadId::getConversationData);
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
conversationMetadata = Transformations.switchMap(messages, m -> metadata);
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
}
void onAttachmentKeyboardOpen() {
@@ -144,10 +156,14 @@ class ConversationViewModel extends ViewModel {
return conversationMetadata;
}
@NonNull LiveData<PagedList<ConversationMessage>> getMessages() {
@NonNull LiveData<List<ConversationMessage>> getMessages() {
return messages;
}
@NonNull PagingController getPagingController() {
return pagingController;
}
long getLastSeen() {
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
}
@@ -167,7 +183,7 @@ class ConversationViewModel extends ViewModel {
@Override
protected void onCleared() {
super.onCleared();
invalidator.invalidate();
context.getContentResolver().unregisterContentObserver(messageObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@@ -177,29 +193,4 @@ class ConversationViewModel extends ViewModel {
return modelClass.cast(new ConversationViewModel());
}
}
private static class DistinctConversationDataByThreadId {
private final ConversationData conversationData;
private DistinctConversationDataByThreadId(@NonNull ConversationData conversationData) {
this.conversationData = conversationData;
}
public @NonNull ConversationData getConversationData() {
return conversationData;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DistinctConversationDataByThreadId that = (DistinctConversationDataByThreadId) o;
return Objects.equals(conversationData.getThreadId(), that.conversationData.getThreadId());
}
@Override
public int hashCode() {
return Objects.hash(conversationData.getThreadId());
}
}
}

View File

@@ -6,6 +6,8 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.logging.Log;
import java.util.Objects;
/**
@@ -20,6 +22,8 @@ import java.util.Objects;
*/
public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver {
private static final String TAG = Log.tag(SnapToTopDataObserver.class);
private final RecyclerView recyclerView;
private final LinearLayoutManager layoutManager;
private final Deferred deferred;
@@ -83,13 +87,19 @@ public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver {
Objects.requireNonNull(scrollRequestValidator, "Cannot request positions when SnapToTopObserver was initialized without a validator.");
if (!scrollRequestValidator.isPositionStillValid(position)) {
Log.d(TAG, "requestScrollPositionInternal(" + position + ") Invalid");
onInvalidPosition.run();
} else if (scrollRequestValidator.isItemAtPositionLoaded(position)) {
onPerformScroll.onPerformScroll(layoutManager, position);
onScrollRequestComplete.run();
Log.d(TAG, "requestScrollPositionInternal(" + position + ") Scrolling");
onPerformScroll.onPerformScroll(layoutManager, position);
onScrollRequestComplete.run();
} else {
Log.d(TAG, "requestScrollPositionInternal(" + position + ") Deferring");
deferred.setDeferred(true);
deferred.defer(() -> requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition));
deferred.defer(() -> {
Log.d(TAG, "requestScrollPositionInternal(" + position + ") Executing deferred");
requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition);
});
}
}
@@ -111,7 +121,8 @@ public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver {
if (newItemPosition != 0 ||
recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE ||
recyclerView.canScrollVertically(layoutManager.getReverseLayout() ? 1 : -1)) {
recyclerView.canScrollVertically(layoutManager.getReverseLayout() ? 1 : -1))
{
return;
}