diff --git a/build.gradle b/build.gradle
index 3b382f85df..bc2072ac6b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -75,7 +75,7 @@ dependencies {
compile('org.whispersystems:libpastelog:1.1.2') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
- compile 'org.whispersystems:signal-service-android:2.7.3'
+ compile 'org.whispersystems:signal-service-android:2.7.5'
compile 'org.whispersystems:webrtc-android:M64'
compile "me.leolin:ShortcutBadger:1.1.16"
@@ -164,7 +164,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:fe56b4db9ec743c8b565e3e4caa9228fafe132dc0bf82000d6e359b97a81177c',
- 'org.whispersystems:signal-service-android:dd0c21b37b239ac9c3eaf0b290791a3708817daa13e82e24b0544631f948d8d3',
+ 'org.whispersystems:signal-service-android:e0a3d55b21c1db483818ed459c500eba96dfb839e70d95dca4d8d4c1a7cd816b',
'org.whispersystems:webrtc-android:ed297e8b795dad9658cf306c2aa0f7d296c65f0997a2ac4353fd0157910acc12',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@@ -203,7 +203,7 @@ dependencyVerification {
'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15',
'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da',
'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1',
- 'org.whispersystems:signal-service-java:6654e52469b77db5c720de9557abe41bf99a9034c170c8a09e00bd2487c86430',
+ 'org.whispersystems:signal-service-java:7b4c34e3a346a236caebd5b81fb2985ed3c91a9974a8a8ddd36b6e1b8ae9350a',
'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
diff --git a/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/res/drawable-hdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000000..57c9fa5460
Binary files /dev/null and b/res/drawable-hdpi/ic_play_arrow_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/res/drawable-mdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000000..c61e948bbf
Binary files /dev/null and b/res/drawable-mdpi/ic_play_arrow_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/res/drawable-xhdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000000..a3c80e73da
Binary files /dev/null and b/res/drawable-xhdpi/ic_play_arrow_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000000..547ef30aac
Binary files /dev/null and b/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png b/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png
new file mode 100644
index 0000000000..be5c062b5f
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png differ
diff --git a/res/drawable/conversation_item_background_animated.xml b/res/drawable/conversation_item_background_animated.xml
new file mode 100644
index 0000000000..3664c2a1d7
--- /dev/null
+++ b/res/drawable/conversation_item_background_animated.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/res/drawable/dismiss_background.xml b/res/drawable/dismiss_background.xml
new file mode 100644
index 0000000000..fed2698e78
--- /dev/null
+++ b/res/drawable/dismiss_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/res/drawable/quote_background.xml b/res/drawable/quote_background.xml
index 1625c0c247..c12ab15dde 100644
--- a/res/drawable/quote_background.xml
+++ b/res/drawable/quote_background.xml
@@ -1,6 +1,6 @@
-
-
+
+
\ No newline at end of file
diff --git a/res/layout/conversation_input_panel.xml b/res/layout/conversation_input_panel.xml
index a2d398e109..8df38d5a61 100644
--- a/res/layout/conversation_input_panel.xml
+++ b/res/layout/conversation_input_panel.xml
@@ -34,7 +34,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
- app:quote_dismissable="true"
+ app:message_type="preview"
tools:visibility="visible"/>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 613267cc6d..f6dc025bce 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -243,7 +243,11 @@
-
+
+
+
+
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 9717cb40dd..41b67a8e9a 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -34,6 +34,7 @@
#20ffffff
#30ffffff
#40ffffff
+ #70ffffff
#aaffffff
#32000000
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 9a66bf4c7c..06da6395fb 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -26,6 +26,9 @@
100dp
320dp
+ 3dp
+ 1dp
+
3
10dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bc945e4e44..71d98b5741 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -194,6 +194,7 @@
SMS
Deleting
Deleting messages...
+ Quoted message not found
There is no browser installed on your device.
@@ -813,6 +814,13 @@
Pause
Download
+
+ Audio
+ Video
+ Photo
+ Document
+ You
+
Batch selection mode
%s selected
diff --git a/src/org/thoughtcrime/securesms/BindableConversationItem.java b/src/org/thoughtcrime/securesms/BindableConversationItem.java
index 601efb3b76..d0ad702c8d 100644
--- a/src/org/thoughtcrime/securesms/BindableConversationItem.java
+++ b/src/org/thoughtcrime/securesms/BindableConversationItem.java
@@ -1,9 +1,11 @@
package org.thoughtcrime.securesms;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -11,11 +13,18 @@ import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
- void bind(@NonNull MessageRecord messageRecord,
- @NonNull GlideRequests glideRequests,
- @NonNull Locale locale,
+ void bind(@NonNull MessageRecord messageRecord,
+ @NonNull GlideRequests glideRequests,
+ @NonNull Locale locale,
@NonNull Set batchSelected,
- @NonNull Recipient recipients);
+ @NonNull Recipient recipients,
+ boolean pulseHighlight);
MessageRecord getMessageRecord();
+
+ void setEventListener(@Nullable EventListener listener);
+
+ interface EventListener {
+ void onQuoteClicked(MmsMessageRecord messageRecord);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index cf040adc02..3b6e8c2f8a 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -65,7 +65,6 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
-import com.annimon.stream.Stream;
import com.google.android.gms.location.places.ui.PlacePicker;
import com.google.protobuf.ByteString;
@@ -2063,7 +2062,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
inputPanel.setQuote(GlideApp.with(this),
- messageRecord.getTimestamp(),
+ messageRecord.getDateSent(),
author,
messageRecord.getBody(),
messageRecord.isMms() ? ((MmsMessageRecord)messageRecord).getSlideDeck() : new SlideDeck());
diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java
index 32bf83a088..23317552aa 100644
--- a/src/org/thoughtcrime/securesms/ConversationAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java
@@ -33,7 +33,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
-import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
@@ -101,6 +100,8 @@ public class ConversationAdapter
private final @NonNull Calendar calendar;
private final @NonNull MessageDigest digest;
+ private MessageRecord recordToPulseHighlight;
+
protected static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(final @NonNull V itemView) {
super(itemView);
@@ -132,7 +133,7 @@ public class ConversationAdapter
}
- interface ItemClickListener {
+ interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MessageRecord item);
void onItemLongClick(MessageRecord item);
}
@@ -190,7 +191,10 @@ public class ConversationAdapter
@Override
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) {
long start = System.currentTimeMillis();
- viewHolder.getView().bind(messageRecord, glideRequests, locale, batchSelected, recipient);
+ viewHolder.getView().bind(messageRecord, glideRequests, locale, batchSelected, recipient, messageRecord == recordToPulseHighlight);
+ if (messageRecord == recordToPulseHighlight) {
+ recordToPulseHighlight = null;
+ }
Log.w(TAG, "Bind time: " + (System.currentTimeMillis() - start));
}
@@ -209,6 +213,7 @@ public class ConversationAdapter
}
return true;
});
+ itemView.setEventListener(clickListener);
Log.w(TAG, "Inflate time: " + (System.currentTimeMillis() - start));
return new ViewHolder(itemView);
}
@@ -341,6 +346,13 @@ public class ConversationAdapter
return Collections.unmodifiableSet(new HashSet<>(batchSelected));
}
+ public void pulseHighlightItem(int position) {
+ if (position < getItemCount()) {
+ recordToPulseHighlight = getRecordForPositionOrThrow(position);
+ notifyItemChanged(position);
+ }
+ }
+
private boolean hasAudio(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
}
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index 08afcf5a3e..1d978e0407 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
@@ -226,6 +227,7 @@ public class ConversationFragment extends Fragment
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_forward).setVisible(false);
+ menu.findItem(R.id.menu_context_reply).setVisible(false);
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_save_attachment).setVisible(false);
menu.findItem(R.id.menu_context_resend).setVisible(false);
@@ -240,6 +242,9 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_forward).setVisible(!actionMessage);
menu.findItem(R.id.menu_context_details).setVisible(!actionMessage);
+ menu.findItem(R.id.menu_context_reply).setVisible(!messageRecord.isPending() &&
+ !messageRecord.isFailed() &&
+ messageRecord.isSecure());
}
menu.findItem(R.id.menu_context_copy).setVisible(!actionMessage && hasText);
}
@@ -414,7 +419,6 @@ public class ConversationFragment extends Fragment
return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT), lastSeen);
}
-
@Override
public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
Log.w(TAG, "onLoadFinished");
@@ -593,7 +597,6 @@ public class ConversationFragment extends Fragment
setCorrectMenuVisibility(actionMode.getMenu());
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
}
-
}
}
@@ -606,6 +609,37 @@ public class ConversationFragment extends Fragment
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
}
+
+ @Override
+ public void onQuoteClicked(MmsMessageRecord messageRecord) {
+ if (messageRecord.getQuote() == null) {
+ Log.w(TAG, "Received a 'quote clicked' event, but there's no quote...");
+ return;
+ }
+
+ new AsyncTask() {
+ @Override
+ protected Integer doInBackground(Void... voids) {
+ return DatabaseFactory.getMmsSmsDatabase(getContext())
+ .getQuotedMessagePosition(threadId, messageRecord.getQuote().getId(), messageRecord.getQuote().getAuthor());
+ }
+
+ @Override
+ protected void onPostExecute(Integer position) {
+ if (position >= 0 && position < getListAdapter().getItemCount()) {
+ list.scrollToPosition(position);
+ getListAdapter().pulseHighlightItem(position);
+ } else {
+ Toast.makeText(getContext(), getResources().getText(R.string.ConversationFragment_quoted_message_not_found), Toast.LENGTH_SHORT).show();
+ if (position < 0) {
+ Log.w(TAG, "Tried to navigate to quoted message, but it was deleted.");
+ } else {
+ Log.w(TAG, "Tried to navigate to quoted message, but it was out of the bounds of the adapter.");
+ }
+ }
+ }
+ }.execute();
+ }
}
private class ActionModeCallback implements ActionMode.Callback {
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index c4bd2cdf9b..54a1504fd4 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -22,6 +22,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
+import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
@@ -126,17 +127,18 @@ public class ConversationItem extends LinearLayout
private DeliveryStatusView deliveryStatusIndicator;
private AlertView alertView;
- private @NonNull Set batchSelected = new HashSet<>();
- private @NonNull Recipient conversationRecipient;
- private @NonNull Stub mediaThumbnailStub;
- private @NonNull Stub audioViewStub;
- private @NonNull Stub documentViewStub;
- private @NonNull ExpirationTimerView expirationTimer;
+ private @NonNull Set batchSelected = new HashSet<>();
+ private @NonNull Recipient conversationRecipient;
+ private @NonNull Stub mediaThumbnailStub;
+ private @NonNull Stub audioViewStub;
+ private @NonNull Stub documentViewStub;
+ private @NonNull ExpirationTimerView expirationTimer;
+ private @Nullable EventListener eventListener;
private int defaultBubbleColor;
- private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
- private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
+ private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
+ private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final Context context;
@@ -191,7 +193,8 @@ public class ConversationItem extends LinearLayout
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set batchSelected,
- @NonNull Recipient conversationRecipient)
+ @NonNull Recipient conversationRecipient,
+ boolean pulseHighlight)
{
this.messageRecord = messageRecord;
this.locale = locale;
@@ -205,7 +208,7 @@ public class ConversationItem extends LinearLayout
this.conversationRecipient.addListener(this);
setMediaAttributes(messageRecord);
- setInteractionState(messageRecord);
+ setInteractionState(messageRecord, pulseHighlight);
setBodyText(messageRecord);
setBubbleState(messageRecord, recipient);
setStatusIcons(messageRecord);
@@ -217,6 +220,11 @@ public class ConversationItem extends LinearLayout
setQuote(messageRecord);
}
+ @Override
+ public void setEventListener(@Nullable EventListener eventListener) {
+ this.eventListener = eventListener;
+ }
+
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
@@ -243,6 +251,27 @@ public class ConversationItem extends LinearLayout
}
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (hasQuote(messageRecord)) {
+ int quoteWidth = quoteView.getMeasuredWidth();
+
+ int availableWidth;
+ if (hasThumbnail(messageRecord)) {
+ availableWidth = mediaThumbnailStub.get().getMeasuredWidth();
+ } else {
+ availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight();
+ }
+
+ if (quoteWidth != availableWidth) {
+ quoteView.getLayoutParams().width = availableWidth;
+ measure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+
private void initializeAttributes() {
final int[] attributes = new int[] {R.attr.conversation_item_bubble_background};
final TypedArray attrs = context.obtainStyledAttributes(attributes);
@@ -309,8 +338,17 @@ public class ConversationItem extends LinearLayout
}
}
- private void setInteractionState(MessageRecord messageRecord) {
- setSelected(batchSelected.contains(messageRecord));
+ private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) {
+ if (batchSelected.contains(messageRecord)) {
+ setBackgroundResource(R.drawable.conversation_item_background);
+ setSelected(true);
+ } else if (pulseHighlight) {
+ setBackgroundResource(R.drawable.conversation_item_background_animated);
+ setSelected(true);
+ postDelayed(() -> setSelected(false), 500);
+ } else {
+ setSelected(false);
+ }
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
@@ -346,6 +384,10 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
+ private boolean hasQuote(MessageRecord messageRecord) {
+ return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getQuote() != null;
+ }
+
private void setBodyText(MessageRecord messageRecord) {
bodyText.setClickable(false);
bodyText.setFocusable(false);
@@ -517,6 +559,16 @@ public class ConversationItem extends LinearLayout
assert quote != null;
quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quote.getText(), quote.getAttachment());
quoteView.setVisibility(View.VISIBLE);
+ quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+ quoteView.setOnClickListener(view -> {
+ if (eventListener != null && batchSelected.isEmpty()) {
+ eventListener.onQuoteClicked((MmsMessageRecord) messageRecord);
+ } else {
+ passthroughClickListener.onClick(view);
+ }
+ });
+ quoteView.setOnLongClickListener(passthroughClickListener);
} else {
quoteView.dismiss();
}
diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
index 48f71a744b..c56edee3da 100644
--- a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
@@ -66,17 +66,23 @@ public class ConversationUpdateItem extends LinearLayout
}
@Override
- public void bind(@NonNull MessageRecord messageRecord,
- @NonNull GlideRequests glideRequests,
- @NonNull Locale locale,
+ public void bind(@NonNull MessageRecord messageRecord,
+ @NonNull GlideRequests glideRequests,
+ @NonNull Locale locale,
@NonNull Set batchSelected,
- @NonNull Recipient conversationRecipient)
+ @NonNull Recipient conversationRecipient,
+ boolean pulseUpdate)
{
this.batchSelected = batchSelected;
bind(messageRecord, locale);
}
+ @Override
+ public void setEventListener(@Nullable EventListener listener) {
+ // No events to report yet
+ }
+
@Override
public MessageRecord getMessageRecord() {
return messageRecord;
diff --git a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
index 93abe19e7a..b279254222 100644
--- a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
+++ b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java
@@ -252,7 +252,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
- conversationItem.bind(messageRecord, glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient);
+ conversationItem.bind(messageRecord, glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, false);
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
}
diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
index a01b05aa27..5c8b40de6b 100644
--- a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
+import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.LinkedList;
import java.util.List;
@@ -16,7 +17,7 @@ public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, @NonNull String location,
- @Nullable String key, @NonNull String relay,
+ @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, boolean voiceNote,
int width, int height)
{
@@ -52,6 +53,22 @@ public class PointerAttachment extends Attachment {
return results;
}
+ public static List forPointers(List pointers) {
+ List results = new LinkedList<>();
+
+ if (pointers != null) {
+ for (SignalServiceDataMessage.Quote.QuotedAttachment pointer : pointers) {
+ Optional result = forPointer(pointer);
+
+ if (result.isPresent()) {
+ results.add(result.get());
+ }
+ }
+ }
+
+ return results;
+ }
+
public static Optional forPointer(Optional pointer) {
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent();
@@ -73,4 +90,20 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getHeight()));
}
+
+ public static Optional forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) {
+ SignalServiceAttachment thumbnail = pointer.getThumbnail();
+
+ return Optional.of(new PointerAttachment(pointer.getContentType(),
+ AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
+ thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
+ pointer.getFileName(),
+ String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0),
+ thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
+ null,
+ thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
+ false,
+ thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
+ thumbnail != null ? thumbnail.asPointer().getHeight() : 0));
+ }
}
diff --git a/src/org/thoughtcrime/securesms/color/MaterialColor.java b/src/org/thoughtcrime/securesms/color/MaterialColor.java
index 104649e202..ef3b57bef5 100644
--- a/src/org/thoughtcrime/securesms/color/MaterialColor.java
+++ b/src/org/thoughtcrime/securesms/color/MaterialColor.java
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.color;
import android.content.Context;
+import android.graphics.Color;
import android.support.annotation.NonNull;
import android.util.TypedValue;
@@ -61,27 +62,57 @@ public enum MaterialColor {
}
public int toConversationColor(@NonNull Context context) {
- if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
- return context.getResources().getColor(conversationColorDark);
- } else {
- return context.getResources().getColor(conversationColorLight);
- }
+ return context.getResources().getColor(isDarkTheme(context) ? conversationColorDark
+ : conversationColorLight);
}
public int toActionBarColor(@NonNull Context context) {
- if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
- return context.getResources().getColor(actionBarColorDark);
- } else {
- return context.getResources().getColor(actionBarColorLight);
- }
+ return context.getResources().getColor(isDarkTheme(context) ? actionBarColorDark
+ : actionBarColorLight);
}
public int toStatusBarColor(@NonNull Context context) {
- if (getAttribute(context, R.attr.theme_type, "light").equals("dark")) {
- return context.getResources().getColor(statusBarColorDark);
- } else {
- return context.getResources().getColor(statusBarColorLight);
+ return context.getResources().getColor(isDarkTheme(context) ? statusBarColorDark
+ : statusBarColorLight);
+ }
+
+ public int toQuoteTitleColor(@NonNull Context context) {
+ return context.getResources().getColor(conversationColorDark);
+ }
+
+ public int toQuoteBarColorResource(@NonNull Context context, boolean outgoing) {
+ if (outgoing) {
+ return conversationColorDark;
}
+ return R.color.white;
+ }
+
+ public int toQuoteBackgroundColor(@NonNull Context context, boolean outgoing) {
+ if (outgoing) {
+ int color = toConversationColor(context);
+ return Color.argb(0x44, Color.red(color), Color.green(color), Color.blue(color));
+ }
+ return context.getResources().getColor(isDarkTheme(context) ? R.color.transparent_white_70
+ : R.color.transparent_white_aa);
+ }
+
+ public int toQuoteOutlineColor(@NonNull Context context, boolean outgoing) {
+ if (!outgoing) {
+ return context.getResources().getColor(R.color.transparent_white_70);
+ }
+ return context.getResources().getColor(isDarkTheme(context) ? R.color.transparent_white_40
+ : R.color.grey_400_transparent);
+ }
+
+ public int toQuoteIconForegroundColor(@NonNull Context context, boolean outgoing) {
+ if (outgoing) {
+ return context.getResources().getColor(R.color.white);
+ }
+ return toConversationColor(context);
+ }
+
+ public int toQuoteIconBackgroundColor(@NonNull Context context, boolean outgoing) {
+ return context.getResources().getColor(toQuoteBarColorResource(context, outgoing));
}
public boolean represents(Context context, int colorValue) {
@@ -107,6 +138,9 @@ public enum MaterialColor {
}
}
+ private boolean isDarkTheme(@NonNull Context context) {
+ return getAttribute(context, R.attr.theme_type, "light").equals("dark");
+ }
public static MaterialColor fromSerialized(String serialized) throws UnknownColorException {
for (MaterialColor color : MaterialColor.values()) {
diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java
index 90627df6e2..0a5b656361 100644
--- a/src/org/thoughtcrime/securesms/components/QuoteView.java
+++ b/src/org/thoughtcrime/securesms/components/QuoteView.java
@@ -3,19 +3,22 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
import android.widget.TextView;
import com.annimon.stream.Stream;
@@ -23,11 +26,14 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
-import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
+import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
+import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
+import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.Util;
@@ -38,19 +44,31 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
private static final String TAG = QuoteView.class.getSimpleName();
+ private static final int MESSAGE_TYPE_PREVIEW = 0;
+ private static final int MESSAGE_TYPE_OUTGOING = 1;
+ private static final int MESSAGE_TYPE_INCOMING = 2;
+
+ private View rootView;
private TextView authorView;
private TextView bodyView;
private ImageView quoteBarView;
private ImageView attachmentView;
+ private ImageView attachmentVideoOverlayView;
+ private ViewGroup attachmentIconContainerView;
+ private ImageView attachmentIconView;
+ private ImageView attachmentIconBackgroundView;
private ImageView dismissView;
private long id;
private Recipient author;
private String body;
- private View mediaDescription;
- private ImageView mediaDescriptionIcon;
private TextView mediaDescriptionText;
private SlideDeck attachments;
+ private int messageType;
+ private int roundedCornerRadiusPx;
+
+ private final Path clipPath = new Path();
+ private final RectF drawRect = new RectF();
public QuoteView(Context context) {
super(context);
@@ -76,27 +94,47 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
private void initialize(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.quote_view, this);
- this.authorView = findViewById(R.id.quote_author);
- this.bodyView = findViewById(R.id.quote_text);
- this.quoteBarView = findViewById(R.id.quote_bar);
- this.attachmentView = findViewById(R.id.quote_attachment);
- this.dismissView = findViewById(R.id.quote_dismiss);
- this.mediaDescriptionIcon = findViewById(R.id.media_icon);
- this.mediaDescriptionText = findViewById(R.id.media_name);
- this.mediaDescription = findViewById(R.id.media_description);
+ this.rootView = findViewById(R.id.quote_root);
+ this.authorView = findViewById(R.id.quote_author);
+ this.bodyView = findViewById(R.id.quote_text);
+ this.quoteBarView = findViewById(R.id.quote_bar);
+ this.attachmentView = findViewById(R.id.quote_attachment);
+ this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay);
+ this.attachmentIconContainerView = findViewById(R.id.quote_attachment_icon_container);
+ this.attachmentIconView = findViewById(R.id.quote_attachment_icon);
+ this.attachmentIconBackgroundView = findViewById(R.id.quote_attachment_icon_background);
+ this.dismissView = findViewById(R.id.quote_dismiss);
+ this.mediaDescriptionText = findViewById(R.id.media_name);
+ this.roundedCornerRadiusPx = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
- boolean dismissable = typedArray.getBoolean(R.styleable.QuoteView_quote_dismissable, true);
+ messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
typedArray.recycle();
- if (!dismissable) dismissView.setVisibility(View.GONE);
- else dismissView.setVisibility(View.VISIBLE);
+ dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
}
- dismissView.setOnClickListener(view -> setVisibility(View.GONE));
+ dismissView.setOnClickListener(view -> setVisibility(GONE));
- setBackgroundDrawable(getContext().getResources().getDrawable(R.drawable.quote_background));
+ setWillNotDraw(false);
+ if (Build.VERSION.SDK_INT < 18) {
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawRect.left = 0;
+ drawRect.top = 0;
+ drawRect.right = getWidth();
+ drawRect.bottom = getHeight();
+
+ clipPath.reset();
+ clipPath.addRoundRect(drawRect, roundedCornerRadiusPx, roundedCornerRadiusPx, Path.Direction.CW);
+ canvas.clipPath(clipPath);
}
public void setQuote(GlideRequests glideRequests, long id, @NonNull Recipient author, @Nullable String body, @NonNull SlideDeck attachments) {
@@ -110,7 +148,7 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
author.addListener(this);
setQuoteAuthor(author);
setQuoteText(body, attachments);
- setQuoteAttachment(glideRequests, attachments);
+ setQuoteAttachment(glideRequests, attachments, author);
}
public void dismiss() {
@@ -120,7 +158,7 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
this.author = null;
this.body = null;
- setVisibility(View.GONE);
+ setVisibility(GONE);
}
@Override
@@ -133,54 +171,96 @@ public class QuoteView extends LinearLayout implements RecipientModifiedListener
}
private void setQuoteAuthor(@NonNull Recipient author) {
- this.authorView.setText(author.toShortString());
- this.authorView.setTextColor(author.getColor().toActionBarColor(getContext()));
- this.quoteBarView.setColorFilter(author.getColor().toActionBarColor(getContext()), PorterDuff.Mode.SRC_IN);
+ boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
+ boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress());
+
+ authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you)
+ : author.toShortString());
+ authorView.setTextColor(author.getColor().toQuoteTitleColor(getContext()));
+ // We use the raw color resource because Android 4.x was struggling with tints here
+ quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
+
+ GradientDrawable background = (GradientDrawable) rootView.getBackground();
+ background.setColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
+ background.setStroke(getResources().getDimensionPixelSize(R.dimen.quote_outline_width),
+ author.getColor().toQuoteOutlineColor(getContext(), outgoing));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
- if (TextUtils.isEmpty(body) && attachments.containsMediaSlide()) {
- mediaDescription.setVisibility(View.VISIBLE);
- bodyView.setVisibility(View.GONE);
-
- List audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
- List documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
- List imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
- List videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
-
- if (!audioSlides.isEmpty()) {
- mediaDescriptionIcon.setImageResource(R.drawable.ic_mic_white_24dp);
- mediaDescriptionText.setText("Audio");
- } else if (!documentSlides.isEmpty()) {
- mediaDescriptionIcon.setImageResource(R.drawable.ic_insert_drive_file_white_24dp);
- mediaDescriptionText.setText(String.format("%s (%s)", documentSlides.get(0).getFileName(), Util.getPrettyFileSize(documentSlides.get(0).getFileSize())));
- } else if (!videoSlides.isEmpty()) {
- mediaDescriptionIcon.setImageResource(R.drawable.ic_videocam_white_24dp);
- mediaDescriptionText.setText("Video");
- } else if (!imageSlides.isEmpty()) {
- mediaDescriptionIcon.setImageResource(R.drawable.ic_camera_alt_white_24dp);
- mediaDescriptionText.setText("Photo");
- }
- } else {
- mediaDescription.setVisibility(View.GONE);
- bodyView.setVisibility(View.VISIBLE);
-
+ if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
+ bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
+ mediaDescriptionText.setVisibility(GONE);
+ return;
+ }
+
+ bodyView.setVisibility(GONE);
+ mediaDescriptionText.setVisibility(VISIBLE);
+ mediaDescriptionText.setTypeface(null, Typeface.ITALIC);
+
+ List audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
+ List documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
+ List imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
+ List videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
+
+ // Given that most types have images, we specifically check images last
+ if (!audioSlides.isEmpty()) {
+ mediaDescriptionText.setText(R.string.QuoteView_audio);
+ } else if (!documentSlides.isEmpty()) {
+ String filename = documentSlides.get(0).getFileName().orNull();
+ if (!TextUtils.isEmpty(filename)) {
+ mediaDescriptionText.setTypeface(null, Typeface.NORMAL);
+ mediaDescriptionText.setText(filename);
+ } else {
+ mediaDescriptionText.setText(R.string.QuoteView_document);
+ }
+ } else if (!videoSlides.isEmpty()) {
+ mediaDescriptionText.setText(R.string.QuoteView_video);
+ } else if (!imageSlides.isEmpty()) {
+ mediaDescriptionText.setText(R.string.QuoteView_photo);
}
}
- private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
+ private void setQuoteAttachment(@NonNull GlideRequests glideRequests,
+ @NonNull SlideDeck slideDeck,
+ @NonNull Recipient author)
+ {
List imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList();
+ List audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
+ List documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
+
+ attachmentVideoOverlayView.setVisibility(GONE);
if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) {
- attachmentView.setVisibility(View.VISIBLE);
- dismissView.setBackgroundResource(R.drawable.circle_alpha);
+ attachmentView.setVisibility(VISIBLE);
+ attachmentIconContainerView.setVisibility(GONE);
+ dismissView.setBackgroundResource(R.drawable.dismiss_background);
+ if (imageVideoSlides.get(0).hasVideo()) {
+ attachmentVideoOverlayView.setVisibility(VISIBLE);
+ }
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(attachmentView);
+ } else if (!audioSlides.isEmpty() || !documentSlides.isEmpty()){
+ boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
+
+ dismissView.setBackgroundResource(R.drawable.circle_alpha);
+ attachmentView.setVisibility(GONE);
+ attachmentIconContainerView.setVisibility(VISIBLE);
+
+ if (!audioSlides.isEmpty()) {
+ attachmentIconView.setImageResource(R.drawable.ic_mic_white_48dp);
+ } else {
+ attachmentIconView.setImageResource(R.drawable.ic_insert_drive_file_white_24dp);
+ }
+
+ attachmentIconView.setColorFilter(author.getColor().toQuoteIconForegroundColor(getContext(), outgoing), PorterDuff.Mode.SRC_IN);
+ attachmentIconBackgroundView.setColorFilter(author.getColor().toQuoteIconBackgroundColor(getContext(), outgoing), PorterDuff.Mode.SRC_IN);
+
} else {
- attachmentView.setVisibility(View.GONE);
+ attachmentView.setVisibility(GONE);
+ attachmentIconContainerView.setVisibility(GONE);
dismissView.setBackgroundDrawable(null);
}
}
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index 98b2ed9fd5..540be57791 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -46,6 +46,8 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.util.BitmapDecodingException;
+import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
@@ -313,13 +315,20 @@ public class AttachmentDatabase extends Database {
public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId attachmentId, @NonNull InputStream inputStream)
throws MmsException
{
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- DataInfo dataInfo = setAttachmentData(inputStream);
- ContentValues values = new ContentValues();
+ DatabaseAttachment placeholder = getAttachment(attachmentId);
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ DataInfo dataInfo = setAttachmentData(inputStream);
+
+ if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) {
+ values.put(THUMBNAIL, dataInfo.file.getAbsolutePath());
+ values.put(THUMBNAIL_RANDOM, dataInfo.random);
+ } else {
+ values.put(DATA, dataInfo.file.getAbsolutePath());
+ values.put(SIZE, dataInfo.length);
+ values.put(DATA_RANDOM, dataInfo.random);
+ }
- values.put(DATA, dataInfo.file.getAbsolutePath());
- values.put(SIZE, dataInfo.length);
- values.put(DATA_RANDOM, dataInfo.random);
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, (String)null);
values.put(CONTENT_DISPOSITION, (String)null);
@@ -635,8 +644,22 @@ public class AttachmentDatabase extends Database {
long rowId = database.insert(TABLE_NAME, null, contentValues);
AttachmentId attachmentId = new AttachmentId(rowId, uniqueId);
+ Uri thumbnailUri = attachment.getThumbnailUri();
+ boolean hasThumbnail = false;
- if (dataInfo != null) {
+ if (thumbnailUri != null) {
+ try (InputStream attachmentStream = PartAuthority.getAttachmentStream(context, thumbnailUri)) {
+ Pair dimens = BitmapUtil.getDimensions(attachmentStream);
+ updateAttachmentThumbnail(attachmentId,
+ PartAuthority.getAttachmentStream(context, thumbnailUri),
+ (float) dimens.first / (float) dimens.second);
+ hasThumbnail = true;
+ } catch (IOException | BitmapDecodingException e) {
+ Log.w(TAG, "Failed to save existing thumbnail.", e);
+ }
+ }
+
+ if (!hasThumbnail && dataInfo != null) {
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri())) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri());
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 6ae773dfa0..68ffc478d0 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -27,6 +27,7 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
+import org.thoughtcrime.securesms.util.Util;
import java.util.HashSet;
import java.util.Set;
@@ -140,6 +141,26 @@ public class MmsSmsDatabase extends Database {
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false, true);
}
+ public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull Address address) {
+ String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
+ String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
+
+ try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.ADDRESS }, selection, order, null)) {
+ String serializedAddress = address.serialize();
+ boolean isOwnNumber = Util.isOwnNumber(context, address);
+
+ while (cursor != null && cursor.moveToNext()) {
+ boolean quoteIdMatches = cursor.getLong(0) == quoteId;
+ boolean addressMatches = serializedAddress.equals(cursor.getString(1));
+
+ if (quoteIdMatches && (addressMatches || isOwnNumber)) {
+ return cursor.getPosition();
+ }
+ }
+ }
+ return -1;
+ }
+
private Cursor queryTables(String[] projection, String selection, String order, String limit) {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index b6bb188045..846d821bd3 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -43,8 +43,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int MIGRATE_SESSIONS_VERSION = 4;
private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5;
private static final int ATTACHMENT_DIMENSIONS = 6;
+ private static final int QUOTED_REPLIES = 7;
- private static final int DATABASE_VERSION = 6;
+ private static final int DATABASE_VERSION = 7;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -167,6 +168,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN height INTEGER DEFAULT 0");
}
+ if (oldVersion < QUOTED_REPLIES) {
+ db.execSQL("ALTER TABLE mms ADD COLUMN quote_id INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE mms ADD COLUMN quote_author TEXT");
+ db.execSQL("ALTER TABLE mms ADD COLUMN quote_body TEXT");
+ db.execSQL("ALTER TABLE mms ADD COLUMN quote_attachment INTEGER DEFAULT -1");
+
+ db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 81edfe66ed..ab48fd9d1e 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -38,11 +38,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
-import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
+import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -894,7 +894,7 @@ public class PushDecryptJob extends ContextJob {
return Optional.of(new QuoteModel(quote.get().getId(),
author,
quote.get().getText(),
- PointerAttachment.forPointers(Optional.of(quote.get().getAttachments()))));
+ PointerAttachment.forPointers(quote.get().getAttachments())));
}
private Optional insertPlaceholder(@NonNull SignalServiceEnvelope envelope) {
diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 81a8a86cf4..f4e1870a84 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -28,6 +28,7 @@ import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
+import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -120,34 +121,35 @@ public abstract class PushSendJob extends SendJob {
protected Optional getQuoteFor(OutgoingMediaMessage message) {
if (message.getOutgoingQuote() == null) return Optional.absent();
- long quoteId = message.getOutgoingQuote().getId();
- String quoteBody = message.getOutgoingQuote().getText();
- Address quoteAuthor = message.getOutgoingQuote().getAuthor();
- List quoteAttachments = new LinkedList<>();
+ long quoteId = message.getOutgoingQuote().getId();
+ String quoteBody = message.getOutgoingQuote().getText();
+ Address quoteAuthor = message.getOutgoingQuote().getAuthor();
+ List quoteAttachments = new LinkedList<>();
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
- BitmapUtil.ScaleResult attachmentData = null;
+ BitmapUtil.ScaleResult thumbnailData = null;
+ SignalServiceAttachment thumbnail = null;
try {
if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
- attachmentData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024);
+ thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024);
} else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
- attachmentData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
+ thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
}
- if (attachmentData != null) {
- quoteAttachments.add(SignalServiceAttachment.newStreamBuilder()
- .withContentType("image/jpeg")
- .withFileName(attachment.getFileName())
- .withHeight(attachmentData.getHeight())
- .withWidth(attachmentData.getWidth())
- .withLength(attachmentData.getBitmap().length)
- .withStream(new ByteArrayInputStream(attachmentData.getBitmap()))
- .build());
- } else {
- quoteAttachments.add(new SignalServiceAttachmentPointer(0, attachment.getContentType(), null, null, Optional.absent(), Optional.absent(), 0, 0, Optional.absent(), Optional.fromNullable(attachment.getFileName()), attachment.isVoiceNote()));
+ if (thumbnailData != null) {
+ thumbnail = SignalServiceAttachment.newStreamBuilder()
+ .withContentType("image/jpeg")
+ .withWidth(thumbnailData.getWidth())
+ .withHeight(thumbnailData.getHeight())
+ .withLength(thumbnailData.getBitmap().length)
+ .withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
+ .build();
}
+ quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
+ attachment.getFileName(),
+ thumbnail));
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}