diff --git a/app/build.gradle b/app/build.gradle index 56a7817529..d8f19fb7f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java index 2e0e0f32c1..8826a936d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java @@ -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 { +class ConversationListAdapter extends ListAdapter { private static final int TYPE_THREAD = 1; private static final int TYPE_ACTION = 2; @@ -46,6 +48,8 @@ class ConversationListAdapter extends PagedListAdapter typingSet = new HashSet<>(); + private PagingController pagingController; + protected ConversationListAdapter(@NonNull GlideRequests glideRequests, @NonNull OnConversationClickListener onConversationClickListener) { @@ -156,6 +160,19 @@ class ConversationListAdapter extends PagedListAdapter typingThreadSet) { this.typingSet.clear(); this.typingSet.addAll(typingThreadSet); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index d3d8013c05..2739c12a5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -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 { - - public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1); +abstract class ConversationListDataSource implements PagedDataSource { 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 callback) { - long start = System.currentTimeMillis(); + public int size() { + return getTotalCount(); + } - List conversations = new ArrayList<>(params.requestedLoadSize); - int totalCount = getTotalCount(); - int effectiveCount = params.requestedStartPosition; + @Override + public @NonNull List load(int start, int length, @NonNull CancellationSignal cancellationSignal) { + Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName()); + + List conversations = new ArrayList<>(length); List 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 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 callback) { - long start = System.currentTimeMillis(); - - List conversations = new ArrayList<>(params.loadSize); - List 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 { - - 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 create() { - return ConversationListDataSource.create(context, invalidator, isArchived); - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index efc6652659..a301e85f33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -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 conversationList) { + defaultAdapter.submitList(conversationList); onPostSubmitList(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java index ba734b488d..e3d0da4996 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -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; - private final MutableLiveData searchResult; - private final LiveData 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; + private final MutableLiveData searchResult; + private final PagedData pagedData; + private final LiveData 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 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> 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 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 hasNoConversations() { - return Transformations.map(getConversationList(), ConversationList::isEmpty); + return hasNoConversations; } @NonNull LiveData getSearchResult() { @@ -105,12 +90,16 @@ class ConversationListViewModel extends ViewModel { return megaphone; } - @NonNull LiveData getConversationList() { - return conversationList; + @NonNull LiveData> 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 conversations; - private final int archivedCount; - private final int pinnedCount; - - ConversationList(PagedList conversations, int archivedCount, int pinnedCount) { - this.conversations = conversations; - this.archivedCount = archivedCount; - this.pinnedCount = pinnedCount; - } - - PagedList getConversations() { - return conversations; - } - - int getArchivedCount() { - return archivedCount; - } - - public int getPinnedCount() { - return pinnedCount; - } - - boolean isEmpty() { - return conversations.isEmpty() && archivedCount == 0; - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java index 4c817751d5..f86a3da058 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java @@ -65,7 +65,11 @@ public final class LiveDataUtil { MediatorLiveData 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; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java b/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java index 9c3f24e61d..bb7bbeded7 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java @@ -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 diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index 3d669c544e..2e5771aa56 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -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'],