Display a loading message if group update message is taking a while to load.

master
Alan Evans 2020-09-23 14:47:00 -03:00 committed by Greyson Parrelli
parent 5ae96905bb
commit 34ef8b52f6
10 changed files with 122 additions and 64 deletions

View File

@ -4,6 +4,7 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.conversation.ConversationMessage;
@ -22,7 +23,8 @@ import java.util.Locale;
import java.util.Set; import java.util.Set;
public interface BindableConversationItem extends Unbindable { public interface BindableConversationItem extends Unbindable {
void bind(@NonNull ConversationMessage messageRecord, void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,

View File

@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList; import androidx.paging.PagedList;
import androidx.paging.PagedListAdapter; import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
@ -84,6 +85,7 @@ public class ConversationAdapter
private static final long FOOTER_ID = Long.MIN_VALUE + 1; private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private final ItemClickListener clickListener; private final ItemClickListener clickListener;
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final Locale locale; private final Locale locale;
private final Recipient recipient; private final Recipient recipient;
@ -99,12 +101,14 @@ public class ConversationAdapter
private View headerView; private View headerView;
private View footerView; private View footerView;
ConversationAdapter(@NonNull GlideRequests glideRequests, ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale, @NonNull Locale locale,
@Nullable ItemClickListener clickListener, @Nullable ItemClickListener clickListener,
@NonNull Recipient recipient) @NonNull Recipient recipient)
{ {
super(new DiffCallback()); super(new DiffCallback());
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.locale = locale; this.locale = locale;
@ -216,7 +220,8 @@ public class ConversationAdapter
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
conversationViewHolder.getBindable().bind(conversationMessage, conversationViewHolder.getBindable().bind(lifecycleOwner,
conversationMessage,
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null), Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null), Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
glideRequests, glideRequests,

View File

@ -479,7 +479,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeListAdapter() { private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) { if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId()); Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get()); ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
list.setAdapter(adapter); list.setAdapter(adapter);
setStickyHeaderDecoration(adapter); setStickyHeaderDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool()); ConversationAdapter.initializePool(list.getRecycledViewPool());

View File

@ -54,6 +54,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -79,8 +80,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -116,9 +115,9 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil; import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} }
@Override @Override
public void bind(@NonNull ConversationMessage conversationMessage, public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,

View File

@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale; import java.util.Locale;
@ -44,9 +44,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout public final class ConversationUpdateItem extends LinearLayout
implements RecipientForeverObserver, implements BindableConversationItem
BindableConversationItem,
Observer<SpannableString>
{ {
private static final String TAG = ConversationUpdateItem.class.getSimpleName(); private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout
private Locale locale; private Locale locale;
private LiveData<SpannableString> displayBody; private LiveData<SpannableString> displayBody;
private final Debouncer bodyClearDebouncer = new Debouncer(150); private final UpdateObserver updateObserver = new UpdateObserver();
private final SenderObserver senderObserver = new SenderObserver();
public ConversationUpdateItem(Context context) { public ConversationUpdateItem(Context context) {
super(context); super(context);
@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout
} }
@Override @Override
public void bind(@NonNull ConversationMessage conversationMessage, public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout
{ {
this.batchSelected = batchSelected; this.batchSelected = batchSelected;
bind(conversationMessage, locale); bind(lifecycleOwner, conversationMessage, locale);
}
@Override
protected void onDetachedFromWindow() {
unbind();
super.onDetachedFromWindow();
} }
@Override @Override
@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout
return conversationMessage; return conversationMessage;
} }
private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) { private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
if (this.sender != null) {
this.sender.removeForeverObserver(this);
}
observeDisplayBody(null);
setBodyText(null);
this.conversationMessage = conversationMessage; this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord(); this.messageRecord = conversationMessage.getMessageRecord();
this.sender = messageRecord.getIndividualRecipient().live();
this.locale = locale; this.locale = locale;
this.sender.observeForever(this); observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext())); UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription); LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new); LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
present(conversationMessage); present(conversationMessage);
observeDisplayBody(spannableStringMessage); observeDisplayBody(lifecycleOwner, spannableStringMessage);
} }
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) { /** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private @NonNull LiveData<String> loading(@NonNull LiveData<String> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
}
private static LiveData<SpannableString> toSpannable(LiveData<String> loading) {
return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
}
@Override
public void unbind() {
}
private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
if (sender != null) {
sender.getLiveData().removeObserver(senderObserver);
}
if (recipient != null) {
sender = recipient.live();
sender.getLiveData().observe(lifecycleOwner, senderObserver);
} else {
sender = null;
}
}
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != displayBody) { if (this.displayBody != displayBody) {
if (this.displayBody != null) { if (this.displayBody != null) {
this.displayBody.removeObserver(this); this.displayBody.removeObserver(updateObserver);
} }
this.displayBody = displayBody; this.displayBody = displayBody;
if (this.displayBody != null) { if (this.displayBody != null) {
this.displayBody.observeForever(this); this.displayBody.observe(lifecycleOwner, updateObserver);
} }
} }
} }
private void setBodyText(@Nullable CharSequence text) { private void setBodyText(@Nullable CharSequence text) {
if (text == null) { if (text == null) {
bodyClearDebouncer.publish(() -> body.setText(null)); body.setVisibility(INVISIBLE);
} else { } else {
bodyClearDebouncer.clear();
body.setText(text); body.setText(text);
body.setVisibility(VISIBLE); body.setVisibility(VISIBLE);
} }
@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout
icon.setColorFilter(getIconTintFilter()); icon.setColorFilter(getIconTintFilter());
} }
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
present(conversationMessage);
}
@Override @Override
public void setOnClickListener(View.OnClickListener l) { public void setOnClickListener(View.OnClickListener l) {
super.setOnClickListener(new InternalClickListener(l)); super.setOnClickListener(new InternalClickListener(l));
} }
@Override private final class SenderObserver implements Observer<Recipient> {
public void unbind() {
if (sender != null) {
sender.removeForeverObserver(this);
}
observeDisplayBody(null); @Override
public void onChanged(Recipient recipient) {
present(conversationMessage);
}
} }
@Override private final class UpdateObserver implements Observer<SpannableString> {
public void onChanged(SpannableString update) {
setBodyText(update); @Override
public void onChanged(SpannableString update) {
setBodyText(update);
}
} }
private class InternalClickListener implements View.OnClickListener { private class InternalClickListener implements View.OnClickListener {

View File

@ -91,7 +91,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
private void initializeList() { private void initializeList() {
RecyclerView list = findViewById(R.id.message_details_list); RecyclerView list = findViewById(R.id.message_details_list);
adapter = new MessageDetailsAdapter(glideRequests); adapter = new MessageDetailsAdapter(this, glideRequests);
list.setAdapter(adapter); list.setAdapter(adapter);
list.setItemAnimator(null); list.setItemAnimator(null);

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -20,13 +21,15 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
private static final Object EXPIRATION_TIMER_CHANGE_PAYLOAD = new Object(); private static final Object EXPIRATION_TIMER_CHANGE_PAYLOAD = new Object();
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private boolean running; private boolean running;
MessageDetailsAdapter(GlideRequests glideRequests) { MessageDetailsAdapter(@NonNull LifecycleOwner lifecycleOwner, @NonNull GlideRequests glideRequests) {
super(new MessageDetailsDiffer()); super(new MessageDetailsDiffer());
this.glideRequests = glideRequests; this.lifecycleOwner = lifecycleOwner;
running = true; this.glideRequests = glideRequests;
this.running = true;
} }
@Override @Override
@ -46,7 +49,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof MessageHeaderViewHolder) { if (holder instanceof MessageHeaderViewHolder) {
((MessageHeaderViewHolder) holder).bind((ConversationMessage) getItem(position).data, running); ((MessageHeaderViewHolder) holder).bind(lifecycleOwner, (ConversationMessage) getItem(position).data, running);
} else if (holder instanceof RecipientHeaderViewHolder) { } else if (holder instanceof RecipientHeaderViewHolder) {
((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data); ((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data);
} else if (holder instanceof RecipientViewHolder) { } else if (holder instanceof RecipientViewHolder) {

View File

@ -8,6 +8,8 @@ import android.view.ViewStub;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -64,9 +66,9 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia); receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
} }
void bind(ConversationMessage conversationMessage, boolean running) { void bind(@NonNull LifecycleOwner lifecycleOwner, @Nullable ConversationMessage conversationMessage, boolean running) {
MessageRecord messageRecord = conversationMessage.getMessageRecord(); MessageRecord messageRecord = conversationMessage.getMessageRecord();
bindMessageView(conversationMessage); bindMessageView(lifecycleOwner, conversationMessage);
bindErrorState(messageRecord); bindErrorState(messageRecord);
bindSentReceivedDates(messageRecord); bindSentReceivedDates(messageRecord);
bindExpirationTime(messageRecord, running); bindExpirationTime(messageRecord, running);
@ -77,7 +79,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
bindExpirationTime(conversationMessage.getMessageRecord(), running); bindExpirationTime(conversationMessage.getMessageRecord(), running);
} }
private void bindMessageView(ConversationMessage conversationMessage) { private void bindMessageView(@NonNull LifecycleOwner lifecycleOwner, @Nullable ConversationMessage conversationMessage) {
if (conversationItem == null) { if (conversationItem == null) {
if (conversationMessage.getMessageRecord().isGroupAction()) { if (conversationMessage.getMessageRecord().isGroupAction()) {
conversationItem = (ConversationItem) updateStub.inflate(); conversationItem = (ConversationItem) updateStub.inflate();
@ -87,7 +89,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
conversationItem = (ConversationItem) receivedStub.inflate(); conversationItem = (ConversationItem) receivedStub.inflate();
} }
} }
conversationItem.bind(conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false); conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false);
} }
private void bindErrorState(MessageRecord messageRecord) { private void bindErrorState(MessageRecord messageRecord) {

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.util.livedata; package org.thoughtcrime.securesms.util.livedata;
import android.os.Handler;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MediatorLiveData;
@ -11,7 +13,6 @@ import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Function; import org.whispersystems.libsignal.util.guava.Function;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -86,7 +87,7 @@ public final class LiveDataUtil {
* Merges the supplied live data streams. * Merges the supplied live data streams.
*/ */
public static <T> LiveData<T> merge(@NonNull List<LiveData<T>> liveDataList) { public static <T> LiveData<T> merge(@NonNull List<LiveData<T>> liveDataList) {
Set<LiveData<T>> set = new LinkedHashSet<>(liveDataList); Set<LiveData<T>> set = new LinkedHashSet<>(liveDataList.size());
set.addAll(liveDataList); set.addAll(liveDataList);
@ -110,6 +111,40 @@ public final class LiveDataUtil {
return new MutableLiveData<>(item); return new MutableLiveData<>(item);
} }
/**
* Emits {@param whileWaiting} until {@param main} starts emitting.
*/
public static @NonNull <T> LiveData<T> until(@NonNull LiveData<T> main,
@NonNull LiveData<T> whileWaiting)
{
MediatorLiveData<T> mediatorLiveData = new MediatorLiveData<>();
mediatorLiveData.addSource(whileWaiting, mediatorLiveData::setValue);
mediatorLiveData.addSource(main, value -> {
mediatorLiveData.removeSource(whileWaiting);
mediatorLiveData.setValue(value);
});
return mediatorLiveData;
}
/**
* After {@param delay} ms after observation, emits a single Object, {@param value}.
*/
public static <T> LiveData<T> delay(long delay, T value) {
return new MutableLiveData<T>() {
boolean emittedValue;
@Override
protected void onActive() {
if (emittedValue) return;
new Handler().postDelayed(() -> setValue(value), delay);
emittedValue = true;
}
};
}
public interface Combine<A, B, R> { public interface Combine<A, B, R> {
@NonNull R apply(@NonNull A a, @NonNull B b); @NonNull R apply(@NonNull A a, @NonNull B b);
} }

View File

@ -1701,6 +1701,9 @@
<!-- conversation_item_received --> <!-- conversation_item_received -->
<string name="conversation_item_received__contact_photo_description">Contact photo</string> <string name="conversation_item_received__contact_photo_description">Contact photo</string>
<!-- ConversationUpdateItem -->
<string name="ConversationUpdateItem_loading">Loading</string>
<!-- audio_view --> <!-- audio_view -->
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string> <string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
<string name="audio_view__download_accessibility_description">Download</string> <string name="audio_view__download_accessibility_description">Download</string>