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"/> - - - - - - - - - - - - - + - + - + - + - + - + + + + + + + + + + + + + + + + + - - - + 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 (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) { + bodyView.setVisibility(VISIBLE); + bodyView.setText(body == null ? "" : body); + mediaDescriptionText.setVisibility(GONE); + return; + } - 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"); + 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 { - mediaDescription.setVisibility(View.GONE); - bodyView.setVisibility(View.VISIBLE); - - bodyView.setText(body == null ? "" : body); + } 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 (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 (dataInfo != null) { + 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); }