mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 23:47:26 +00:00
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:
@@ -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'
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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) ->
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user