diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt index 24c957bae..9fd3f59a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import org.signal.paging.LivePagedData import org.signal.paging.PagedData import org.signal.paging.PagingConfig import org.signal.paging.PagingController @@ -30,7 +31,7 @@ class ContactSearchViewModel( .setStartIndex(0) .build() - private val pagedData = MutableLiveData>() + private val pagedData = MutableLiveData>() private val configurationStore = Store(ContactSearchState()) private val selectionStore = Store>(emptySet()) @@ -45,7 +46,7 @@ class ContactSearchViewModel( fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) { val pagedDataSource = ContactSearchPagedDataSource(contactSearchConfiguration) - pagedData.value = PagedData.create(pagedDataSource, pagingConfig) + pagedData.value = PagedData.createForLiveData(pagedDataSource, pagingConfig) } fun setQuery(query: String?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java deleted file mode 100644 index b876da918..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import androidx.annotation.NonNull; - -/** - * Represents metadata about a conversation. - */ -public final class ConversationData { - private final long threadId; - private final long lastSeen; - private final int lastSeenPosition; - private final int lastScrolledPosition; - private final boolean hasSent; - private final int jumpToPosition; - private final int threadSize; - private final MessageRequestData messageRequestData; - private final boolean showUniversalExpireTimerMessage; - - ConversationData(long threadId, - long lastSeen, - int lastSeenPosition, - int lastScrolledPosition, - boolean hasSent, - int jumpToPosition, - int threadSize, - @NonNull MessageRequestData messageRequestData, - boolean showUniversalExpireTimerMessage) - { - this.threadId = threadId; - this.lastSeen = lastSeen; - this.lastSeenPosition = lastSeenPosition; - this.lastScrolledPosition = lastScrolledPosition; - this.hasSent = hasSent; - this.jumpToPosition = jumpToPosition; - this.threadSize = threadSize; - this.messageRequestData = messageRequestData; - this.showUniversalExpireTimerMessage = showUniversalExpireTimerMessage; - } - - public long getThreadId() { - return threadId; - } - - long getLastSeen() { - return lastSeen; - } - - int getLastSeenPosition() { - return lastSeenPosition; - } - - int getLastScrolledPosition() { - return lastScrolledPosition; - } - - boolean hasSent() { - return hasSent; - } - - boolean shouldJumpToMessage() { - return jumpToPosition >= 0; - } - - boolean shouldScrollToLastSeen() { - return lastSeenPosition > 0; - } - - int getJumpToPosition() { - return jumpToPosition; - } - - int getThreadSize() { - return threadSize; - } - - @NonNull MessageRequestData getMessageRequestData() { - return messageRequestData; - } - - public boolean showUniversalExpireTimerMessage() { - return showUniversalExpireTimerMessage; - } - - static final class MessageRequestData { - - private final boolean messageRequestAccepted; - private final boolean groupsInCommon; - private final boolean isGroup; - - public MessageRequestData(boolean messageRequestAccepted) { - this(messageRequestAccepted, false, false); - } - - public MessageRequestData(boolean messageRequestAccepted, boolean groupsInCommon, boolean isGroup) { - this.messageRequestAccepted = messageRequestAccepted; - this.groupsInCommon = groupsInCommon; - this.isGroup = isGroup; - } - - public boolean isMessageRequestAccepted() { - return messageRequestAccepted; - } - - public boolean includeWarningUpdateMessage() { - return !messageRequestAccepted && !groupsInCommon; - } - - public boolean isGroup() { - return isGroup; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt new file mode 100644 index 000000000..ece1d40bf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.kt @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.conversation + +/** + * Represents metadata about a conversation. + */ +data class ConversationData( + val threadId: Long, + val lastSeen: Long, + val lastSeenPosition: Int, + val lastScrolledPosition: Int, + val jumpToPosition: Int, + val threadSize: Int, + val messageRequestData: MessageRequestData, + @get:JvmName("showUniversalExpireTimerMessage") val showUniversalExpireTimerMessage: Boolean +) { + + fun shouldJumpToMessage(): Boolean { + return jumpToPosition >= 0 + } + + fun shouldScrollToLastSeen(): Boolean { + return lastSeenPosition > 0 + } + + data class MessageRequestData @JvmOverloads constructor( + val isMessageRequestAccepted: Boolean, + private val groupsInCommon: Boolean = false, + val isGroup: Boolean = false + ) { + + fun includeWarningUpdateMessage(): Boolean { + return !isMessageRequestAccepted && !groupsInCommon + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index 4fb285a60..8ac469779 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -47,25 +47,41 @@ class ConversationDataSource implements PagedDataSource load(int start, int length, @NonNull CancellationSignal cancellationSignal) { Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 47e509bc9..b58ec0c97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -195,8 +195,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private static final int SCROLL_ANIMATION_THRESHOLD = 50; private static final int CODE_ADD_EDIT_CONTACT = 77; - private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); + private final ActionModeCallback actionModeCallback = new ActionModeCallback(); + private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); + private final LifecycleDisposable disposables = new LifecycleDisposable(); private ConversationFragmentListener listener; @@ -241,6 +242,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private MultiselectItemDecoration multiselectItemDecoration; private LifecycleDisposable lifecycleDisposable; + private @Nullable ConversationData conversationData; + private @Nullable ChatWallpaper chatWallpaper; + public static void prepare(@NonNull Context context) { FrameLayout parent = new FrameLayout(context); parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); @@ -263,6 +267,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { + disposables.bindTo(getViewLifecycleOwner()); + final View view = inflater.inflate(R.layout.conversation_fragment, container, false); videoContainer = view.findViewById(R.id.video_container); list = view.findViewById(android.R.id.list); @@ -291,8 +297,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect () -> conversationViewModel.shouldPlayMessageAnimations() && list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE, () -> list.canScrollVertically(1) || list.canScrollVertically(-1)); - multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), - () -> conversationViewModel.getWallpaper().getValue()); + multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), () -> chatWallpaper); list.setHasFixedSize(false); list.setLayoutManager(layoutManager); @@ -333,17 +338,22 @@ public class ConversationFragment extends LoggingFragment implements Multiselect this.messageCountsViewModel = new ViewModelProvider(getParentFragment()).get(MessageCountsViewModel.class); this.conversationViewModel = new ViewModelProvider(getParentFragment(), new ConversationViewModel.Factory()).get(ConversationViewModel.class); - conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors); - conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> { + disposables.add(conversationViewModel.getChatColors().subscribe(recyclerViewColorizer::setChatColors)); + disposables.add(conversationViewModel.getMessageData().subscribe(messageData -> { ConversationAdapter adapter = getListAdapter(); if (adapter != null) { + List messages = messageData.getMessages(); getListAdapter().submitList(messages, () -> { - list.post(() -> conversationViewModel.onMessagesCommitted(messages)); + list.post(() -> { + conversationViewModel.onMessagesCommitted(messages); + }); }); } - }); - conversationViewModel.getConversationMetadata().observe(getViewLifecycleOwner(), this::presentConversationMetadata); + presentConversationMetadata(messageData.getMetadata()); + })); + + disposables.add(conversationViewModel.getWallpaper().subscribe(w -> chatWallpaper = w.orElse(null))); conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> { if (shouldShow) { @@ -367,14 +377,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect updateToolbarDependentMargins(); colorizer = new Colorizer(); - conversationViewModel.getNameColorsMap().observe(getViewLifecycleOwner(), nameColorsMap -> { + disposables.add(conversationViewModel.getNameColorsMap().subscribe(nameColorsMap -> { colorizer.onNameColorsChanged(nameColorsMap); ConversationAdapter adapter = getListAdapter(); if (adapter != null) { adapter.notifyItemRangeChanged(0, adapter.getItemCount(), ConversationAdapter.PAYLOAD_NAME_COLORS); } - }); + })); conversationUpdateTick = new ConversationUpdateTick(this::updateConversationItemTimestamps); getViewLifecycleOwner().getLifecycle().addObserver(conversationUpdateTick); @@ -491,7 +501,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } public void moveToLastSeen() { - if (conversationViewModel.getLastSeenPosition() <= 0) { + int lastSeenPosition = conversationData != null ? conversationData.getLastSeenPosition() : 0; + if (lastSeenPosition <= 0) { Log.i(TAG, "No need to move to last seen."); return; } @@ -501,7 +512,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect return; } - int position = getListAdapter().getAdapterPositionForMessagePosition(conversationViewModel.getLastSeenPosition()); + int position = getListAdapter().getAdapterPositionForMessagePosition(lastSeenPosition); snapToTopDataObserver.requestScrollPosition(position); } @@ -631,9 +642,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } private void initializeResources() { - long oldThreadId = threadId; - - int startingPosition = getStartPosition(); + long oldThreadId = threadId; + int startingPosition = getStartPosition(); this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId()); this.threadId = conversationViewModel.getArgs().getThreadId(); @@ -678,14 +688,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect adapter.registerAdapterDataObserver(snapToTopDataObserver); adapter.registerAdapterDataObserver(new CheckExpirationDataObserver()); - setLastSeen(conversationViewModel.getLastSeen()); + setLastSeen(conversationData != null ? conversationData.getLastSeen() : 0); adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onItemRangeInserted(int positionStart, int itemCount) { + adapter.unregisterAdapterDataObserver(this); startupStopwatch.split("data-set"); SignalLocalMetrics.ConversationOpen.onDataLoaded(); - adapter.unregisterAdapterDataObserver(this); list.post(() -> { startupStopwatch.split("first-render"); startupStopwatch.stop(TAG); @@ -1100,6 +1110,13 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } private void presentConversationMetadata(@NonNull ConversationData conversation) { + if (conversationData != null && conversationData.getThreadId() == conversation.getThreadId()) { + Log.d(TAG, "Already presented conversation data for thread " + threadId); + return; + } + + conversationData = conversation; + ConversationAdapter adapter = getListAdapter(); if (adapter == null) { return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 1b571989e..d65716cf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -108,6 +108,9 @@ import org.thoughtcrime.securesms.PromptMmsActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.ShortcutLauncherActivity; import org.thoughtcrime.securesms.TransportOption; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; +import org.thoughtcrime.securesms.util.LifecycleDisposable; +import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.audio.AudioRecorder; @@ -439,6 +442,8 @@ public class ConversationParentFragment extends Fragment private boolean isSecurityInitialized = false; private boolean isSearchRequested = false; + private final LifecycleDisposable disposables = new LifecycleDisposable(); + private volatile boolean screenInitialized = false; private IdentityRecordList identityRecords = new IdentityRecordList(Collections.emptyList()); @@ -452,6 +457,8 @@ public class ConversationParentFragment extends Fragment @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + disposables.bindTo(getViewLifecycleOwner()); + if (requireActivity() instanceof Callback) { callback = (Callback) requireActivity(); } else if (getParentFragment() instanceof Callback) { @@ -552,7 +559,7 @@ public class ConversationParentFragment extends Fragment initializeInsightObserver(); initializeActionBar(); - viewModel.getStoryViewState(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), titleView::setStoryRingFromState); + disposables.add(viewModel.getStoryViewState().subscribe(titleView::setStoryRingFromState)); requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { @Override @@ -1026,13 +1033,13 @@ public class ConversationParentFragment extends Fragment } hideMenuItem(menu, R.id.menu_create_bubble); - viewModel.canShowAsBubble().observe(getViewLifecycleOwner(), canShowAsBubble -> { + disposables.add(viewModel.canShowAsBubble().subscribe(canShowAsBubble -> { MenuItem item = menu.findItem(R.id.menu_create_bubble); if (item != null) { item.setVisible(canShowAsBubble && !isInBubble()); } - }); + })); if (threadId == -1L) { hideMenuItem(menu, R.id.menu_view_media); @@ -2300,8 +2307,8 @@ public class ConversationParentFragment extends Fragment this.viewModel = new ViewModelProvider(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class); this.viewModel.setArgs(args); - this.viewModel.getWallpaper().observe(getViewLifecycleOwner(), this::updateWallpaper); this.viewModel.getEvents().observe(getViewLifecycleOwner(), this::onViewModelEvent); + disposables.add(this.viewModel.getWallpaper().subscribe(w -> updateWallpaper(w.orElse(null)))); } private void initializeGroupViewModel() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java index 5f6ba9339..4ae6e104b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java @@ -5,10 +5,9 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; @@ -21,26 +20,15 @@ import org.thoughtcrime.securesms.util.ConversationUtil; import java.util.List; import java.util.Optional; -import java.util.concurrent.Executor; class ConversationRepository { + private static final String TAG = Log.tag(ConversationRepository.class); + private final Context context; - private final Executor executor; ConversationRepository() { - this.context = ApplicationDependencies.getApplication(); - this.executor = SignalExecutors.BOUNDED; - } - - LiveData getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) { - MutableLiveData liveData = new MutableLiveData<>(); - - executor.execute(() -> { - liveData.postValue(getConversationDataInternal(threadId, recipient, jumpToPosition)); - }); - - return liveData; + this.context = ApplicationDependencies.getApplication(); } @WorkerThread @@ -54,11 +42,11 @@ class ConversationRepository { } } - private @NonNull ConversationData getConversationDataInternal(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) { + @WorkerThread + public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) { ThreadDatabase.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); int threadSize = SignalDatabase.mmsSms().getConversationCount(threadId); long lastSeen = metadata.getLastSeen(); - boolean hasSent = metadata.hasSent(); int lastSeenPosition = 0; long lastScrolled = metadata.getLastScrolled(); int lastScrolledPosition = 0; @@ -108,6 +96,6 @@ class ConversationRepository { showUniversalExpireTimerUpdate = true; } - return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate); + return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index da400ba3a..c5ef05e76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -5,7 +5,6 @@ import android.app.Application; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveDataReactiveStreams; import androidx.lifecycle.MutableLiveData; @@ -19,9 +18,9 @@ import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; -import org.reactivestreams.Publisher; import org.signal.core.util.MapUtil; import org.signal.core.util.logging.Log; +import org.signal.paging.ObservablePagedData; import org.signal.paging.PagedData; import org.signal.paging.PagingConfig; import org.signal.paging.PagingController; @@ -31,12 +30,12 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette; import org.thoughtcrime.securesms.conversation.colors.NameColor; import org.thoughtcrime.securesms.database.DatabaseObserver; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.LiveGroup; -import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.MediaRepository; import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile; @@ -44,7 +43,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles; import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.DefaultValueLiveData; +import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -63,39 +62,41 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.BackpressureStrategy; -import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.BehaviorSubject; public 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> recentMedia; - private final MutableLiveData threadId; - private final LiveData> messages; - private final LiveData conversationMetadata; - private final MutableLiveData showScrollButtons; - private final MutableLiveData hasUnreadMentions; - private final LiveData canShowAsBubble; - private final ProxyPagingController pagingController; - private final DatabaseObserver.Observer conversationObserver; - private final DatabaseObserver.MessageObserver messageUpdateObserver; - private final DatabaseObserver.MessageObserver messageInsertObserver; - private final MutableLiveData recipientId; - private final LiveData wallpaper; - private final SingleLiveEvent events; - private final LiveData chatColors; - private final MutableLiveData toolbarBottom; - private final MutableLiveData inlinePlayerHeight; - private final LiveData conversationTopMargin; - private final Store threadAnimationStateStore; - private final Observer threadAnimationStateStoreDriver; - private final NotificationProfilesRepository notificationProfilesRepository; - private final MutableLiveData searchQuery; + private final Application context; + private final MediaRepository mediaRepository; + private final ConversationRepository conversationRepository; + private final MutableLiveData> recentMedia; + private final BehaviorSubject threadId; + private final Observable messageData; + private final MutableLiveData showScrollButtons; + private final MutableLiveData hasUnreadMentions; + private final Observable canShowAsBubble; + private final ProxyPagingController pagingController; + private final DatabaseObserver.Observer conversationObserver; + private final DatabaseObserver.MessageObserver messageUpdateObserver; + private final DatabaseObserver.MessageObserver messageInsertObserver; + private final BehaviorSubject recipientId; + private final Observable> wallpaper; + private final SingleLiveEvent events; + private final Observable chatColors; + private final MutableLiveData toolbarBottom; + private final MutableLiveData inlinePlayerHeight; + private final LiveData conversationTopMargin; + private final Store threadAnimationStateStore; + private final Observer threadAnimationStateStoreDriver; + private final NotificationProfilesRepository notificationProfilesRepository; + private final MutableLiveData searchQuery; private final Map> sessionMemberCache = new HashMap<>(); @@ -107,10 +108,8 @@ public class ConversationViewModel extends ViewModel { this.mediaRepository = new MediaRepository(); this.conversationRepository = new ConversationRepository(); this.recentMedia = new MutableLiveData<>(); - this.threadId = new MutableLiveData<>(); this.showScrollButtons = new MutableLiveData<>(false); this.hasUnreadMentions = new MutableLiveData<>(false); - this.recipientId = new MutableLiveData<>(); this.events = new SingleLiveEvent<>(); this.pagingController = new ProxyPagingController<>(); this.conversationObserver = pagingController::onDataInvalidated; @@ -122,65 +121,75 @@ public class ConversationViewModel extends ViewModel { this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false)); this.notificationProfilesRepository = new NotificationProfilesRepository(); this.searchQuery = new MutableLiveData<>(); + this.recipientId = BehaviorSubject.create(); + this.threadId = BehaviorSubject.create(); - LiveData recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved); - LiveData threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new); + BehaviorSubject recipientCache = BehaviorSubject.create(); - LiveData metadata = Transformations.switchMap(threadAndRecipient, d -> { - LiveData conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition); + recipientId + .observeOn(Schedulers.io()) + .distinctUntilChanged() + .map(Recipient::resolved) + .subscribe(recipientCache); - jumpToPosition = -1; + BehaviorSubject conversationMetadata = BehaviorSubject.create(); - return conversationData; - }); + Observable.combineLatest(threadId, recipientCache, Pair::new) + .observeOn(Schedulers.io()) + .distinctUntilChanged() + .map(threadIdAndRecipient -> { + SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted(); + ConversationData conversationData = conversationRepository.getConversationData(threadIdAndRecipient.first(), threadIdAndRecipient.second(), jumpToPosition); + SignalLocalMetrics.ConversationOpen.onMetadataLoaded(); + + jumpToPosition = -1; + + return conversationData; + }) + .subscribe(conversationMetadata); ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver); - LiveData>> pagedDataForThreadId = Transformations.map(metadata, data -> { - int startPosition; - ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData(); + messageData = conversationMetadata + .observeOn(Schedulers.io()) + .switchMap(data -> { + int startPosition; - if (data.shouldJumpToMessage()) { - startPosition = data.getJumpToPosition(); - } else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) { - startPosition = data.getLastSeenPosition(); - } else if (messageRequestData.isMessageRequestAccepted()) { - startPosition = data.getLastScrolledPosition(); - } else { - startPosition = data.getThreadSize(); - } + ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData(); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); - ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver); - ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver); + if (data.shouldJumpToMessage()) { + startPosition = data.getJumpToPosition(); + } else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) { + startPosition = data.getLastSeenPosition(); + } else if (messageRequestData.isMessageRequestAccepted()) { + startPosition = data.getLastScrolledPosition(); + } else { + startPosition = data.getThreadSize(); + } - ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage()); - PagingConfig config = new PagingConfig.Builder().setPageSize(25) - .setBufferPages(3) - .setStartIndex(Math.max(startPosition, 0)) - .build(); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); + ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver); + ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver); - 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)); - }); + ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage(), data.getThreadSize()); + PagingConfig config = new PagingConfig.Builder().setPageSize(25) + .setBufferPages(2) + .setStartIndex(Math.max(startPosition, 0)) + .build(); - this.messages = Transformations.switchMap(pagedDataForThreadId, pair -> { - pagingController.set(pair.second().getController()); - return pair.second().getData(); - }); + Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition()); + ObservablePagedData pagedData = PagedData.createForObservable(dataSource, config); - conversationMetadata = Transformations.switchMap(messages, m -> metadata); - canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble); - wallpaper = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId, - id -> Recipient.live(id).getLiveData()), - Recipient::getWallpaper); + pagingController.set(pagedData.getController()); + return pagedData.getData(); + }) + .observeOn(Schedulers.io()) + .withLatestFrom(conversationMetadata, (messages, metadata) -> new MessageData(metadata, messages)); - EventBus.getDefault().register(this); - - chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId, - id -> Recipient.live(id).getLiveData()), - Recipient::getChatColors); + canShowAsBubble = threadId.observeOn(Schedulers.io()).map(conversationRepository::canShowAsBubble); + wallpaper = recipientCache.map(r -> Optional.ofNullable(r.getWallpaper())).distinctUntilChanged(); + chatColors = recipientCache.map(Recipient::getChatColors).distinctUntilChanged(); threadAnimationStateStore.update(threadId, (id, state) -> { if (state.getThreadId() == id) { @@ -190,7 +199,7 @@ public class ConversationViewModel extends ViewModel { } }); - threadAnimationStateStore.update(metadata, (m, state) -> { + threadAnimationStateStore.update(conversationMetadata, (m, state) -> { if (state.getThreadId() == m.getThreadId()) { return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList()); } else { @@ -200,14 +209,16 @@ public class ConversationViewModel extends ViewModel { this.threadAnimationStateStoreDriver = state -> {}; threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver); + + EventBus.getDefault().register(this); } - LiveData getStoryViewState(@NonNull LifecycleOwner lifecycle) { - Publisher recipientIdPublisher = LiveDataReactiveStreams.toPublisher(lifecycle, recipientId); - Flowable storyViewState = Flowable.fromPublisher(recipientIdPublisher) - .flatMap(id -> StoryViewState.getForRecipientId(id).toFlowable(BackpressureStrategy.LATEST)); - - return LiveDataReactiveStreams.fromPublisher(storyViewState); + Observable getStoryViewState() { + return recipientId + .subscribeOn(Schedulers.io()) + .switchMap(StoryViewState::getForRecipientId) + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()); } void onMessagesCommitted(@NonNull List conversationMessages) { @@ -249,13 +260,13 @@ public class ConversationViewModel extends ViewModel { Log.d(TAG, "[onConversationDataAvailable] recipientId: " + recipientId + ", threadId: " + threadId + ", startingPosition: " + startingPosition); this.jumpToPosition = startingPosition; - this.threadId.setValue(threadId); - this.recipientId.setValue(recipientId); + this.threadId.onNext(threadId); + this.recipientId.onNext(recipientId); } void clearThreadId() { this.jumpToPosition = -1; - this.threadId.postValue(-1L); + this.threadId.onNext(-1L); } void setSearchQuery(@Nullable String query) { @@ -270,8 +281,9 @@ public class ConversationViewModel extends ViewModel { return conversationTopMargin; } - @NonNull LiveData canShowAsBubble() { - return canShowAsBubble; + @NonNull Observable canShowAsBubble() { + return canShowAsBubble + .observeOn(AndroidSchedulers.mainThread()); } @NonNull LiveData getShowScrollToBottom() { @@ -282,16 +294,18 @@ public class ConversationViewModel extends ViewModel { return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b)); } - @NonNull LiveData getWallpaper() { - return wallpaper; + @NonNull Observable> getWallpaper() { + return wallpaper + .observeOn(AndroidSchedulers.mainThread()); } @NonNull LiveData getEvents() { return events; } - @NonNull LiveData getChatColors() { - return chatColors; + @NonNull Observable getChatColors() { + return chatColors + .observeOn(AndroidSchedulers.mainThread()); } void setHasUnreadMentions(boolean hasUnreadMentions) { @@ -310,55 +324,45 @@ public class ConversationViewModel extends ViewModel { return recentMedia; } - @NonNull LiveData getConversationMetadata() { - return conversationMetadata; + @NonNull Observable getMessageData() { + return messageData + .observeOn(AndroidSchedulers.mainThread()); } - @NonNull LiveData> getMessages() { - return messages; - } - - @NonNull PagingController getPagingController() { + @NonNull PagingController getPagingController() { return pagingController; } - @NonNull LiveData> getNameColorsMap() { - LiveData recipient = Transformations.switchMap(recipientId, r -> Recipient.live(r).getLiveData()); - LiveData> group = Transformations.map(recipient, Recipient::getGroupId); - LiveData> groupMembers = Transformations.switchMap(group, g -> { - //noinspection CodeBlock2Expr - return g.map(this::getSessionGroupRecipients) - .orElseGet(() -> new DefaultValueLiveData<>(Collections.emptySet())); - }); + @NonNull Observable> getNameColorsMap() { + return recipientId.map(Recipient::resolved) + .map(Recipient::getGroupId) + .map(groupId -> { + if (groupId.isPresent()) { + List fullMembers = SignalDatabase.groups().getGroupMembers(groupId.get(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF); + Set cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId.get(), new HashSet<>()); + cachedMembers.addAll(fullMembers); + sessionMemberCache.put(groupId.get(), cachedMembers); + return cachedMembers; + } else { + return Collections.emptySet(); + } + }) + .map(members -> { + List sorted = Stream.of(members) + .filter(member -> !Objects.equals(member, Recipient.self())) + .sortBy(Recipient::requireStringId) + .toList(); - return Transformations.map(groupMembers, members -> { - List sorted = Stream.of(members) - .filter(member -> !Objects.equals(member, Recipient.self())) - .sortBy(Recipient::requireStringId) - .toList(); + List names = ChatColorsPalette.Names.getAll(); + Map colors = new HashMap<>(); + for (int i = 0; i < sorted.size(); i++) { + colors.put(sorted.get(i).getId(), names.get(i % names.size())); + } - List names = ChatColorsPalette.Names.getAll(); - Map colors = new HashMap<>(); - for (int i = 0; i < sorted.size(); i++) { - colors.put(sorted.get(i).getId(), names.get(i % names.size())); - } - - return colors; - }); - } - - private @NonNull LiveData> getSessionGroupRecipients(@NonNull GroupId groupId) { - LiveData> fullMembers = Transformations.map(new LiveGroup(groupId).getFullMembers(), - members -> Stream.of(members) - .map(GroupMemberEntry.FullMember::getMember) - .toList()); - - return Transformations.map(fullMembers, currentMembership -> { - Set cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId, new HashSet<>()); - cachedMembers.addAll(currentMembership); - sessionMemberCache.put(groupId, cachedMembers); - return cachedMembers; - }); + return colors; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); } @NonNull LiveData> getActiveNotificationProfile() { @@ -368,14 +372,6 @@ public class ConversationViewModel extends ViewModel { return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST)); } - long getLastSeen() { - return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0; - } - - int getLastSeenPosition() { - return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeenPosition() : 0; - } - void setArgs(@NonNull ConversationIntents.Args args) { this.args = args; } @@ -403,14 +399,21 @@ public class ConversationViewModel extends ViewModel { SHOW_RECAPTCHA } - private static class ThreadAndRecipient { + static class MessageData { + private final List messages; + private final ConversationData metadata; - private final long threadId; - private final Recipient recipient; + MessageData(@NonNull ConversationData metadata, @NonNull List messages) { + this.metadata = metadata; + this.messages = messages; + } - public ThreadAndRecipient(long threadId, Recipient recipient) { - this.threadId = threadId; - this.recipient = recipient; + public @NonNull List getMessages() { + return messages; + } + + public @NonNull ConversationData getMetadata() { + return metadata; } } 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 3d2cd5117..4e2ef3669 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.logging.Log; +import org.signal.paging.LivePagedData; import org.signal.paging.PagedData; import org.signal.paging.PagingConfig; import org.signal.paging.PagingController; @@ -56,24 +57,24 @@ class ConversationListViewModel extends ViewModel { private static boolean coldStart = true; - private final MutableLiveData megaphone; - private final MutableLiveData searchResult; - private final MutableLiveData selectedConversations; - private final Set internalSelection; - private final ConversationListDataSource conversationListDataSource; - private final PagedData pagedData; - private final LiveData hasNoConversations; - private final SearchRepository searchRepository; - private final MegaphoneRepository megaphoneRepository; - private final Debouncer messageSearchDebouncer; - private final Debouncer contactSearchDebouncer; - private final ThrottledDebouncer updateDebouncer; - private final DatabaseObserver.Observer observer; - private final Invalidator invalidator; - private final CompositeDisposable disposables; - private final UnreadPaymentsLiveData unreadPaymentsLiveData; - private final UnreadPaymentsRepository unreadPaymentsRepository; - private final NotificationProfilesRepository notificationProfilesRepository; + private final MutableLiveData megaphone; + private final MutableLiveData searchResult; + private final MutableLiveData selectedConversations; + private final Set internalSelection; + private final ConversationListDataSource conversationListDataSource; + private final LivePagedData pagedData; + private final LiveData hasNoConversations; + private final SearchRepository searchRepository; + private final MegaphoneRepository megaphoneRepository; + private final Debouncer messageSearchDebouncer; + private final Debouncer contactSearchDebouncer; + private final ThrottledDebouncer updateDebouncer; + private final DatabaseObserver.Observer observer; + private final Invalidator invalidator; + private final CompositeDisposable disposables; + private final UnreadPaymentsLiveData unreadPaymentsLiveData; + private final UnreadPaymentsRepository unreadPaymentsRepository; + private final NotificationProfilesRepository notificationProfilesRepository; private String activeQuery; private SearchResult activeSearchResult; @@ -95,8 +96,8 @@ class ConversationListViewModel extends ViewModel { this.invalidator = new Invalidator(); this.disposables = new CompositeDisposable(); this.conversationListDataSource = ConversationListDataSource.create(application, isArchived); - this.pagedData = PagedData.create(conversationListDataSource, - new PagingConfig.Builder() + this.pagedData = PagedData.createForLiveData(conversationListDataSource, + new PagingConfig.Builder() .setPageSize(15) .setBufferPages(2) .build()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 15ae47cba..6cab2c800 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -1379,10 +1379,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return DeviceLastResetTime.newBuilder().build() } - fun setBadges(id: RecipientId, badges: List) { + fun setBadges(id: RecipientId, badges: List) { val badgeListBuilder = BadgeList.newBuilder() for (badge in badges) { - badgeListBuilder.addBadges(toDatabaseBadge(badge!!)) + badgeListBuilder.addBadges(toDatabaseBadge(badge)) } val values = ContentValues(1).apply { @@ -1390,7 +1390,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } if (update(id, values)) { - ApplicationDependencies.getDatabaseObserver().notifyNotificationProfileObservers() ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java index b1c9f99db..aa7830a95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider; import com.annimon.stream.Stream; +import org.signal.paging.LivePagedData; import org.signal.paging.PagedData; import org.signal.paging.PagingConfig; import org.signal.paging.PagingController; @@ -28,12 +29,12 @@ import java.util.Objects; */ public final class GiphyMp4ViewModel extends ViewModel { - private final GiphyMp4Repository repository; - private final MutableLiveData> pagedData; - private final LiveData images; - private final LiveData> pagingController; - private final SingleLiveEvent saveResultEvents; - private final boolean isForMms; + private final GiphyMp4Repository repository; + private final MutableLiveData> pagedData; + private final LiveData images; + private final LiveData> pagingController; + private final SingleLiveEvent saveResultEvents; + private final boolean isForMms; private String query; @@ -52,7 +53,7 @@ public final class GiphyMp4ViewModel extends ViewModel { .collect(MappingModelList.toMappingModelList()))); } - LiveData> getPagedData() { + LiveData> getPagedData() { return pagedData; } @@ -81,9 +82,9 @@ public final class GiphyMp4ViewModel extends ViewModel { return pagingController; } - private PagedData getGiphyImagePagedData(@Nullable String query) { - return PagedData.create(new GiphyMp4PagedDataSource(query), - new PagingConfig.Builder().setPageSize(20) + private LivePagedData getGiphyImagePagedData(@Nullable String query) { + return PagedData.createForLiveData(new GiphyMp4PagedDataSource(query), + new PagingConfig.Builder().setPageSize(20) .setBufferPages(1) .build()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java index 089cbd264..832afe166 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.signal.core.util.tracing.Tracer; +import org.signal.paging.LivePagedData; import org.signal.paging.PagedData; import org.signal.paging.PagingConfig; import org.signal.paging.PagingController; @@ -53,7 +54,7 @@ public class SubmitDebugLogViewModel extends ViewModel { .setStartIndex(0) .build(); - PagedData pagedData = PagedData.create(dataSource, config); + LivePagedData pagedData = PagedData.createForLiveData(dataSource, config); ThreadUtil.runOnMain(() -> { pagingController.set(pagedData.getController()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java index 991a7b491..9be25067f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java @@ -155,7 +155,6 @@ public class MessageRequestViewModel extends ViewModel { private void loadRecipient() { liveRecipient.observeForever(recipientObserver); SignalExecutors.BOUNDED.execute(() -> { - liveRecipient.refresh(); recipient.postValue(liveRecipient.get()); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt index 7c22b47ea..9cafbbe4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.viewer.reply.group import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.paging.LivePagedData import org.signal.paging.PagedData import org.signal.paging.PagingConfig import org.thoughtcrime.securesms.database.DatabaseObserver @@ -12,10 +13,10 @@ import org.thoughtcrime.securesms.recipients.RecipientId class StoryGroupReplyRepository { - fun getPagedReplies(parentStoryId: Long): Observable> { - return Observable.create> { emitter -> + fun getPagedReplies(parentStoryId: Long): Observable> { + return Observable.create> { emitter -> fun refresh() { - emitter.onNext(PagedData.create(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build())) + emitter.onNext(PagedData.createForLiveData(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build())) } val observer = DatabaseObserver.Observer { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt index eaf5f2f30..553f8e606 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModelProvider import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import org.signal.paging.PagedData +import org.signal.paging.LivePagedData import org.signal.paging.PagingController import org.thoughtcrime.securesms.conversation.colors.NameColors import org.thoughtcrime.securesms.groups.GroupId @@ -23,7 +23,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit val state: LiveData = store.stateLiveData - private val pagedData: MutableLiveData> = MutableLiveData() + private val pagedData: MutableLiveData> = MutableLiveData() val pagingController: LiveData> val pageData: LiveData> diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt index 2cc05edf9..2ff12ead2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocalMetrics.kt @@ -68,8 +68,8 @@ object LocalMetrics { executor.execute { val lastTime: Long? = lastSplitTimeById[id] - - if (lastTime != null) { + val splitDoesNotExist: Boolean = eventsById[id]?.splits?.none { it.name == split } ?: true + if (lastTime != null && splitDoesNotExist) { eventsById[id]?.splits?.add(LocalMetricsSplit(split, time - lastTime)) lastSplitTimeById[id] = time } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java index 657015b3f..edb40a27e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalLocalMetrics.java @@ -72,8 +72,10 @@ public final class SignalLocalMetrics { public static final class ConversationOpen { private static final String NAME = "conversation-open"; - private static final String SPLIT_DATA_LOADED = "data-loaded"; - private static final String SPLIT_RENDER = "render"; + private static final String SPLIT_VIEWMODEL_INIT = "viewmodel-init"; + private static final String SPLIT_METADATA_LOADED = "metadata-loaded"; + private static final String SPLIT_DATA_LOADED = "data-loaded"; + private static final String SPLIT_RENDER = "render"; private static String id; @@ -82,6 +84,14 @@ public final class SignalLocalMetrics { LocalMetrics.getInstance().start(id, NAME); } + public static void onMetadataLoadStarted() { + LocalMetrics.getInstance().split(id, SPLIT_VIEWMODEL_INIT); + } + + public static void onMetadataLoaded() { + LocalMetrics.getInstance().split(id, SPLIT_METADATA_LOADED); + } + public static void onDataLoaded() { LocalMetrics.getInstance().split(id, SPLIT_DATA_LOADED); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/Store.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/Store.java index 320978eb2..81f981936 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/Store.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/Store.java @@ -4,6 +4,7 @@ import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; +import androidx.lifecycle.LiveDataReactiveStreams; import androidx.lifecycle.MediatorLiveData; import com.annimon.stream.function.Function; @@ -15,6 +16,9 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executor; +import io.reactivex.rxjava3.core.BackpressureStrategy; +import io.reactivex.rxjava3.core.Observable; + /** * Manages a state to be updated from a view model and provide direct and live access. Updates * occur serially on the same executor to allow updating in a thread safe way. While not @@ -46,6 +50,11 @@ public class Store { liveStore.update(source, action); } + @MainThread + public void update(@NonNull Observable source, @NonNull Action action) { + liveStore.update(LiveDataReactiveStreams.fromPublisher(source.toFlowable(BackpressureStrategy.LATEST)), action); + } + @MainThread public void clear() { liveStore.clear(); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7229dae91..fe45e653e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2302,6 +2302,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + diff --git a/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java b/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java index 5a671d0b8..4f5d3bc4e 100644 --- a/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java +++ b/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; +import org.signal.paging.LivePagedData; import org.signal.paging.PagingController; import org.signal.paging.PagingConfig; import org.signal.paging.PagedData; @@ -12,14 +13,14 @@ import java.util.List; public class MainViewModel extends ViewModel { - private final PagedData pagedData; - private final MainDataSource dataSource; + private final LivePagedData pagedData; + private final MainDataSource dataSource; public MainViewModel() { this.dataSource = new MainDataSource(1000); - this.pagedData = PagedData.create(dataSource, new PagingConfig.Builder().setBufferPages(3) - .setPageSize(25) - .build()); + this.pagedData = PagedData.createForLiveData(dataSource, new PagingConfig.Builder().setBufferPages(3) + .setPageSize(25) + .build()); } public void onItemClicked(@NonNull String key) { diff --git a/paging/lib/build.gradle b/paging/lib/build.gradle index c87025511..e21f35bbd 100644 --- a/paging/lib/build.gradle +++ b/paging/lib/build.gradle @@ -18,6 +18,8 @@ android { dependencies { implementation libs.androidx.appcompat implementation libs.material.material + implementation libs.rxjava3.rxandroid + implementation libs.rxjava3.rxjava implementation project(':core-util') testImplementation testLibs.junit.junit } \ No newline at end of file diff --git a/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java b/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java index 609b6d3ed..25584d553 100644 --- a/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java @@ -1,9 +1,7 @@ package org.signal.paging; import androidx.annotation.NonNull; -import androidx.lifecycle.MutableLiveData; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -24,16 +22,19 @@ class BufferedPagingController implements PagingController { private final PagedDataSource dataSource; private final PagingConfig config; - private final MutableLiveData> liveData; + private final DataStream dataStream; private final Executor serializationExecutor; private PagingController activeController; private int lastRequestedIndex; - BufferedPagingController(PagedDataSource dataSource, PagingConfig config, @NonNull MutableLiveData> liveData) { + BufferedPagingController(@NonNull PagedDataSource dataSource, + @NonNull PagingConfig config, + @NonNull DataStream dataStream) + { this.dataSource = dataSource; this.config = config; - this.liveData = liveData; + this.dataStream = dataStream; this.serializationExecutor = Executors.newSingleThreadExecutor(); this.activeController = null; @@ -57,7 +58,7 @@ class BufferedPagingController implements PagingController { activeController.onDataInvalidated(); } - activeController = new FixedSizePagingController<>(dataSource, config, liveData, dataSource.size()); + activeController = new FixedSizePagingController<>(dataSource, config, dataStream, dataSource.size()); activeController.onDataNeededAroundIndex(lastRequestedIndex); }); } diff --git a/paging/lib/src/main/java/org/signal/paging/DataStream.java b/paging/lib/src/main/java/org/signal/paging/DataStream.java new file mode 100644 index 000000000..d617ee4cf --- /dev/null +++ b/paging/lib/src/main/java/org/signal/paging/DataStream.java @@ -0,0 +1,10 @@ +package org.signal.paging; + +import java.util.List; + +/** + * An abstraction over different types of ways the paging lib can provide data, e.g. Observables vs LiveData. + */ +interface DataStream { + void next(List data); +} diff --git a/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java b/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java index 61ba65f79..fc36fe813 100644 --- a/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java @@ -1,7 +1,6 @@ package org.signal.paging; import androidx.annotation.NonNull; -import androidx.lifecycle.MutableLiveData; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; @@ -29,7 +28,7 @@ class FixedSizePagingController implements PagingController { private final PagedDataSource dataSource; private final PagingConfig config; - private final MutableLiveData> liveData; + private final DataStream dataStream; private final DataStatus loadState; private final Map keyToPosition; @@ -39,15 +38,17 @@ class FixedSizePagingController implements PagingController { FixedSizePagingController(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config, - @NonNull MutableLiveData> liveData, + @NonNull DataStream dataStream, int size) { this.dataSource = dataSource; this.config = config; - this.liveData = liveData; + this.dataStream = dataStream; this.loadState = DataStatus.obtain(size); this.data = new CompressedList<>(loadState.size()); this.keyToPosition = new HashMap<>(); + + if (DEBUG) Log.d(TAG, "[Constructor] Creating with size " + size + " (loadState.size() = " + loadState.size() + ")"); } /** @@ -58,7 +59,7 @@ class FixedSizePagingController implements PagingController { @Override public void onDataNeededAroundIndex(int aroundIndex) { if (invalidated) { - Log.w(TAG, buildLog(aroundIndex, "Invalidated! At very beginning.")); + Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! At very beginning.")); return; } @@ -67,7 +68,7 @@ class FixedSizePagingController implements PagingController { synchronized (loadState) { if (loadState.size() == 0) { - liveData.postValue(Collections.emptyList()); + dataStream.next(Collections.emptyList()); return; } @@ -81,14 +82,14 @@ class FixedSizePagingController implements PagingController { loadStart = loadState.getEarliestUnmarkedIndexInRange(leftLoadBoundary, rightLoadBoundary); if (loadStart < 0) { - if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "loadStart < 0")); + if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "loadStart < 0")); return; } loadEnd = loadState.getLatestUnmarkedIndexInRange(Math.max(leftLoadBoundary, loadStart), rightLoadBoundary) + 1; if (loadEnd <= loadStart) { - if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "loadEnd <= loadStart, loadEnd: " + loadEnd + ", loadStart: " + loadStart)); + if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "loadEnd <= loadStart, loadEnd: " + loadEnd + ", loadStart: " + loadStart)); return; } @@ -96,19 +97,19 @@ class FixedSizePagingController implements PagingController { loadState.markRange(loadStart, loadEnd); - if (DEBUG) Log.i(TAG, buildLog(aroundIndex, "start: " + loadStart + ", end: " + loadEnd + ", totalSize: " + totalSize)); + if (DEBUG) Log.i(TAG, buildDataNeededLog(aroundIndex, "start: " + loadStart + ", end: " + loadEnd + ", totalSize: " + totalSize)); } FETCH_EXECUTOR.execute(() -> { if (invalidated) { - Log.w(TAG, buildLog(aroundIndex, "Invalidated! At beginning of load task.")); + Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! At beginning of load task.")); return; } List loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated); if (invalidated) { - Log.w(TAG, buildLog(aroundIndex, "Invalidated! Just after data was loaded.")); + Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! Just after data was loaded.")); return; } @@ -123,7 +124,7 @@ class FixedSizePagingController implements PagingController { } data = updated; - liveData.postValue(updated); + dataStream.next(updated); }); } @@ -139,6 +140,8 @@ class FixedSizePagingController implements PagingController { @Override public void onDataItemChanged(Key key) { + if (DEBUG) Log.d(TAG, buildItemChangedLog(key, "")); + FETCH_EXECUTOR.execute(() -> { Integer position = keyToPosition.get(key); @@ -172,12 +175,16 @@ class FixedSizePagingController implements PagingController { updatedList.set(position, item); data = updatedList; - liveData.postValue(updatedList); + dataStream.next(updatedList); + + if (DEBUG) Log.d(TAG, buildItemChangedLog(key, "Published updated data")); }); } @Override public void onDataItemInserted(Key key, int position) { + if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, "")); + FETCH_EXECUTOR.execute(() -> { if (keyToPosition.containsKey(key)) { Log.w(TAG, "Notified of key " + key + " being inserted at " + position + ", but the item already exists!"); @@ -191,6 +198,7 @@ class FixedSizePagingController implements PagingController { synchronized (loadState) { loadState.insertState(position, true); + if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, "Size of loadState updated to " + loadState.size())); } Data item = dataSource.load(key); @@ -211,7 +219,9 @@ class FixedSizePagingController implements PagingController { rebuildKeyToPositionMap(keyToPosition, updatedList, dataSource); data = updatedList; - liveData.postValue(updatedList); + dataStream.next(updatedList); + + if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, "Published updated data")); }); } @@ -226,7 +236,15 @@ class FixedSizePagingController implements PagingController { } } - private static String buildLog(int aroundIndex, String message) { - return "onDataNeededAroundIndex(" + aroundIndex + ") " + message; + private String buildDataNeededLog(int aroundIndex, String message) { + return "[onDataNeededAroundIndex(" + aroundIndex + "), size: " + loadState.size() + "] " + message; + } + + private String buildItemInsertedLog(Key key, int position, String message) { + return "[onDataItemInserted(" + key + ", " + position + "), size: " + loadState.size() + "] " + message; + } + + private String buildItemChangedLog(Key key, String message) { + return "[onDataItemInserted(" + key + "), size: " + loadState.size() + "] " + message; } } diff --git a/paging/lib/src/main/java/org/signal/paging/LivePagedData.java b/paging/lib/src/main/java/org/signal/paging/LivePagedData.java new file mode 100644 index 000000000..4f89a579e --- /dev/null +++ b/paging/lib/src/main/java/org/signal/paging/LivePagedData.java @@ -0,0 +1,25 @@ +package org.signal.paging; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import java.util.List; + +/** + * An implementation of {@link PagedData} that will provide data as a {@link LiveData}. + */ +public class LivePagedData extends PagedData { + + private final LiveData> data; + + LivePagedData(@NonNull LiveData> data, @NonNull PagingController controller) { + super(controller); + this.data = data; + } + + @AnyThread + public @NonNull LiveData> getData() { + return data; + } +} diff --git a/paging/lib/src/main/java/org/signal/paging/ObservablePagedData.java b/paging/lib/src/main/java/org/signal/paging/ObservablePagedData.java new file mode 100644 index 000000000..16ad1f8fc --- /dev/null +++ b/paging/lib/src/main/java/org/signal/paging/ObservablePagedData.java @@ -0,0 +1,27 @@ +package org.signal.paging; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; + +import java.util.List; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.Subject; + +/** + * An implementation of {@link PagedData} that will provide data as an {@link Observable}. + */ +public class ObservablePagedData extends PagedData { + + private final Observable> data; + + ObservablePagedData(@NonNull Observable> data, @NonNull PagingController controller) { + super(controller); + this.data = data; + } + + @AnyThread + public @NonNull Observable> getData() { + return data; + } +} diff --git a/paging/lib/src/main/java/org/signal/paging/PagedData.java b/paging/lib/src/main/java/org/signal/paging/PagedData.java index 970be9fd3..89fa2e02d 100644 --- a/paging/lib/src/main/java/org/signal/paging/PagedData.java +++ b/paging/lib/src/main/java/org/signal/paging/PagedData.java @@ -2,39 +2,41 @@ package org.signal.paging; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import java.util.List; +import io.reactivex.rxjava3.subjects.BehaviorSubject; +import io.reactivex.rxjava3.subjects.Subject; + /** * The primary entry point for creating paged data. */ -public final class PagedData { +public class PagedData { - private final LiveData> data; private final PagingController controller; - @AnyThread - public static PagedData create(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config) { - MutableLiveData> liveData = new MutableLiveData<>(); - PagingController controller = new BufferedPagingController<>(dataSource, config, liveData); - - return new PagedData<>(liveData, controller); - } - - private PagedData(@NonNull LiveData> data, @NonNull PagingController controller) { - this.data = data; + protected PagedData(PagingController controller) { this.controller = controller; } @AnyThread - public @NonNull LiveData> getData() { - return data; + public static LivePagedData createForLiveData(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config) { + MutableLiveData> liveData = new MutableLiveData<>(); + PagingController controller = new BufferedPagingController<>(dataSource, config, liveData::postValue); + + return new LivePagedData<>(liveData, controller); } @AnyThread - public @NonNull PagingController getController() { + public static ObservablePagedData createForObservable(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config) { + Subject> subject = BehaviorSubject.create(); + PagingController controller = new BufferedPagingController<>(dataSource, config, subject::onNext); + + return new ObservablePagedData<>(subject, controller); + } + + public PagingController getController() { return controller; } }