Improve conversation open speed.

Co-authored-by: Cody Henthorne <cody@signal.org>
fork-5.53.8
Greyson Parrelli 2022-03-16 10:10:01 -04:00 zatwierdzone przez Cody Henthorne
rodzic d3049a3433
commit 666218773c
27 zmienionych plików z 462 dodań i 395 usunięć

Wyświetl plik

@ -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<PagedData<ContactSearchKey, ContactSearchData>>()
private val pagedData = MutableLiveData<LivePagedData<ContactSearchKey, ContactSearchData>>()
private val configurationStore = Store(ContactSearchState())
private val selectionStore = Store<Set<ContactSearchKey>>(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?) {

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -47,25 +47,41 @@ class ConversationDataSource implements PagedDataSource<MessageId, ConversationM
private final MessageRequestData messageRequestData;
private final boolean showUniversalExpireTimerUpdate;
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate) {
/** Used once for the initial fetch, then cleared. */
private int baseSize;
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate, int baseSize) {
this.context = context;
this.threadId = threadId;
this.messageRequestData = messageRequestData;
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
this.baseSize = baseSize;
}
@Override
public int size() {
long startTime = System.currentTimeMillis();
int size = SignalDatabase.mmsSms().getConversationCount(threadId) +
int size = getSizeInternal() +
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
(showUniversalExpireTimerUpdate ? 1 : 0);
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
Log.d(TAG, "[size(), thread " + threadId + "] " + (System.currentTimeMillis() - startTime) + " ms");
return size;
}
private int getSizeInternal() {
synchronized (this) {
if (baseSize != -1) {
int size = baseSize;
baseSize = -1;
return size;
}
}
return SignalDatabase.mmsSms().getConversationCount(threadId);
}
@Override
public @NonNull List<ConversationMessage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);

Wyświetl plik

@ -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<ConversationMessage> 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;

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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<ConversationData> getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) {
MutableLiveData<ConversationData> 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);
}
}

Wyświetl plik

@ -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<List<Media>> recentMedia;
private final MutableLiveData<Long> threadId;
private final LiveData<List<ConversationMessage>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final LiveData<Boolean> canShowAsBubble;
private final ProxyPagingController<MessageId> pagingController;
private final DatabaseObserver.Observer conversationObserver;
private final DatabaseObserver.MessageObserver messageUpdateObserver;
private final DatabaseObserver.MessageObserver messageInsertObserver;
private final MutableLiveData<RecipientId> recipientId;
private final LiveData<ChatWallpaper> wallpaper;
private final SingleLiveEvent<Event> events;
private final LiveData<ChatColors> chatColors;
private final MutableLiveData<Integer> toolbarBottom;
private final MutableLiveData<Integer> inlinePlayerHeight;
private final LiveData<Integer> conversationTopMargin;
private final Store<ThreadAnimationState> threadAnimationStateStore;
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final NotificationProfilesRepository notificationProfilesRepository;
private final MutableLiveData<String> searchQuery;
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final BehaviorSubject<Long> threadId;
private final Observable<MessageData> messageData;
private final MutableLiveData<Boolean> showScrollButtons;
private final MutableLiveData<Boolean> hasUnreadMentions;
private final Observable<Boolean> canShowAsBubble;
private final ProxyPagingController<MessageId> pagingController;
private final DatabaseObserver.Observer conversationObserver;
private final DatabaseObserver.MessageObserver messageUpdateObserver;
private final DatabaseObserver.MessageObserver messageInsertObserver;
private final BehaviorSubject<RecipientId> recipientId;
private final Observable<Optional<ChatWallpaper>> wallpaper;
private final SingleLiveEvent<Event> events;
private final Observable<ChatColors> chatColors;
private final MutableLiveData<Integer> toolbarBottom;
private final MutableLiveData<Integer> inlinePlayerHeight;
private final LiveData<Integer> conversationTopMargin;
private final Store<ThreadAnimationState> threadAnimationStateStore;
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final NotificationProfilesRepository notificationProfilesRepository;
private final MutableLiveData<String> searchQuery;
private final Map<GroupId, Set<Recipient>> 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<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
LiveData<ConversationData> metadata = Transformations.switchMap(threadAndRecipient, d -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition);
recipientId
.observeOn(Schedulers.io())
.distinctUntilChanged()
.map(Recipient::resolved)
.subscribe(recipientCache);
jumpToPosition = -1;
BehaviorSubject<ConversationData> 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<Pair<Long, PagedData<MessageId, ConversationMessage>>> 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<MessageId, ConversationMessage> 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<StoryViewState> getStoryViewState(@NonNull LifecycleOwner lifecycle) {
Publisher<RecipientId> recipientIdPublisher = LiveDataReactiveStreams.toPublisher(lifecycle, recipientId);
Flowable<StoryViewState> storyViewState = Flowable.fromPublisher(recipientIdPublisher)
.flatMap(id -> StoryViewState.getForRecipientId(id).toFlowable(BackpressureStrategy.LATEST));
return LiveDataReactiveStreams.fromPublisher(storyViewState);
Observable<StoryViewState> getStoryViewState() {
return recipientId
.subscribeOn(Schedulers.io())
.switchMap(StoryViewState::getForRecipientId)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread());
}
void onMessagesCommitted(@NonNull List<ConversationMessage> 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<Boolean> canShowAsBubble() {
return canShowAsBubble;
@NonNull Observable<Boolean> canShowAsBubble() {
return canShowAsBubble
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Boolean> getShowScrollToBottom() {
@ -282,16 +294,18 @@ public class ConversationViewModel extends ViewModel {
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
}
@NonNull LiveData<ChatWallpaper> getWallpaper() {
return wallpaper;
@NonNull Observable<Optional<ChatWallpaper>> getWallpaper() {
return wallpaper
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<Event> getEvents() {
return events;
}
@NonNull LiveData<ChatColors> getChatColors() {
return chatColors;
@NonNull Observable<ChatColors> getChatColors() {
return chatColors
.observeOn(AndroidSchedulers.mainThread());
}
void setHasUnreadMentions(boolean hasUnreadMentions) {
@ -310,55 +324,45 @@ public class ConversationViewModel extends ViewModel {
return recentMedia;
}
@NonNull LiveData<ConversationData> getConversationMetadata() {
return conversationMetadata;
@NonNull Observable<MessageData> getMessageData() {
return messageData
.observeOn(AndroidSchedulers.mainThread());
}
@NonNull LiveData<List<ConversationMessage>> getMessages() {
return messages;
}
@NonNull PagingController getPagingController() {
@NonNull PagingController<MessageId> getPagingController() {
return pagingController;
}
@NonNull LiveData<Map<RecipientId, NameColor>> getNameColorsMap() {
LiveData<Recipient> recipient = Transformations.switchMap(recipientId, r -> Recipient.live(r).getLiveData());
LiveData<Optional<GroupId>> group = Transformations.map(recipient, Recipient::getGroupId);
LiveData<Set<Recipient>> groupMembers = Transformations.switchMap(group, g -> {
//noinspection CodeBlock2Expr
return g.map(this::getSessionGroupRecipients)
.orElseGet(() -> new DefaultValueLiveData<>(Collections.emptySet()));
});
@NonNull Observable<Map<RecipientId, NameColor>> getNameColorsMap() {
return recipientId.map(Recipient::resolved)
.map(Recipient::getGroupId)
.map(groupId -> {
if (groupId.isPresent()) {
List<Recipient> fullMembers = SignalDatabase.groups().getGroupMembers(groupId.get(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF);
Set<Recipient> cachedMembers = MapUtil.getOrDefault(sessionMemberCache, groupId.get(), new HashSet<>());
cachedMembers.addAll(fullMembers);
sessionMemberCache.put(groupId.get(), cachedMembers);
return cachedMembers;
} else {
return Collections.<Recipient>emptySet();
}
})
.map(members -> {
List<Recipient> sorted = Stream.of(members)
.filter(member -> !Objects.equals(member, Recipient.self()))
.sortBy(Recipient::requireStringId)
.toList();
return Transformations.map(groupMembers, members -> {
List<Recipient> sorted = Stream.of(members)
.filter(member -> !Objects.equals(member, Recipient.self()))
.sortBy(Recipient::requireStringId)
.toList();
List<NameColor> names = ChatColorsPalette.Names.getAll();
Map<RecipientId, NameColor> colors = new HashMap<>();
for (int i = 0; i < sorted.size(); i++) {
colors.put(sorted.get(i).getId(), names.get(i % names.size()));
}
List<NameColor> names = ChatColorsPalette.Names.getAll();
Map<RecipientId, NameColor> 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<Set<Recipient>> getSessionGroupRecipients(@NonNull GroupId groupId) {
LiveData<List<Recipient>> fullMembers = Transformations.map(new LiveGroup(groupId).getFullMembers(),
members -> Stream.of(members)
.map(GroupMemberEntry.FullMember::getMember)
.toList());
return Transformations.map(fullMembers, currentMembership -> {
Set<Recipient> 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<Optional<NotificationProfile>> 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<ConversationMessage> messages;
private final ConversationData metadata;
private final long threadId;
private final Recipient recipient;
MessageData(@NonNull ConversationData metadata, @NonNull List<ConversationMessage> messages) {
this.metadata = metadata;
this.messages = messages;
}
public ThreadAndRecipient(long threadId, Recipient recipient) {
this.threadId = threadId;
this.recipient = recipient;
public @NonNull List<ConversationMessage> getMessages() {
return messages;
}
public @NonNull ConversationData getMetadata() {
return metadata;
}
}

Wyświetl plik

@ -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> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final MutableLiveData<ConversationSet> selectedConversations;
private final Set<Conversation> internalSelection;
private final ConversationListDataSource conversationListDataSource;
private final PagedData<Long, Conversation> pagedData;
private final LiveData<Boolean> 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> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final MutableLiveData<ConversationSet> selectedConversations;
private final Set<Conversation> internalSelection;
private final ConversationListDataSource conversationListDataSource;
private final LivePagedData<Long, Conversation> pagedData;
private final LiveData<Boolean> 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());

Wyświetl plik

@ -1379,10 +1379,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return DeviceLastResetTime.newBuilder().build()
}
fun setBadges(id: RecipientId, badges: List<Badge?>) {
fun setBadges(id: RecipientId, badges: List<Badge>) {
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)
}
}

Wyświetl plik

@ -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<String, GiphyImage>> pagedData;
private final LiveData<MappingModelList> images;
private final LiveData<PagingController<String>> pagingController;
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
private final boolean isForMms;
private final GiphyMp4Repository repository;
private final MutableLiveData<LivePagedData<String, GiphyImage>> pagedData;
private final LiveData<MappingModelList> images;
private final LiveData<PagingController<String>> pagingController;
private final SingleLiveEvent<GiphyMp4SaveResult> saveResultEvents;
private final boolean isForMms;
private String query;
@ -52,7 +53,7 @@ public final class GiphyMp4ViewModel extends ViewModel {
.collect(MappingModelList.toMappingModelList())));
}
LiveData<PagedData<String, GiphyImage>> getPagedData() {
LiveData<LivePagedData<String, GiphyImage>> getPagedData() {
return pagedData;
}
@ -81,9 +82,9 @@ public final class GiphyMp4ViewModel extends ViewModel {
return pagingController;
}
private PagedData<String, GiphyImage> getGiphyImagePagedData(@Nullable String query) {
return PagedData.create(new GiphyMp4PagedDataSource(query),
new PagingConfig.Builder().setPageSize(20)
private LivePagedData<String, GiphyImage> getGiphyImagePagedData(@Nullable String query) {
return PagedData.createForLiveData(new GiphyMp4PagedDataSource(query),
new PagingConfig.Builder().setPageSize(20)
.setBufferPages(1)
.build());
}

Wyświetl plik

@ -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<Long, LogLine> pagedData = PagedData.create(dataSource, config);
LivePagedData<Long, LogLine> pagedData = PagedData.createForLiveData(dataSource, config);
ThreadUtil.runOnMain(() -> {
pagingController.set(pagedData.getController());

Wyświetl plik

@ -155,7 +155,6 @@ public class MessageRequestViewModel extends ViewModel {
private void loadRecipient() {
liveRecipient.observeForever(recipientObserver);
SignalExecutors.BOUNDED.execute(() -> {
liveRecipient.refresh();
recipient.postValue(liveRecipient.get());
});
}

Wyświetl plik

@ -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<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> {
return Observable.create<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> { emitter ->
fun getPagedReplies(parentStoryId: Long): Observable<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> {
return Observable.create<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> { 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 {

Wyświetl plik

@ -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<StoryGroupReplyState> = store.stateLiveData
private val pagedData: MutableLiveData<PagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> = MutableLiveData()
private val pagedData: MutableLiveData<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> = MutableLiveData()
val pagingController: LiveData<PagingController<StoryGroupReplyItemData.Key>>
val pageData: LiveData<List<StoryGroupReplyItemData>>

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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<State> {
liveStore.update(source, action);
}
@MainThread
public <Input> void update(@NonNull Observable<Input> source, @NonNull Action<Input, State> action) {
liveStore.update(LiveDataReactiveStreams.fromPublisher(source.toFlowable(BackpressureStrategy.LATEST)), action);
}
@MainThread
public void clear() {
liveStore.clear();

Wyświetl plik

@ -2302,6 +2302,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5b3582a1e9fd9e9037ee933ab9486ff323d6244b5f1b6ff6ebadc2bfa957ce5b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.reactivex.rxjava3" name="rxjava" version="3.0.0">
<artifact name="rxjava-3.0.0.jar">
<sha256 value="dd1eebeb292ddc40429e5b3e72f6a61facbec21ad5493f9f54ffb2bdf254bf57" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.reactivex.rxjava3" name="rxjava" version="3.0.13">
<artifact name="rxjava-3.0.13.jar">
<sha256 value="598abaf71dbc970dd0727e6d5f4f786dc999df5b972cbf261316a32e155b2c69" origin="Generated by Gradle"/>

Wyświetl plik

@ -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<String, Item> pagedData;
private final MainDataSource dataSource;
private final LivePagedData<String, Item> 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) {

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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<Key, Data> implements PagingController<Key> {
private final PagedDataSource<Key, Data> dataSource;
private final PagingConfig config;
private final MutableLiveData<List<Data>> liveData;
private final DataStream<Data> dataStream;
private final Executor serializationExecutor;
private PagingController<Key> activeController;
private int lastRequestedIndex;
BufferedPagingController(PagedDataSource<Key, Data> dataSource, PagingConfig config, @NonNull MutableLiveData<List<Data>> liveData) {
BufferedPagingController(@NonNull PagedDataSource<Key, Data> dataSource,
@NonNull PagingConfig config,
@NonNull DataStream<Data> 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<Key, Data> implements PagingController<Key> {
activeController.onDataInvalidated();
}
activeController = new FixedSizePagingController<>(dataSource, config, liveData, dataSource.size());
activeController = new FixedSizePagingController<>(dataSource, config, dataStream, dataSource.size());
activeController.onDataNeededAroundIndex(lastRequestedIndex);
});
}

Wyświetl plik

@ -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<Data> {
void next(List<Data> data);
}

Wyświetl plik

@ -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<Key, Data> implements PagingController<Key> {
private final PagedDataSource<Key, Data> dataSource;
private final PagingConfig config;
private final MutableLiveData<List<Data>> liveData;
private final DataStream<Data> dataStream;
private final DataStatus loadState;
private final Map<Key, Integer> keyToPosition;
@ -39,15 +38,17 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
FixedSizePagingController(@NonNull PagedDataSource<Key, Data> dataSource,
@NonNull PagingConfig config,
@NonNull MutableLiveData<List<Data>> liveData,
@NonNull DataStream<Data> 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<Key, Data> implements PagingController<Key> {
@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<Key, Data> implements PagingController<Key> {
synchronized (loadState) {
if (loadState.size() == 0) {
liveData.postValue(Collections.emptyList());
dataStream.next(Collections.emptyList());
return;
}
@ -81,14 +82,14 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
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<Key, Data> implements PagingController<Key> {
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<Data> 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<Key, Data> implements PagingController<Key> {
}
data = updated;
liveData.postValue(updated);
dataStream.next(updated);
});
}
@ -139,6 +140,8 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
@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<Key, Data> implements PagingController<Key> {
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<Key, Data> implements PagingController<Key> {
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<Key, Data> implements PagingController<Key> {
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<Key, Data> implements PagingController<Key> {
}
}
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;
}
}

Wyświetl plik

@ -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<Key, Data> extends PagedData<Key> {
private final LiveData<List<Data>> data;
LivePagedData(@NonNull LiveData<List<Data>> data, @NonNull PagingController<Key> controller) {
super(controller);
this.data = data;
}
@AnyThread
public @NonNull LiveData<List<Data>> getData() {
return data;
}
}

Wyświetl plik

@ -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<Key, Data> extends PagedData<Key> {
private final Observable<List<Data>> data;
ObservablePagedData(@NonNull Observable<List<Data>> data, @NonNull PagingController<Key> controller) {
super(controller);
this.data = data;
}
@AnyThread
public @NonNull Observable<List<Data>> getData() {
return data;
}
}

Wyświetl plik

@ -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<Key, Data> {
public class PagedData<Key> {
private final LiveData<List<Data>> data;
private final PagingController<Key> controller;
@AnyThread
public static <Key, Data> PagedData<Key, Data> create(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
MutableLiveData<List<Data>> liveData = new MutableLiveData<>();
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, liveData);
return new PagedData<>(liveData, controller);
}
private PagedData(@NonNull LiveData<List<Data>> data, @NonNull PagingController<Key> controller) {
this.data = data;
protected PagedData(PagingController<Key> controller) {
this.controller = controller;
}
@AnyThread
public @NonNull LiveData<List<Data>> getData() {
return data;
public static <Key, Data> LivePagedData<Key, Data> createForLiveData(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
MutableLiveData<List<Data>> liveData = new MutableLiveData<>();
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, liveData::postValue);
return new LivePagedData<>(liveData, controller);
}
@AnyThread
public @NonNull PagingController<Key> getController() {
public static <Key, Data> ObservablePagedData<Key, Data> createForObservable(@NonNull PagedDataSource<Key, Data> dataSource, @NonNull PagingConfig config) {
Subject<List<Data>> subject = BehaviorSubject.create();
PagingController<Key> controller = new BufferedPagingController<>(dataSource, config, subject::onNext);
return new ObservablePagedData<>(subject, controller);
}
public PagingController<Key> getController() {
return controller;
}
}