Improve storage management detail view descriptions.

This commit is contained in:
Alan Evans 2019-12-04 20:13:51 -05:00 committed by Greyson Parrelli
parent 544b75a2a7
commit 911ca7c29d
10 changed files with 264 additions and 93 deletions

View file

@ -13,34 +13,28 @@
<TextView
android:id="@+id/line1"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:ellipsize="marquee"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:singleLine="true"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/image_container"
app:layout_constraintTop_toTopOf="parent"
tools:text="Sent voice note, 02:35"
tools:visibility="visible" />
tools:text="Sent voice note, 02:35" />
<TextView
android:id="@+id/line2"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:ellipsize="marquee"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:singleLine="true"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/image_container"
app:layout_constraintTop_toTopOf="parent"
tools:text="2.7 MB, 11.06.19 at 5:25 AM"
tools:visibility="visible" />
tools:text="2.7 MB, 11.06.19 at 5:25 AM" />
</LinearLayout>

View file

@ -353,11 +353,7 @@
<string name="DeviceListItem_today">Today</string>
<!-- DocumentView -->
<string name="DocumentView_unknown_file">Unknown file</string>
<string name="DocumentView_unnamed_file">Unnamed file</string>
<string name="DocumentView_audio_file">Audio file</string>
<string name="DocumentView_image_file">Image file</string>
<string name="DocumentView_video_file">Video file</string>
<!-- DozeReminder -->
<string name="DozeReminder_optimize_for_missing_play_services">Optimize for missing Play Services</string>
@ -486,6 +482,9 @@
<!-- MediaOverviewActivity -->
<string name="MediaOverviewActivity_Media">Media</string>
<string name="MediaOverviewActivity_Files">Files</string>
<string name="MediaOverviewActivity_Audio">Audio</string>
<string name="MediaOverviewActivity_All">All</string>
<plurals name="MediaOverviewActivity_Media_delete_confirm_title">
<item quantity="one">Delete selected item?</item>
<item quantity="other">Delete selected items?</item>
@ -496,11 +495,8 @@
</plurals>
<string name="MediaOverviewActivity_Media_delete_progress_title">Deleting</string>
<string name="MediaOverviewActivity_Media_delete_progress_message">Deleting messages…</string>
<string name="MediaOverviewActivity_Files">Files</string>
<string name="MediaOverviewActivity_Select_all">Select all</string>
<string name="MediaOverviewActivity_collecting_attachments">Collecting attachments…</string>
<string name="MediaOverviewActivity_Audio">Audio</string>
<string name="MediaOverviewActivity_All">All</string>
<string name="MediaOverviewActivity_Sort_by">Sort by</string>
<string name="MediaOverviewActivity_Newest">Newest</string>
<string name="MediaOverviewActivity_Oldest">Oldest</string>
@ -510,6 +506,18 @@
<string name="MediaOverviewActivity_List_view_description">List view</string>
<string name="MediaOverviewActivity_Selected_description">Selected</string>
<string name="MediaOverviewActivity_file">File</string>
<string name="MediaOverviewActivity_audio">Audio</string>
<string name="MediaOverviewActivity_video">Video</string>
<string name="MediaOverviewActivity_image">Image</string>
<string name="MediaOverviewActivity_detail_line_2_part" translatable="false">%1$s &#183; %2$s</string>
<string name="MediaOverviewActivity_detail_line_3_part" translatable="false">%1$s &#183; %2$s &#183; %3$s</string>
<string name="MediaOverviewActivity_sent_by_s">Sent by %1$s</string>
<string name="MediaOverviewActivity_sent_by_you">Sent by you</string>
<string name="MediaOverviewActivity_sent_by_s_to_s">Sent by %1$s to %2$s</string>
<string name="MediaOverviewActivity_sent_by_you_to_s">Sent by you to %1$s</string>
<!--- NotificationBarManager -->
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
<string name="NotificationBarManager__establishing_signal_call">Establishing Signal call</string>

View file

@ -52,7 +52,6 @@ import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
@ -722,13 +721,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
RecipientId recipientId = mediaRecord.getRecipientId();
RecipientId threadRecipientId = DatabaseFactory.getThreadDatabase(context)
.getRecipientIdForThreadId(mediaRecord.getThreadId());
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError();
return new MediaItem(Recipient.live(recipientId).get(),
threadRecipientId != null ? Recipient.live(threadRecipientId).get() : null,
Recipient.live(threadRecipientId).get(),
mediaRecord.getAttachment(),
mediaRecord.getAttachment().getDataUri(),
mediaRecord.getContentType(),

View file

@ -5,6 +5,7 @@ import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import net.sqlcipher.database.SQLiteDatabase;
@ -17,7 +18,8 @@ import java.util.List;
public class MediaDatabase extends Database {
public static final int ALL_THREADS = -1;
public static final int ALL_THREADS = -1;
private static final String THREAD_RECIPIENT_ID = "THREAD_RECIPIENT_ID";
private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
@ -48,9 +50,12 @@ public class MediaDatabase extends Database {
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + " "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + ", "
+ ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " as " + THREAD_RECIPIENT_ID + " "
+ "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME
+ " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " "
+ "LEFT JOIN " + ThreadDatabase.TABLE_NAME
+ " ON " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + " "
+ "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID
+ " FROM " + MmsDatabase.TABLE_NAME
+ " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND "
@ -167,16 +172,24 @@ public class MediaDatabase extends Database {
private final DatabaseAttachment attachment;
private final RecipientId recipientId;
private final RecipientId threadRecipientId;
private final long threadId;
private final long date;
private final boolean outgoing;
private MediaRecord(DatabaseAttachment attachment, @NonNull RecipientId recipientId, long threadId, long date, boolean outgoing) {
this.attachment = attachment;
this.recipientId = recipientId;
this.threadId = threadId;
this.date = date;
this.outgoing = outgoing;
private MediaRecord(@Nullable DatabaseAttachment attachment,
@NonNull RecipientId recipientId,
@NonNull RecipientId threadRecipientId,
long threadId,
long date,
boolean outgoing)
{
this.attachment = attachment;
this.recipientId = recipientId;
this.threadRecipientId = threadRecipientId;
this.threadId = threadId;
this.date = date;
this.outgoing = outgoing;
}
public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) {
@ -194,10 +207,17 @@ public class MediaDatabase extends Database {
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED));
}
return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, recipientId, threadId, date, outgoing);
RecipientId threadRecipient = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_RECIPIENT_ID)));
return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null,
recipientId,
threadRecipient,
threadId,
date,
outgoing);
}
public DatabaseAttachment getAttachment() {
public @Nullable DatabaseAttachment getAttachment() {
return attachment;
}
@ -209,6 +229,10 @@ public class MediaDatabase extends Database {
return recipientId;
}
public @NonNull RecipientId getThreadRecipientId() {
return threadRecipientId;
}
public long getThreadId() {
return threadId;
}

View file

@ -17,12 +17,14 @@
package org.thoughtcrime.securesms.mediaoverview;
import android.content.Context;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.RecyclerView;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
@ -37,9 +39,14 @@ import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader.Grou
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
import java.util.HashMap;
@ -50,6 +57,7 @@ import java.util.Map;
final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
private final Context context;
private final boolean showThread;
private final GlideRequests glideRequests;
private final ItemClickListener itemClickListener;
private final Map<AttachmentId, MediaRecord> selected = new HashMap<>();
@ -63,12 +71,18 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
private static final int GALLERY_DETAIL = 3;
private static final int DOCUMENT_DETAIL = 4;
public void pause(RecyclerView.ViewHolder holder) {
void pause(RecyclerView.ViewHolder holder) {
if (holder instanceof AudioDetailViewHolder) {
((AudioDetailViewHolder) holder).pause();
}
}
void detach(RecyclerView.ViewHolder holder) {
if (holder instanceof SelectableViewHolder) {
((SelectableViewHolder) holder).onDetached();
}
}
private static class HeaderHolder extends HeaderViewHolder {
TextView textView;
@ -82,13 +96,15 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
@NonNull GlideRequests glideRequests,
GroupedThreadMedia media,
ItemClickListener clickListener,
boolean showFileSizes)
boolean showFileSizes,
boolean showThread)
{
this.context = context;
this.glideRequests = glideRequests;
this.media = media;
this.itemClickListener = clickListener;
this.showFileSizes = showFileSizes;
this.showThread = showThread;
}
public void setMedia(GroupedThreadMedia media) {
@ -143,7 +159,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder instanceof SelectableViewHolder) {
((SelectableViewHolder) holder).detached();
((SelectableViewHolder) holder).onDetached();
}
}
@ -182,8 +198,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
void selectAllMedia() {
for (int section = 0; section < media.getSectionCount(); section++) {
for (int item = 0; item < media.getSectionItemCount(section); item++) {
int sectionCount = media.getSectionCount();
for (int section = 0; section < sectionCount; section++) {
int sectionItemCount = media.getSectionItemCount(section);
for (int item = 0; item < sectionItemCount; item++) {
MediaRecord mediaRecord = media.get(section, item);
selected.put(mediaRecord.getAttachment().getAttachmentId(), mediaRecord);
}
@ -203,6 +221,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
private final View selectedIndicator;
private MediaDatabase.MediaRecord mediaRecord;
private boolean bound;
SelectableViewHolder(@NonNull View itemView) {
super(itemView);
@ -210,8 +229,16 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
if (bound) {
unbind();
}
this.mediaRecord = mediaRecord;
updateSelectedView();
bound = true;
}
void unbind() {
bound = false;
}
private void updateSelectedView() {
@ -226,7 +253,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
return true;
}
void detached() {
void onDetached() {
if (bound) {
unbind();
}
}
}
@ -259,16 +289,22 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
void detached() {
void unbind() {
thumbnailView.clear(glideRequests);
super.unbind();
}
}
private class DetailViewHolder extends SelectableViewHolder {
private abstract class DetailViewHolder extends SelectableViewHolder implements Observer<Pair<Recipient, Recipient>> {
protected final View itemView;
private final TextView line1;
private final TextView line2;
protected final View itemView;
private final TextView line1;
private final TextView line2;
private LiveDataPair<Recipient, Recipient> liveDataPair;
private Optional<String> fileName;
private String fileTypeDescription;
private Handler handler;
private Runnable selectForMarque;
DetailViewHolder(@NonNull View itemView) {
super(itemView);
@ -281,27 +317,82 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
super.bind(context, mediaRecord, slide);
line1.setText(getLine1(context, slide));
line2.setText(getLine2(mediaRecord, slide));
line1.setVisibility(View.VISIBLE);
line2.setVisibility(View.VISIBLE);
fileName = slide.getFileName();
fileTypeDescription = getFileTypeDescription(context, slide);
line1.setText(fileName.or(fileTypeDescription));
line2.setText(getLine2(context, mediaRecord, slide));
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
itemView.setOnLongClickListener(view -> onLongClick());
selectForMarque = () -> line1.setSelected(true);
handler = new Handler();
handler.postDelayed(selectForMarque, 2500);
LiveRecipient from = mediaRecord.isOutgoing() ? Recipient.self().live() : Recipient.live(mediaRecord.getRecipientId());
LiveRecipient to = Recipient.live(mediaRecord.getThreadRecipientId());
liveDataPair = new LiveDataPair<>(from.getLiveData(), to.getLiveData(), Recipient.UNKNOWN, Recipient.UNKNOWN);
liveDataPair.observeForever(this);
}
private String getLine1(@NonNull Context context, @NonNull Slide slide) {
return slide.getFileName()
.or(slide.getCaption())
.or(() -> describeUnnamedFile(context, slide));
@Override
void unbind() {
liveDataPair.removeObserver(this);
handler.removeCallbacks(selectForMarque);
line1.setSelected(false);
super.unbind();
}
private String getLine2(@NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
String date = DateUtils.formatDate(Locale.getDefault(), mediaRecord.getDate());
return Util.getPrettyFileSize(slide.getFileSize()) + " " + date;
private String getLine2(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
return context.getString(R.string.MediaOverviewActivity_detail_line_3_part,
Util.getPrettyFileSize(slide.getFileSize()),
getFileTypeDescription(context, slide),
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate()));
}
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
return context.getString(R.string.DocumentView_unnamed_file);
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide){
return context.getString(R.string.MediaOverviewActivity_file);
}
@Override
public void onChanged(Pair<Recipient, Recipient> fromToPair) {
line1.setText(describe(fromToPair.first(), fromToPair.second()));
}
private String describe(@NonNull Recipient from, @NonNull Recipient thread) {
if (from == Recipient.UNKNOWN && thread == Recipient.UNKNOWN) {
return fileName.or(fileTypeDescription);
}
String sentFromToString = getSentFromToString(from, thread);
if (fileName.isPresent()) {
return context.getString(R.string.MediaOverviewActivity_detail_line_2_part,
fileName.get(),
sentFromToString);
} else {
return sentFromToString;
}
}
private String getSentFromToString(@NonNull Recipient from, @NonNull Recipient thread) {
if (from.isLocalNumber() && from == thread) {
return context.getString(R.string.note_to_self);
}
if (showThread && (from.isLocalNumber() || thread.isGroup())) {
if (from.isLocalNumber()) {
return context.getString(R.string.MediaOverviewActivity_sent_by_you_to_s, thread.toShortString(context));
} else {
return context.getString(R.string.MediaOverviewActivity_sent_by_s_to_s, from.toShortString(context), thread.toShortString(context));
}
} else {
if (from.isLocalNumber()) {
return context.getString(R.string.MediaOverviewActivity_sent_by_you);
} else {
return context.getString(R.string.MediaOverviewActivity_sent_by_s, from.toShortString(context));
}
}
}
}
@ -345,13 +436,14 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
void detached() {
void unbind() {
audioView.stopPlaybackAndReset();
super.unbind();
}
@Override
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
return context.getString(R.string.DocumentView_audio_file);
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
return context.getString(R.string.MediaOverviewActivity_audio);
}
public void pause() {
@ -377,15 +469,16 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
@Override
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
if (slide.hasVideo()) return context.getString(R.string.DocumentView_video_file);
if (slide.hasImage()) return context.getString(R.string.DocumentView_image_file);
return super.describeUnnamedFile(context, slide);
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
if (slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video);
if (slide.hasImage()) return context.getString(R.string.MediaOverviewActivity_image);
return super.getFileTypeDescription(context, slide);
}
@Override
void detached() {
void unbind() {
thumbnailView.clear(glideRequests);
super.unbind();
}
}

View file

@ -103,7 +103,8 @@ public final class MediaOverviewPageFragment extends Fragment
GlideApp.with(this),
new GroupedThreadMediaLoader.EmptyGroupedThreadMedia(),
this,
sorting.isRelatedToFileSize());
sorting.isRelatedToFileSize(),
threadId == MediaDatabase.ALL_THREADS);
this.recyclerView.setAdapter(adapter);
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);
@ -182,7 +183,16 @@ public final class MediaOverviewPageFragment extends Fragment
int childCount = recyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
adapter.pause(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
int childCount = recyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
adapter.detach(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
}
}
private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {

View file

@ -6,6 +6,7 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
@ -168,6 +169,10 @@ public final class LiveRecipient {
set(recipient);
}
public @NonNull LiveData<Recipient> getLiveData() {
return liveData;
}
private @NonNull Recipient fetchRecipientFromDisk(RecipientId id) {
RecipientSettings settings = recipientDatabase.getRecipientSettings(id);
RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings)

View file

@ -4,7 +4,6 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.Transformations;
@ -14,6 +13,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
@ -201,27 +201,4 @@ public final class RegistrationViewModel extends ViewModel {
}
}, this::setKeyBackupCurrentToken);
}
public static class LiveDataPair<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public LiveDataPair(LiveData<A> ld1, LiveData<B> ld2) {
setValue(new Pair<>(a, b));
addSource(ld1, (a) -> {
if(a != null) {
this.a = a;
}
setValue(new Pair<>(a, b));
});
addSource(ld2, (b) -> {
if(b != null) {
this.b = b;
}
setValue(new Pair<>(a, b));
});
}
}
}

View file

@ -24,6 +24,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@ -136,6 +137,10 @@ public class DateUtils extends android.text.format.DateUtils {
return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale);
}
public static String formatDateWithoutDayOfWeek(@NonNull Locale locale, long timestamp) {
return getFormattedDateTime(timestamp, "MMM d yyyy", locale);
}
public static boolean isSameDay(long t1, long t2) {
return DATE_FORMAT.format(new Date(t1)).equals(DATE_FORMAT.format(new Date(t2)));
}

View file

@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.util.livedata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import org.whispersystems.libsignal.util.Pair;
public final class LiveDataPair<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public LiveDataPair(@NonNull LiveData<A> liveDataA,
@NonNull LiveData<B> liveDataB)
{
this(liveDataA, liveDataB, null, null);
}
public LiveDataPair(@NonNull LiveData<A> liveDataA,
@NonNull LiveData<B> liveDataB,
@Nullable A initialA,
@Nullable B initialB)
{
a = initialA;
b = initialB;
setValue(new Pair<>(a, b));
if (liveDataA == liveDataB) {
addSource(liveDataA, (a) -> {
if (a != null) {
this.a = a;
//noinspection unchecked: A is B if live datas are same instance
this.b = (B) a;
}
setValue(new Pair<>(a, b));
});
} else {
addSource(liveDataA, (a) -> {
if (a != null) {
this.a = a;
}
setValue(new Pair<>(a, b));
});
addSource(liveDataB, (b) -> {
if (b != null) {
this.b = b;
}
setValue(new Pair<>(a, b));
});
}
}
}