diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 05a237b932..45571e0080 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -249,6 +249,12 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 99eed8bf51..3539433ecd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -690,7 +690,7 @@ public class ConversationFragment extends Fragment {
private void handleDisplayDetails(MessageRecord message) {
- Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
+ Intent intent = new Intent(getActivity(), org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
index 2a75a7224c..2742a54394 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -1377,7 +1377,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (!shouldInterceptClicks(messageRecord) && parent != null) {
parent.onClick(v);
} else if (messageRecord.isFailed()) {
- Intent intent = new Intent(context, MessageDetailsActivity.class);
+ Intent intent = new Intent(context, org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
index 45222fe1ba..7a4765905e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java
@@ -78,6 +78,10 @@ public abstract class DisplayRecord {
!MmsSmsColumns.Types.isIdentityDefault(type);
}
+ public boolean isSent() {
+ return MmsSmsColumns.Types.isSentType(type);
+ }
+
public boolean isOutgoing() {
return MmsSmsColumns.Types.isOutgoingMessageType(type);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java
new file mode 100644
index 0000000000..1525309399
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java
@@ -0,0 +1,85 @@
+package org.thoughtcrime.securesms.messagedetails;
+
+import androidx.annotation.NonNull;
+
+import com.annimon.stream.ComparatorCompat;
+
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+
+final class MessageDetails {
+ private static final Comparator HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()));
+ private static final Comparator ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication()));
+ private static final Comparator RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL);
+
+ private final MessageRecord messageRecord;
+
+ private final Collection pending;
+ private final Collection sent;
+ private final Collection delivered;
+ private final Collection read;
+ private final Collection notSent;
+
+ MessageDetails(MessageRecord messageRecord, List recipients) {
+ this.messageRecord = messageRecord;
+
+ pending = new TreeSet<>(RECIPIENT_COMPARATOR);
+ sent = new TreeSet<>(RECIPIENT_COMPARATOR);
+ delivered = new TreeSet<>(RECIPIENT_COMPARATOR);
+ read = new TreeSet<>(RECIPIENT_COMPARATOR);
+ notSent = new TreeSet<>(RECIPIENT_COMPARATOR);
+
+ if (messageRecord.isOutgoing()) {
+ for (RecipientDeliveryStatus status : recipients) {
+ switch (status.getDeliveryStatus()) {
+ case UNKNOWN:
+ notSent.add(status);
+ break;
+ case PENDING:
+ pending.add(status);
+ break;
+ case SENT:
+ sent.add(status);
+ break;
+ case DELIVERED:
+ delivered.add(status);
+ break;
+ case READ:
+ read.add(status);
+ break;
+ }
+ }
+ } else {
+ sent.addAll(recipients);
+ }
+ }
+
+ @NonNull MessageRecord getMessageRecord() {
+ return messageRecord;
+ }
+
+ @NonNull Collection getPending() {
+ return pending;
+ }
+
+ @NonNull Collection getSent() {
+ return sent;
+ }
+
+ @NonNull Collection getDelivered() {
+ return delivered;
+ }
+
+ @NonNull Collection getRead() {
+ return read;
+ }
+
+ @NonNull Collection getNotSent() {
+ return notSent;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java
new file mode 100644
index 0000000000..2b8711f3e2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java
@@ -0,0 +1,147 @@
+package org.thoughtcrime.securesms.messagedetails;
+
+import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.color.MaterialColor;
+import org.thoughtcrime.securesms.messagedetails.MessageDetailsAdapter.MessageDetailsViewState;
+import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public final class MessageDetailsActivity extends PassphraseRequiredActionBarActivity {
+
+ public static final String MESSAGE_ID_EXTRA = "message_id";
+ public static final String THREAD_ID_EXTRA = "thread_id";
+ public static final String TYPE_EXTRA = "type";
+ public static final String RECIPIENT_EXTRA = "recipient_id";
+
+ private GlideRequests glideRequests;
+ private MessageDetailsViewModel viewModel;
+ private MessageDetailsAdapter adapter;
+
+ private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
+
+ @Override
+ protected void onPreCreate() {
+ dynamicTheme.onCreate(this);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+ setContentView(R.layout.message_details_2_activity);
+
+ glideRequests = GlideApp.with(this);
+
+ initializeList();
+ initializeViewModel();
+ initializeActionBar();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ adapter.resumeMessageExpirationTimer();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ adapter.pauseMessageExpirationTimer();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void initializeList() {
+ RecyclerView list = findViewById(R.id.message_details_list);
+ adapter = new MessageDetailsAdapter(glideRequests);
+
+ list.setAdapter(adapter);
+ }
+
+ private void initializeViewModel() {
+ final RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
+ final String type = getIntent().getStringExtra(TYPE_EXTRA);
+ final Long messageId = getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1);
+ final Factory factory = new Factory(recipientId, type, messageId);
+
+ viewModel = ViewModelProviders.of(this, factory).get(MessageDetailsViewModel.class);
+ viewModel.getMessageDetails().observe(this, details -> {
+ if (details == null) {
+ finish();
+ } else {
+ adapter.submitList(convertToRows(details));
+ }
+ });
+ }
+
+ private void initializeActionBar() {
+ assert getSupportActionBar() != null;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ viewModel.getRecipientColor().observe(this, this::setActionBarColor);
+ }
+
+ private void setActionBarColor(MaterialColor color) {
+ assert getSupportActionBar() != null;
+ getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ getWindow().setStatusBarColor(color.toStatusBarColor(this));
+ }
+ }
+
+ private List> convertToRows(MessageDetails details) {
+ List> list = new ArrayList<>();
+
+ list.add(new MessageDetailsViewState<>(details.getMessageRecord(), MessageDetailsViewState.MESSAGE_HEADER));
+
+ int headerOrder = 0;
+ if (details.getMessageRecord().isOutgoing()) {
+ if (addRecipients(list, RecipientHeader.notSent(headerOrder), details.getNotSent())) headerOrder++;
+ if (addRecipients(list, RecipientHeader.read(headerOrder), details.getRead())) headerOrder++;
+ if (addRecipients(list, RecipientHeader.delivered(headerOrder), details.getDelivered())) headerOrder++;
+ if (addRecipients(list, RecipientHeader.sentTo(headerOrder), details.getSent())) headerOrder++;
+ addRecipients(list, RecipientHeader.pending(headerOrder), details.getPending());
+ } else {
+ addRecipients(list, RecipientHeader.sentFrom(headerOrder), details.getSent());
+ }
+
+ return list;
+ }
+
+ private boolean addRecipients(List> list, RecipientHeader header, Collection recipients) {
+ if (recipients.isEmpty()) {
+ return false;
+ }
+
+ list.add(new MessageDetailsViewState<>(header, MessageDetailsViewState.RECIPIENT_HEADER));
+ for (RecipientDeliveryStatus status : recipients) {
+ list.add(new MessageDetailsViewState<>(status, MessageDetailsViewState.RECIPIENT));
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java
new file mode 100644
index 0000000000..4662fce859
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java
@@ -0,0 +1,150 @@
+package org.thoughtcrime.securesms.messagedetails;
+
+import android.annotation.SuppressLint;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+
+import java.util.List;
+
+final class MessageDetailsAdapter extends ListAdapter, RecyclerView.ViewHolder> {
+
+ private static final Object EXPIRATION_TIMER_CHANGE_PAYLOAD = new Object();
+
+ private final GlideRequests glideRequests;
+ private boolean running;
+
+ MessageDetailsAdapter(GlideRequests glideRequests) {
+ super(new MessageDetailsDiffer());
+ this.glideRequests = glideRequests;
+ running = true;
+ }
+
+ @Override
+ public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case MessageDetailsViewState.MESSAGE_HEADER:
+ return new MessageHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_header, parent, false), glideRequests);
+ case MessageDetailsViewState.RECIPIENT_HEADER:
+ return new RecipientHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_recipient_header, parent, false));
+ case MessageDetailsViewState.RECIPIENT:
+ return new RecipientViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_recipient, parent, false));
+ default:
+ throw new AssertionError("unknown view type");
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof MessageHeaderViewHolder) {
+ ((MessageHeaderViewHolder) holder).bind((MessageRecord) getItem(position).data, running);
+ } else if (holder instanceof RecipientHeaderViewHolder) {
+ ((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data);
+ } else if (holder instanceof RecipientViewHolder) {
+ ((RecipientViewHolder) holder).bind((RecipientDeliveryStatus) getItem(position).data);
+ } else {
+ throw new AssertionError("unknown view holder");
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List