Switch the conversation list to our own paging library.

This commit is contained in:
Greyson Parrelli
2020-12-19 18:22:08 -05:00
committed by Alan Evans
parent b7477d287b
commit 4b5f1d64e6
8 changed files with 96 additions and 185 deletions

View File

@@ -311,8 +311,6 @@ dependencies {
implementation "androidx.camera:camera-view:1.0.0-alpha18"
implementation "androidx.concurrent:concurrent-futures:1.0.0"
implementation "androidx.autofill:autofill:1.0.0"
implementation "androidx.paging:paging-common:2.1.2"
implementation "androidx.paging:paging-runtime:2.1.2"
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'

View File

@@ -7,10 +7,12 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
@@ -28,7 +30,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> {
private static final int TYPE_THREAD = 1;
private static final int TYPE_ACTION = 2;
@@ -46,6 +48,8 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private PagingController pagingController;
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
@NonNull OnConversationClickListener onConversationClickListener)
{
@@ -156,6 +160,19 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
}
}
@Override
protected Conversation getItem(int position) {
if (pagingController != null) {
pagingController.onDataNeededAroundIndex(position);
}
return super.getItem(position);
}
public void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}
void setTypingThreads(@NonNull Set<Long> typingThreadSet) {
this.typingSet.clear();
this.typingSet.addAll(typingThreadSet);

View File

@@ -7,110 +7,66 @@ import android.database.MergeCursor;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.tracing.Trace;
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.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
@Trace
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
abstract class ConversationListDataSource implements PagedDataSource<Conversation> {
private static final String TAG = Log.tag(ConversationListDataSource.class);
protected final ThreadDatabase threadDatabase;
protected ConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
protected ConversationListDataSource(@NonNull Context context) {
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
DatabaseObserver.Observer observer = new DatabaseObserver.Observer() {
@Override
public void onChanged() {
invalidate();
ApplicationDependencies.getDatabaseObserver().unregisterObserver(this);
}
};
invalidator.observe(() -> {
invalidate();
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
});
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
}
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
else return new ArchivedConversationListDataSource(context, invalidator);
public static ConversationListDataSource create(@NonNull Context context, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context);
else return new ArchivedConversationListDataSource(context);
}
@Override
public final void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Conversation> callback) {
long start = System.currentTimeMillis();
public int size() {
return getTotalCount();
}
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
int totalCount = getTotalCount();
int effectiveCount = params.requestedStartPosition;
@Override
public @NonNull List<Conversation> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName());
List<Conversation> conversations = new ArrayList<>(length);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
try (ConversationReader reader = new ConversationReader(getCursor(start, length))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
effectiveCount++;
}
}
ApplicationDependencies.getRecipientCache().addToCache(recipients);
if (!isInvalid()) {
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal() + ", class: " + getClass().getSimpleName());
} else {
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + " -- invalidated");
}
}
@Override
public final void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Conversation> callback) {
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.loadSize);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.startPosition, params.loadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
}
}
stopwatch.split("cursor");
ApplicationDependencies.getRecipientCache().addToCache(recipients);
callback.onResult(conversations);
stopwatch.split("cache-recipients");
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | start: " + params.startPosition + ", size: " + params.loadSize + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
stopwatch.stop(TAG);
return conversations;
}
protected abstract int getTotalCount();
@@ -118,8 +74,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
ArchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
ArchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -141,8 +97,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private int archivedCount;
private int unpinnedCount;
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
UnarchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -212,22 +168,4 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
return archivedCount != 0;
}
}
static class Factory extends DataSource.Factory<Integer, Conversation> {
private final Context context;
private final Invalidator invalidator;
private final boolean isArchived;
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
this.context = context;
this.invalidator = invalidator;
this.isArchived = isArchived;
}
@Override
public @NonNull DataSource<Integer, Conversation> create() {
return ConversationListDataSource.create(context, invalidator, isArchived);
}
}
}

View File

@@ -242,8 +242,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.execute();
});
initializeListAdapters();
initializeViewModel();
initializeListAdapters();
initializeTypingObserver();
initializeSearchListener();
@@ -502,6 +502,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return;
}
if (adapter instanceof ConversationListAdapter) {
((ConversationListAdapter) adapter).setPagingController(viewModel.getPagingController());
}
list.setAdapter(adapter);
if (adapter == defaultAdapter) {
@@ -820,13 +824,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
if (conversationList.getConversations().isDetached()) {
return;
}
defaultAdapter.submitList(conversationList.getConversations());
private void onSubmitList(@NonNull List<Conversation> conversationList) {
defaultAdapter.submitList(conversationList);
onPostSubmitList();
}

View File

@@ -6,15 +6,13 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
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.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -26,24 +24,27 @@ import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import java.util.Objects;
import java.util.List;
class ConversationListViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationListViewModel.class);
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final LiveData<ConversationList> conversationList;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final DatabaseObserver.Observer observer;
private final Invalidator invalidator;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final PagedData<Conversation> pagedData;
private final LiveData<Boolean> hasNoConversations;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final DatabaseObserver.Observer observer;
private final Invalidator invalidator;
private String lastQuery;
private int pinnedCount;
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
this.megaphone = new MutableLiveData<>();
@@ -52,49 +53,33 @@ class ConversationListViewModel extends ViewModel {
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
this.debouncer = new Debouncer(300);
this.invalidator = new Invalidator();
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
new PagingConfig.Builder()
.setPageSize(15)
.setBufferPages(2)
.build());
this.observer = () -> {
if (!TextUtils.isEmpty(getLastQuery())) {
searchRepository.query(getLastQuery(), searchResult::postValue);
}
pagedData.getController().onDataInvalidated();
};
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15)
.setInitialLoadSizeHint(30)
.setEnablePlaceholders(true)
.build();
this.hasNoConversations = LiveDataUtil.mapAsync(pagedData.getData(), conversations -> {
pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
LiveData<PagedList<Conversation>> conversationList = new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
.setInitialLoadKey(0)
.build();
if (conversations.size() > 0) {
return false;
} else {
return DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount() == 0;
}
});
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
this.conversationList = Transformations.switchMap(conversationList, conversation -> {
if (conversation.getDataSource().isInvalid()) {
Log.w(TAG, "Received an invalid conversation list. Ignoring.");
return new MutableLiveData<>();
}
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
if (isArchived) {
updated.postValue(new ConversationList(conversation, 0, 0));
} else {
SignalExecutors.BOUNDED.execute(() -> {
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
int pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
updated.postValue(new ConversationList(conversation, archiveCount, pinnedCount));
});
}
return updated;
});
}
public LiveData<Boolean> hasNoConversations() {
return Transformations.map(getConversationList(), ConversationList::isEmpty);
return hasNoConversations;
}
@NonNull LiveData<SearchResult> getSearchResult() {
@@ -105,12 +90,16 @@ class ConversationListViewModel extends ViewModel {
return megaphone;
}
@NonNull LiveData<ConversationList> getConversationList() {
return conversationList;
@NonNull LiveData<List<Conversation>> getConversationList() {
return pagedData.getData();
}
@NonNull PagingController getPagingController() {
return pagedData.getController();
}
public int getPinnedCount() {
return Objects.requireNonNull(getConversationList().getValue()).pinnedCount;
return pinnedCount;
}
void onVisible() {
@@ -168,32 +157,4 @@ class ConversationListViewModel extends ViewModel {
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
}
}
final static class ConversationList {
private final PagedList<Conversation> conversations;
private final int archivedCount;
private final int pinnedCount;
ConversationList(PagedList<Conversation> conversations, int archivedCount, int pinnedCount) {
this.conversations = conversations;
this.archivedCount = archivedCount;
this.pinnedCount = pinnedCount;
}
PagedList<Conversation> getConversations() {
return conversations;
}
int getArchivedCount() {
return archivedCount;
}
public int getPinnedCount() {
return pinnedCount;
}
boolean isEmpty() {
return conversations.isEmpty() && archivedCount == 0;
}
}
}

View File

@@ -65,7 +65,11 @@ public final class LiveDataUtil {
MediatorLiveData<B> outputLiveData = new MediatorLiveData<>();
Executor liveDataExecutor = new SerialMonoLifoExecutor(executor);
outputLiveData.addSource(source, currentValue -> liveDataExecutor.execute(() -> outputLiveData.postValue(backgroundFunction.apply(currentValue))));
outputLiveData.addSource(source, currentValue -> {
liveDataExecutor.execute(() -> {
outputLiveData.postValue(backgroundFunction.apply(currentValue));
});
});
return outputLiveData;
}

View File

@@ -55,7 +55,7 @@ public class UnarchivedConversationListDataSourceTest {
when(DatabaseFactory.getThreadDatabase(any())).thenReturn(threadDatabase);
when(ApplicationDependencies.getDatabaseObserver()).thenReturn(mock(DatabaseObserver.class));
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ApplicationProvider.getApplicationContext(), mock(Invalidator.class));
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(mock(Application.class));
}
@Test

View File

@@ -156,12 +156,6 @@ dependencyVerification {
['androidx.navigation:navigation-ui:2.1.0',
'1ec0558d692982c5bcfcca6de5b5972723e6b4a9870aa7fc1eddf5e869f116ed'],
['androidx.paging:paging-common:2.1.2',
'891dd24bad908d5d866d7d3545114ab2d26994847cd0200ac68477287c0710b5'],
['androidx.paging:paging-runtime:2.1.2',
'4e81d8ab584a184e2781c6f0d50b6f00acd11741f759270e7c976ef3307d78a7'],
['androidx.preference:preference:1.0.0',
'ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1'],