diff --git a/build.gradle b/build.gradle index 562ee7a3c5..3b382f85df 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.2' + compile 'org.whispersystems:signal-service-android:2.7.3' 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:a7dfcb2f88ec69e8a1d31215cc7b67f0db50a96cd9d3832bfe75f56e67188537', + 'org.whispersystems:signal-service-android:dd0c21b37b239ac9c3eaf0b290791a3708817daa13e82e24b0544631f948d8d3', '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:f5ca4595eb09e25b9c9fd39c83bdcf1978a61d8a4b6f770bb548f3dd40ecc493', + 'org.whispersystems:signal-service-java:6654e52469b77db5c720de9557abe41bf99a9034c170c8a09e00bd2487c86430', 'com.github.bumptech.glide:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b', 'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', diff --git a/res/drawable-hdpi/ic_close_white_18dp.png b/res/drawable-hdpi/ic_close_white_18dp.png new file mode 100644 index 0000000000..b4e25fbf03 Binary files /dev/null and b/res/drawable-hdpi/ic_close_white_18dp.png differ diff --git a/res/drawable-hdpi/ic_insert_photo_white_18dp.png b/res/drawable-hdpi/ic_insert_photo_white_18dp.png new file mode 100644 index 0000000000..c596091436 Binary files /dev/null and b/res/drawable-hdpi/ic_insert_photo_white_18dp.png differ diff --git a/res/drawable-hdpi/ic_reply_white_24dp.png b/res/drawable-hdpi/ic_reply_white_24dp.png new file mode 100644 index 0000000000..0424c2bd6d Binary files /dev/null and b/res/drawable-hdpi/ic_reply_white_24dp.png differ diff --git a/res/drawable-mdpi/ic_close_white_18dp.png b/res/drawable-mdpi/ic_close_white_18dp.png new file mode 100644 index 0000000000..01bc75c84a Binary files /dev/null and b/res/drawable-mdpi/ic_close_white_18dp.png differ diff --git a/res/drawable-mdpi/ic_insert_photo_white_18dp.png b/res/drawable-mdpi/ic_insert_photo_white_18dp.png new file mode 100644 index 0000000000..112e981d2b Binary files /dev/null and b/res/drawable-mdpi/ic_insert_photo_white_18dp.png differ diff --git a/res/drawable-mdpi/ic_reply_white_24dp.png b/res/drawable-mdpi/ic_reply_white_24dp.png new file mode 100644 index 0000000000..862114b82d Binary files /dev/null and b/res/drawable-mdpi/ic_reply_white_24dp.png differ diff --git a/res/drawable-xhdpi/ic_close_white_18dp.png b/res/drawable-xhdpi/ic_close_white_18dp.png new file mode 100644 index 0000000000..ceb1a1eebf Binary files /dev/null and b/res/drawable-xhdpi/ic_close_white_18dp.png differ diff --git a/res/drawable-xhdpi/ic_insert_photo_white_18dp.png b/res/drawable-xhdpi/ic_insert_photo_white_18dp.png new file mode 100644 index 0000000000..25a9576ddd Binary files /dev/null and b/res/drawable-xhdpi/ic_insert_photo_white_18dp.png differ diff --git a/res/drawable-xhdpi/ic_reply_white_24dp.png b/res/drawable-xhdpi/ic_reply_white_24dp.png new file mode 100644 index 0000000000..885623e4d0 Binary files /dev/null and b/res/drawable-xhdpi/ic_reply_white_24dp.png differ diff --git a/res/drawable-xxhdpi/ic_close_white_18dp.png b/res/drawable-xxhdpi/ic_close_white_18dp.png new file mode 100644 index 0000000000..86bd673afc Binary files /dev/null and b/res/drawable-xxhdpi/ic_close_white_18dp.png differ diff --git a/res/drawable-xxhdpi/ic_insert_photo_white_18dp.png b/res/drawable-xxhdpi/ic_insert_photo_white_18dp.png new file mode 100644 index 0000000000..5dd7b66eff Binary files /dev/null and b/res/drawable-xxhdpi/ic_insert_photo_white_18dp.png differ diff --git a/res/drawable-xxhdpi/ic_reply_white_24dp.png b/res/drawable-xxhdpi/ic_reply_white_24dp.png new file mode 100644 index 0000000000..de0dad2047 Binary files /dev/null and b/res/drawable-xxhdpi/ic_reply_white_24dp.png differ diff --git a/res/drawable-xxxhdpi/ic_close_white_18dp.png b/res/drawable-xxxhdpi/ic_close_white_18dp.png new file mode 100644 index 0000000000..6b717e0dda Binary files /dev/null and b/res/drawable-xxxhdpi/ic_close_white_18dp.png differ diff --git a/res/drawable-xxxhdpi/ic_insert_photo_white_18dp.png b/res/drawable-xxxhdpi/ic_insert_photo_white_18dp.png new file mode 100644 index 0000000000..f9f1defa6d Binary files /dev/null and b/res/drawable-xxxhdpi/ic_insert_photo_white_18dp.png differ diff --git a/res/drawable-xxxhdpi/ic_reply_white_24dp.png b/res/drawable-xxxhdpi/ic_reply_white_24dp.png new file mode 100644 index 0000000000..ed85f50ab9 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_reply_white_24dp.png differ diff --git a/res/drawable/quote_background.xml b/res/drawable/quote_background.xml new file mode 100644 index 0000000000..1625c0c247 --- /dev/null +++ b/res/drawable/quote_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/quote_bar.xml b/res/drawable/quote_bar.xml new file mode 100644 index 0000000000..5a139a97b4 --- /dev/null +++ b/res/drawable/quote_bar.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_input_panel.xml b/res/layout/conversation_input_panel.xml index c7ed6911fa..a2d398e109 100644 --- a/res/layout/conversation_input_panel.xml +++ b/res/layout/conversation_input_panel.xml @@ -1,9 +1,8 @@ - - + android:clipToPadding="false" + android:orientation="vertical"> - - - - - + android:visibility="gone" + app:quote_dismissable="true" + tools:visibility="visible"/> - + - + android:contentDescription="@string/conversation_activity__emoji_toggle_description" /> - + + + + - + - + - + + + + + + + - diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 50ae5a5f72..a179017cc4 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -75,6 +75,14 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/conversation_context.xml b/res/menu/conversation_context.xml index e5955f5c4a..45291fd085 100644 --- a/res/menu/conversation_context.xml +++ b/res/menu/conversation_context.xml @@ -30,4 +30,11 @@ android:visible="false" android:icon="?menu_save_icon" app:showAsAction="always" /> + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 419777bb06..613267cc6d 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -108,6 +108,7 @@ + @@ -241,5 +242,9 @@ + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index f506215986..9717cb40dd 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -23,6 +23,19 @@ #ff383838 #ff111111 + #05000000 + #10000000 + #20000000 + #30000000 + #40000000 + + #05ffffff + #10ffffff + #20ffffff + #30ffffff + #40ffffff + #aaffffff + #32000000 @color/gray65 diff --git a/res/values/themes.xml b/res/values/themes.xml index 337b2b6f66..5e6da48406 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -219,6 +219,7 @@ @drawable/ic_info_outline_white_24dp @drawable/ic_forward_white_24dp @drawable/ic_save_white_24dp + @drawable/ic_reply_white_24dp @drawable/ic_audio_light @drawable/ic_video_light @@ -343,6 +344,7 @@ @drawable/ic_info_outline_white_24dp @drawable/ic_forward_white_24dp @drawable/ic_save_white_24dp + @drawable/ic_reply_white_24dp @drawable/ic_audio_dark @drawable/ic_video_dark diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 525575675d..cf040adc02 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -65,6 +65,7 @@ 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; @@ -111,6 +112,8 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.identity.IdentityRecordList; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; @@ -766,7 +769,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity .setType(GroupContext.Type.QUIT) .build(); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipient(), context, null, System.currentTimeMillis(), 0); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipient(), context, null, System.currentTimeMillis(), 0, null); MessageSender.send(self, outgoingMessage, threadId, false, null); DatabaseFactory.getGroupDatabase(self).remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(self))); initializeEnabledCheck(); @@ -1648,7 +1651,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity handleUnverifiedRecipients(); } else if (!forceSms && identityRecords.isUntrusted()) { handleUntrustedRecipients(); - } else if (attachmentManager.isAttachmentPresent() || recipient.isGroupRecipient() || recipient.getAddress().isEmail()) { + } else if (attachmentManager.isAttachmentPresent() || recipient.isGroupRecipient() || recipient.getAddress().isEmail() || inputPanel.getQuote().isPresent()) { sendMediaMessage(forceSms, expiresIn, subscriptionId, initiating); } else { sendTextMessage(forceSms, expiresIn, subscriptionId, initiating); @@ -1668,12 +1671,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void sendMediaMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, boolean initiating) throws InvalidMessageException { + Log.w(TAG, "Sending media message..."); sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId, initiating); } private ListenableFuture sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId, final boolean initiating) { - - OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType); + OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull()); final SettableFuture future = new SettableFuture<>(); final Context context = getApplicationContext(); @@ -1691,6 +1694,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity .ifNecessary(!isSecureText || forceSms) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms)) .onAllGranted(() -> { + inputPanel.clearQuote(); attachmentManager.clear(glideRequests, false); composeText.setText(""); final long id = fragment.stageOutgoingMessage(outgoingMessage); @@ -2048,6 +2052,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity this.threadId = threadId; } + @Override + public void handleReplyMessage(MessageRecord messageRecord) { + Recipient author; + + if (messageRecord.isOutgoing()) { + author = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), true); + } else { + author = messageRecord.getIndividualRecipient(); + } + + inputPanel.setQuote(GlideApp.with(this), + messageRecord.getTimestamp(), + author, + messageRecord.getBody(), + messageRecord.isMms() ? ((MmsMessageRecord)messageRecord).getSlideDeck() : new SlideDeck()); + } + @Override public void onAttachmentChanged() { handleSecurityChange(isSecureText, isDefaultSms); diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index ac0d48a02d..32bf83a088 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -29,7 +29,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +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; @@ -54,6 +57,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -260,10 +264,11 @@ public class ConversationAdapter @Override public long getItemId(@NonNull Cursor cursor) { - String fastPreflightId = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.FAST_PREFLIGHT_ID)); + List attachments = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(cursor); + List messageAttachments = Stream.of(attachments).filterNot(DatabaseAttachment::isQuote).toList(); - if (fastPreflightId != null) { - return Long.valueOf(fastPreflightId); + if (messageAttachments.size() > 0 && messageAttachments.get(0).getFastPreflightId() != null) { + return Long.valueOf(messageAttachments.get(0).getFastPreflightId()); } final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID)); diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index eda7a82506..08afcf5a3e 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -386,6 +386,10 @@ public class ConversationFragment extends Fragment }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); } + private void handleReplyMessage(final MessageRecord message) { + listener.handleReplyMessage(message); + } + private void handleSaveAttachment(final MediaMmsMessageRecord message) { SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -493,6 +497,7 @@ public class ConversationFragment extends Fragment public interface ConversationFragmentListener { void setThreadId(long threadId); + void handleReplyMessage(MessageRecord messageRecord); } private class ConversationScrollListener extends OnScrollListener { @@ -668,6 +673,10 @@ public class ConversationFragment extends Fragment handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord()); actionMode.finish(); return true; + case R.id.menu_context_reply: + handleReplyMessage(getSelectedMessageRecord()); + actionMode.finish(); + return true; } return false; diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index f0c64daace..c4bd2cdf9b 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.ExpirationTimerView; +import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; 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.database.model.Quote; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.MmsDownloadJob; import org.thoughtcrime.securesms.jobs.MmsSendJob; @@ -111,6 +113,7 @@ public class ConversationItem extends LinearLayout private GlideRequests glideRequests; protected View bodyBubble; + private QuoteView quoteView; private TextView bodyText; private TextView dateText; private TextView simInfoText; @@ -173,6 +176,7 @@ public class ConversationItem extends LinearLayout this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); this.expirationTimer = findViewById(R.id.expiration_indicator); this.groupSenderHolder = findViewById(R.id.group_sender_holder); + this.quoteView = findViewById(R.id.quote_view); setOnClickListener(new ClickListener(null)); @@ -210,6 +214,7 @@ public class ConversationItem extends LinearLayout setMinimumWidth(); setSimInfo(messageRecord); setExpiration(messageRecord); + setQuote(messageRecord); } @Override @@ -506,6 +511,17 @@ public class ConversationItem extends LinearLayout } } + private void setQuote(@NonNull MessageRecord messageRecord) { + if (messageRecord.isMms() && !messageRecord.isMmsNotification() && ((MediaMmsMessageRecord)messageRecord).getQuote() != null) { + Quote quote = ((MediaMmsMessageRecord)messageRecord).getQuote(); + assert quote != null; + quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quote.getText(), quote.getAttachment()); + quoteView.setVisibility(View.VISIBLE); + } else { + quoteView.dismiss(); + } + } + private void setFailedStatusIcons() { alertView.setFailed(); deliveryStatusIndicator.setNone(); diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java index 13cbe0d6f6..7cc795d853 100644 --- a/src/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java @@ -35,10 +35,12 @@ public abstract class Attachment { private final int width; private final int height; + private final boolean quote; + public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName, @Nullable String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, - int width, int height) + int width, int height, boolean quote) { this.contentType = contentType; this.transferState = transferState; @@ -52,6 +54,7 @@ public abstract class Attachment { this.voiceNote = voiceNote; this.width = width; this.height = height; + this.quote = quote; } @Nullable @@ -119,4 +122,8 @@ public abstract class Attachment { public int getHeight() { return height; } + + public boolean isQuote() { + return quote; + } } diff --git a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index f06347a24d..ba57be3752 100644 --- a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -17,9 +17,9 @@ public class DatabaseAttachment extends Attachment { String contentType, int transferProgress, long size, String fileName, String location, String key, String relay, byte[] digest, String fastPreflightId, boolean voiceNote, - int width, int height) + int width, int height, boolean quote) { - super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height); + super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index 0801a24d6d..0011af6c92 100644 --- a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0); + super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false); } @Nullable diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java index 9cf930535d..a01b05aa27 100644 --- a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -15,12 +15,12 @@ import java.util.List; public class PointerAttachment extends Attachment { private PointerAttachment(@NonNull String contentType, int transferState, long size, - @Nullable String fileName, @NonNull String location, - @NonNull String key, @NonNull String relay, - @Nullable byte[] digest, boolean voiceNote, + @Nullable String fileName, @NonNull String location, + @Nullable String key, @NonNull String relay, + @Nullable byte[] digest, boolean voiceNote, int width, int height) { - super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height); + super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false); } @Nullable @@ -41,23 +41,36 @@ public class PointerAttachment extends Attachment { if (pointers.isPresent()) { for (SignalServiceAttachment pointer : pointers.get()) { - if (pointer.isPointer()) { - String encodedKey = Base64.encodeBytes(pointer.asPointer().getKey()); + Optional result = forPointer(Optional.of(pointer)); - results.add(new PointerAttachment(pointer.getContentType(), - AttachmentDatabase.TRANSFER_PROGRESS_PENDING, - pointer.asPointer().getSize().or(0), - pointer.asPointer().getFileName().orNull(), - String.valueOf(pointer.asPointer().getId()), - encodedKey, pointer.asPointer().getRelay().orNull(), - pointer.asPointer().getDigest().orNull(), - pointer.asPointer().getVoiceNote(), - pointer.asPointer().getWidth(), - pointer.asPointer().getHeight())); + if (result.isPresent()) { + results.add(result.get()); } } } return results; } + + public static Optional forPointer(Optional pointer) { + if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent(); + + String encodedKey = null; + + if (pointer.get().asPointer().getKey() != null) { + encodedKey = Base64.encodeBytes(pointer.get().asPointer().getKey()); + } + + return Optional.of(new PointerAttachment(pointer.get().getContentType(), + AttachmentDatabase.TRANSFER_PROGRESS_PENDING, + pointer.get().asPointer().getSize().or(0), + pointer.get().asPointer().getFileName().orNull(), + String.valueOf(pointer.get().asPointer().getId()), + encodedKey, pointer.get().asPointer().getRelay().orNull(), + pointer.get().asPointer().getDigest().orNull(), + pointer.get().asPointer().getVoiceNote(), + pointer.get().asPointer().getWidth(), + pointer.get().asPointer().getHeight())); + + } } diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java index d1cd989c40..72c4f365ed 100644 --- a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -10,17 +10,17 @@ public class UriAttachment extends Attachment { private final @Nullable Uri thumbnailUri; public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, - @Nullable String fileName, boolean voiceNote) + @Nullable String fileName, boolean voiceNote, boolean quote) { - this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote); + this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote); } public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, @NonNull String contentType, int transferState, long size, int width, int height, @Nullable String fileName, @Nullable String fastPreflightId, - boolean voiceNote) + boolean voiceNote, boolean quote) { - super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height); + super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote); this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/src/org/thoughtcrime/securesms/components/InputPanel.java b/src/org/thoughtcrime/securesms/components/InputPanel.java index d565a6e20a..ae610dfc84 100644 --- a/src/org/thoughtcrime/securesms/components/InputPanel.java +++ b/src/org/thoughtcrime/securesms/components/InputPanel.java @@ -23,12 +23,17 @@ import android.widget.Toast; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.QuoteModel; +import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; +import org.whispersystems.libsignal.util.guava.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -43,12 +48,13 @@ public class InputPanel extends LinearLayout private static final int FADE_TIME = 150; - private EmojiToggle emojiToggle; - private ComposeText composeText; - private View quickCameraToggle; - private View quickAudioToggle; - private View buttonToggle; - private View recordingContainer; + private QuoteView quoteView; + private EmojiToggle emojiToggle; + private ComposeText composeText; + private View quickCameraToggle; + private View quickAudioToggle; + private View buttonToggle; + private View recordingContainer; private MicrophoneRecorderView microphoneRecorderView; private SlideToCancel slideToCancel; @@ -74,15 +80,18 @@ public class InputPanel extends LinearLayout public void onFinishInflate() { super.onFinishInflate(); - this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle); - this.composeText = ViewUtil.findById(this, R.id.embedded_text_editor); - this.quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); - this.quickAudioToggle = ViewUtil.findById(this, R.id.quick_audio_toggle); - this.buttonToggle = ViewUtil.findById(this, R.id.button_toggle); - this.recordingContainer = ViewUtil.findById(this, R.id.recording_container); - this.recordTime = new RecordTime((TextView) ViewUtil.findById(this, R.id.record_time)); - this.slideToCancel = new SlideToCancel(ViewUtil.findById(this, R.id.slide_to_cancel)); - this.microphoneRecorderView = ViewUtil.findById(this, R.id.recorder_view); + View quoteDismiss = findViewById(R.id.quote_dismiss); + + this.quoteView = findViewById(R.id.quote_view); + this.emojiToggle = findViewById(R.id.emoji_toggle); + this.composeText = findViewById(R.id.embedded_text_editor); + this.quickCameraToggle = findViewById(R.id.quick_camera_toggle); + this.quickAudioToggle = findViewById(R.id.quick_audio_toggle); + this.buttonToggle = findViewById(R.id.button_toggle); + this.recordingContainer = findViewById(R.id.recording_container); + this.recordTime = new RecordTime(findViewById(R.id.record_time)); + this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel)); + this.microphoneRecorderView = findViewById(R.id.recorder_view); this.microphoneRecorderView.setListener(this); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { @@ -97,6 +106,8 @@ public class InputPanel extends LinearLayout emojiToggle.setVisibility(View.VISIBLE); emojiVisible = true; } + + quoteDismiss.setOnClickListener(v -> clearQuote()); } public void setListener(final @NonNull Listener listener) { @@ -109,6 +120,23 @@ public class InputPanel extends LinearLayout composeText.setMediaListener(listener); } + public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments) { + this.quoteView.setQuote(glideRequests, id, author, body, attachments); + this.quoteView.setVisibility(View.VISIBLE); + } + + public void clearQuote() { + this.quoteView.dismiss(); + } + + public Optional getQuote() { + if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) { + return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getAddress(), quoteView.getBody(), quoteView.getAttachments())); + } else { + return Optional.absent(); + } + } + public void setEmojiDrawer(@NonNull EmojiDrawer emojiDrawer) { emojiToggle.attach(emojiDrawer); } @@ -210,6 +238,7 @@ public class InputPanel extends LinearLayout composeText.insertEmoji(emoji); } + public interface Listener { void onRecorderStarted(); void onRecorderFinished(); diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java new file mode 100644 index 0000000000..90627df6e2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -0,0 +1,203 @@ +package org.thoughtcrime.securesms.components; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +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.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.annimon.stream.Stream; +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.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.Slide; +import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; +import org.thoughtcrime.securesms.util.Util; + +import java.util.List; + +public class QuoteView extends LinearLayout implements RecipientModifiedListener { + + private static final String TAG = QuoteView.class.getSimpleName(); + + private TextView authorView; + private TextView bodyView; + private ImageView quoteBarView; + private ImageView attachmentView; + private ImageView dismissView; + + private long id; + private Recipient author; + private String body; + private View mediaDescription; + private ImageView mediaDescriptionIcon; + private TextView mediaDescriptionText; + private SlideDeck attachments; + + public QuoteView(Context context) { + super(context); + initialize(null); + } + + public QuoteView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(attrs); + } + + public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(attrs); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initialize(attrs); + } + + 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); + + if (attrs != null) { + TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0); + boolean dismissable = typedArray.getBoolean(R.styleable.QuoteView_quote_dismissable, true); + typedArray.recycle(); + + if (!dismissable) dismissView.setVisibility(View.GONE); + else dismissView.setVisibility(View.VISIBLE); + } + + dismissView.setOnClickListener(view -> setVisibility(View.GONE)); + + setBackgroundDrawable(getContext().getResources().getDrawable(R.drawable.quote_background)); + } + + public void setQuote(GlideRequests glideRequests, long id, @NonNull Recipient author, @Nullable String body, @NonNull SlideDeck attachments) { + if (this.author != null) this.author.removeListener(this); + + this.id = id; + this.author = author; + this.body = body; + this.attachments = attachments; + + author.addListener(this); + setQuoteAuthor(author); + setQuoteText(body, attachments); + setQuoteAttachment(glideRequests, attachments); + } + + public void dismiss() { + if (this.author != null) this.author.removeListener(this); + + this.id = 0; + this.author = null; + this.body = null; + + setVisibility(View.GONE); + } + + @Override + public void onModified(Recipient recipient) { + Util.runOnMain(() -> { + if (recipient == author) { + setQuoteAuthor(recipient); + } + }); + } + + 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); + } + + 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); + + bodyView.setText(body == null ? "" : body); + } + } + + private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { + List imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo()).limit(1).toList(); + + if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) { + attachmentView.setVisibility(View.VISIBLE); + dismissView.setBackgroundResource(R.drawable.circle_alpha); + glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri())) + .centerCrop() + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(attachmentView); + } else { + attachmentView.setVisibility(View.GONE); + dismissView.setBackgroundDrawable(null); + } + } + + public long getQuoteId() { + return id; + } + + public Recipient getAuthor() { + return author; + } + + public String getBody() { + return body; + } + + public List getAttachments() { + return attachments.asAttachments(); + } +} diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 04f4cbab00..98b2ed9fd5 100644 --- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -33,6 +33,8 @@ import android.util.Pair; import net.sqlcipher.database.SQLiteDatabase; +import org.json.JSONArray; +import org.json.JSONException; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; @@ -44,6 +46,7 @@ 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.JsonUtils; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.StorageUtil; @@ -55,6 +58,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; @@ -67,7 +71,7 @@ public class AttachmentDatabase extends Database { public static final String TABLE_NAME = "part"; public static final String ROW_ID = "_id"; - public static final String ATTACHMENT_ID_ALIAS = "attachment_id"; + static final String ATTACHMENT_JSON_ALIAS = "attachment_json"; static final String MMS_ID = "mid"; static final String CONTENT_TYPE = "ct"; static final String NAME = "name"; @@ -82,7 +86,8 @@ public class AttachmentDatabase extends Database { public static final String UNIQUE_ID = "unique_id"; static final String DIGEST = "digest"; static final String VOICE_NOTE = "voice_note"; - public static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; + static final String QUOTE = "quote"; + static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; public static final String DATA_RANDOM = "data_random"; private static final String THUMBNAIL_RANDOM = "thumbnail_random"; static final String WIDTH = "width"; @@ -97,12 +102,12 @@ public class AttachmentDatabase extends Database { private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; - private static final String[] PROJECTION = new String[] {ROW_ID + " AS " + ATTACHMENT_ID_ALIAS, + private static final String[] PROJECTION = new String[] {ROW_ID, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, - DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT}; + QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT}; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + @@ -114,7 +119,7 @@ public class AttachmentDatabase extends Database { FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " + VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " + - WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);"; + QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -181,9 +186,15 @@ public class AttachmentDatabase extends Database { try { cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); - if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor); - else return null; + if (cursor != null && cursor.moveToFirst()) { + List list = getAttachment(cursor); + if (list != null && list.size() > 0) { + return list.get(0); + } + } + + return null; } finally { if (cursor != null) cursor.close(); @@ -200,7 +211,7 @@ public class AttachmentDatabase extends Database { null, null, null); while (cursor != null && cursor.moveToNext()) { - results.add(getAttachment(cursor)); + results.addAll(getAttachment(cursor)); } return results; @@ -218,7 +229,7 @@ public class AttachmentDatabase extends Database { try { cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); while (cursor != null && cursor.moveToNext()) { - attachments.add(getAttachment(cursor)); + attachments.addAll(getAttachment(cursor)); } } finally { if (cursor != null) cursor.close(); @@ -327,15 +338,19 @@ public class AttachmentDatabase extends Database { thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); } - void insertAttachmentsForMessage(long mmsId, @NonNull List attachments) + void insertAttachmentsForMessage(long mmsId, @NonNull List attachments, @NonNull List quoteAttachment) throws MmsException { Log.w(TAG, "insertParts(" + attachments.size() + ")"); for (Attachment attachment : attachments) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment); + AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote()); Log.w(TAG, "Inserted attachment at ID: " + attachmentId); } + + for (Attachment attachment : quoteAttachment) { + insertAttachment(mmsId, attachment, true); + } } public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment, @@ -376,7 +391,8 @@ public class AttachmentDatabase extends Database { databaseAttachment.getFastPreflightId(), databaseAttachment.isVoiceNote(), mediaStream.getWidth(), - mediaStream.getHeight()); + mediaStream.getHeight(), + databaseAttachment.isQuote()); } @@ -519,28 +535,68 @@ public class AttachmentDatabase extends Database { } } - DatabaseAttachment getAttachment(@NonNull Cursor cursor) { - return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)), - cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), - cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), - !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), - !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), - cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), - cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), - StorageUtil.getCleanFileName(cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME))), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), - cursor.getString(cursor.getColumnIndexOrThrow(NAME)), - cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), - cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), - cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT))); + public List getAttachment(@NonNull Cursor cursor) { + try { + if (cursor.getColumnIndex(AttachmentDatabase.ATTACHMENT_JSON_ALIAS) != -1) { + if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { + return new LinkedList<>(); + } + + List result = new LinkedList<>(); + JSONArray array = new JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))); + + for (int i=0;i attachments = attachmentDatabase.getAttachment(cursor); + String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); + boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX))); + Address address = null; if (serializedAddress != null) { address = Address.fromSerialized(serializedAddress); @@ -107,7 +110,7 @@ public class MediaDatabase extends Database { date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)); } - return new MediaRecord(attachment, address, date, outgoing); + return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, address, date, outgoing); } public DatabaseAttachment getAttachment() { diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 8f915878e4..b1a855ac64 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; @@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; 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.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; @@ -86,6 +88,11 @@ public class MmsDatabase extends MessagingDatabase { static final String PART_COUNT = "part_count"; static final String NETWORK_FAILURE = "network_failures"; + static final String QUOTE_ID = "quote_id"; + static final String QUOTE_AUTHOR = "quote_author"; + static final String QUOTE_BODY = "quote_body"; + static final String QUOTE_ATTACHMENT = "quote_attachment"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " + @@ -102,7 +109,8 @@ public class MmsDatabase extends MessagingDatabase { NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " + EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " INTEGER DEFAULT 0, " + - READ_RECEIPT_COUNT + " INTEGER DEFAULT 0);"; + READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + QUOTE_ID + " INTEGER DEFAULT 0, " + + QUOTE_AUTHOR + " TEXT, " + QUOTE_BODY + " TEXT, " + QUOTE_ATTACHMENT + " INTEGER DEFAULT -1);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", @@ -122,24 +130,26 @@ public class MmsDatabase extends MessagingDatabase { MESSAGE_SIZE, STATUS, TRANSACTION_ID, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID, - EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, - AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS, - AttachmentDatabase.UNIQUE_ID, - AttachmentDatabase.MMS_ID, - AttachmentDatabase.SIZE, - AttachmentDatabase.FILE_NAME, - AttachmentDatabase.DATA, - AttachmentDatabase.THUMBNAIL, - AttachmentDatabase.CONTENT_TYPE, - AttachmentDatabase.CONTENT_LOCATION, - AttachmentDatabase.DIGEST, - AttachmentDatabase.FAST_PREFLIGHT_ID, - AttachmentDatabase.VOICE_NOTE, - AttachmentDatabase.WIDTH, - AttachmentDatabase.HEIGHT, - AttachmentDatabase.CONTENT_DISPOSITION, - AttachmentDatabase.NAME, - AttachmentDatabase.TRANSFER_STATE + EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, + "json_group_array(json_object(" + + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," + + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," + + "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," + + "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," + + "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, }; private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?"; @@ -276,7 +286,7 @@ public class MmsDatabase extends MessagingDatabase { return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") + " FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + - " WHERE " + where, arguments); + " WHERE " + where + " GROUP BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, arguments); } public Cursor getMessage(long messageId) { @@ -537,25 +547,37 @@ public class MmsDatabase extends MessagingDatabase { cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); if (cursor != null && cursor.moveToNext()) { + List associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId); + long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)); long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)); - List attachments = new LinkedList<>(attachmentDatabase.getAttachmentsForMessage(messageId)); String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId); + List attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote).map(a -> (Attachment)a).toList(); - Recipient recipient = Recipient.from(context, Address.fromSerialized(address), false); + long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)); + String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)); + String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)); + List quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList(); + + Recipient recipient = Recipient.from(context, Address.fromSerialized(address), false); + QuoteModel quote = null; + + if (quoteId > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) { + quote = new QuoteModel(quoteId, Address.fromSerialized(quoteAuthor), quoteText, quoteAttachments); + } if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { - return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0); + return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote); } else if (Types.isExpirationTimerUpdate(outboxType)) { return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn); } - OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType); + OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote); if (Types.isSecureType(outboxType)) { return new OutgoingSecureMediaMessage(message); @@ -604,11 +626,13 @@ public class MmsDatabase extends MessagingDatabase { databaseAttachment.getFastPreflightId(), databaseAttachment.isVoiceNote(), databaseAttachment.getWidth(), - databaseAttachment.getHeight())); + databaseAttachment.getHeight(), + databaseAttachment.isQuote())); } return insertMediaMessage(request.getBody(), attachments, + new LinkedList<>(), contentValues, null); } catch (NoSuchMessageException e) { @@ -651,12 +675,22 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); } + List quoteAttachments = new LinkedList<>(); + + if (retrieved.getQuote() != null) { + contentValues.put(QUOTE_ID, retrieved.getQuote().getId()); + contentValues.put(QUOTE_BODY, retrieved.getQuote().getText()); + contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize()); + + quoteAttachments = retrieved.getQuote().getAttachments(); + } + if (retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) { Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")"); return Optional.absent(); } - long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), contentValues, null); + long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, contentValues, null); if (!Types.isExpirationTimerUpdate(mailbox)) { DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1); @@ -784,7 +818,17 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum()); - long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), contentValues, insertListener); + List quoteAttachments = new LinkedList<>(); + + if (message.getOutgoingQuote() != null) { + contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId()); + contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize()); + contentValues.put(QUOTE_BODY, message.getOutgoingQuote().getText()); + + quoteAttachments.addAll(message.getOutgoingQuote().getAttachments()); + } + + long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, contentValues, insertListener); if (message.getRecipient().getAddress().isGroup()) { List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false); @@ -806,6 +850,7 @@ public class MmsDatabase extends MessagingDatabase { private long insertMediaMessage(@Nullable String body, @NonNull List attachments, + @NonNull List quoteAttachments, @NonNull ContentValues contentValues, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException @@ -820,7 +865,7 @@ public class MmsDatabase extends MessagingDatabase { try { long messageId = db.insert(TABLE_NAME, null, contentValues); - partsDatabase.insertAttachmentsForMessage(messageId, attachments); + partsDatabase.insertAttachmentsForMessage(messageId, attachments, quoteAttachments); db.setTransactionSuccessful(); return messageId; @@ -870,7 +915,6 @@ public class MmsDatabase extends MessagingDatabase { } } - /*package*/ void deleteThreads(Set threadIds) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); String where = ""; @@ -1021,7 +1065,13 @@ public class MmsDatabase extends MessagingDatabase { new LinkedList(), message.getSubscriptionId(), message.getExpiresIn(), - System.currentTimeMillis(), 0); + System.currentTimeMillis(), 0, + message.getOutgoingQuote() != null ? + new Quote(message.getOutgoingQuote().getId(), + message.getOutgoingQuote().getAuthor(), + message.getOutgoingQuote().getText(), + new SlideDeck(context, message.getOutgoingQuote().getAttachments())) : + null); } } @@ -1118,12 +1168,13 @@ public class MmsDatabase extends MessagingDatabase { List mismatches = getMismatchedIdentities(mismatchDocument); List networkFailures = getFailures(networkDocument); SlideDeck slideDeck = getSlideDeck(cursor); + Quote quote = getQuote(cursor); return new MediaMmsMessageRecord(context, id, recipient, recipient, addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, body, slideDeck, partCount, box, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, - readReceiptCount); + readReceiptCount, quote); } private Recipient getRecipientFor(String serialized) { @@ -1163,8 +1214,24 @@ public class MmsDatabase extends MessagingDatabase { } private SlideDeck getSlideDeck(@NonNull Cursor cursor) { - Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor); - return new SlideDeck(context, attachment); + List attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor); + List messageAttachmnets = Stream.of(attachment).filterNot(Attachment::isQuote).toList(); + return new SlideDeck(context, messageAttachmnets); + } + + private @Nullable Quote getQuote(@NonNull Cursor cursor) { + long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_ID)); + String quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_AUTHOR)); + String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY)); + List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor); + List quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList(); + SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments); + + if (quoteId > 0 && !TextUtils.isEmpty(quoteAuthor)) { + return new Quote(quoteId, Address.fromExternal(context, quoteAuthor), quoteText, quoteDeck); + } else { + return null; + } } public void close() { diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index c1e5c4ae9a..6ae773dfa0 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -60,28 +60,20 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.EXPIRE_STARTED, MmsSmsColumns.NOTIFIED, TRANSPORT, - AttachmentDatabase.ATTACHMENT_ID_ALIAS, - AttachmentDatabase.UNIQUE_ID, - AttachmentDatabase.MMS_ID, - AttachmentDatabase.SIZE, - AttachmentDatabase.FILE_NAME, - AttachmentDatabase.DATA, - AttachmentDatabase.THUMBNAIL, - AttachmentDatabase.CONTENT_TYPE, - AttachmentDatabase.CONTENT_LOCATION, - AttachmentDatabase.DIGEST, - AttachmentDatabase.FAST_PREFLIGHT_ID, - AttachmentDatabase.VOICE_NOTE, - AttachmentDatabase.WIDTH, - AttachmentDatabase.HEIGHT, - AttachmentDatabase.CONTENT_DISPOSITION, - AttachmentDatabase.NAME, - AttachmentDatabase.TRANSFER_STATE}; + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + MmsDatabase.QUOTE_ID, + MmsDatabase.QUOTE_AUTHOR, + MmsDatabase.QUOTE_BODY, + MmsDatabase.QUOTE_ATTACHMENT}; public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } + public Cursor getMessagesFor(long timestamp) { + return queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null); + } + public Cursor getConversation(long threadId, long limit) { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; @@ -155,7 +147,25 @@ public class MmsSmsDatabase extends Database { "'MMS::' || " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " || '::' || " + MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, - AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS, + "json_group_array(json_object(" + + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + "," + + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + + "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + + "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + + "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, @@ -166,22 +176,10 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED, MmsSmsColumns.NOTIFIED, MmsDatabase.NETWORK_FAILURE, TRANSPORT, - AttachmentDatabase.UNIQUE_ID, - AttachmentDatabase.MMS_ID, - AttachmentDatabase.SIZE, - AttachmentDatabase.FILE_NAME, - AttachmentDatabase.DATA, - AttachmentDatabase.THUMBNAIL, - AttachmentDatabase.CONTENT_TYPE, - AttachmentDatabase.CONTENT_LOCATION, - AttachmentDatabase.DIGEST, - AttachmentDatabase.FAST_PREFLIGHT_ID, - AttachmentDatabase.VOICE_NOTE, - AttachmentDatabase.WIDTH, - AttachmentDatabase.HEIGHT, - AttachmentDatabase.CONTENT_DISPOSITION, - AttachmentDatabase.NAME, - AttachmentDatabase.TRANSFER_STATE}; + MmsDatabase.QUOTE_ID, + MmsDatabase.QUOTE_AUTHOR, + MmsDatabase.QUOTE_BODY, + MmsDatabase.QUOTE_ATTACHMENT}; String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, @@ -189,7 +187,7 @@ public class MmsSmsDatabase extends Database { "'SMS::' || " + MmsSmsColumns.ID + " || '::' || " + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, - "NULL AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS, + "NULL AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, @@ -200,22 +198,10 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED, MmsSmsColumns.NOTIFIED, MmsDatabase.NETWORK_FAILURE, TRANSPORT, - AttachmentDatabase.UNIQUE_ID, - AttachmentDatabase.MMS_ID, - AttachmentDatabase.SIZE, - AttachmentDatabase.FILE_NAME, - AttachmentDatabase.DATA, - AttachmentDatabase.THUMBNAIL, - AttachmentDatabase.CONTENT_TYPE, - AttachmentDatabase.CONTENT_LOCATION, - AttachmentDatabase.DIGEST, - AttachmentDatabase.FAST_PREFLIGHT_ID, - AttachmentDatabase.VOICE_NOTE, - AttachmentDatabase.WIDTH, - AttachmentDatabase.HEIGHT, - AttachmentDatabase.CONTENT_DISPOSITION, - AttachmentDatabase.NAME, - AttachmentDatabase.TRANSFER_STATE}; + MmsDatabase.QUOTE_ID, + MmsDatabase.QUOTE_AUTHOR, + MmsDatabase.QUOTE_BODY, + MmsDatabase.QUOTE_ATTACHMENT}; SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); @@ -226,11 +212,7 @@ public class MmsSmsDatabase extends Database { smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME); mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + - " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " = " + - " (SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + - " FROM " + AttachmentDatabase.TABLE_NAME + " WHERE " + - AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + - MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " LIMIT 1)"); + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID); Set mmsColumnsPresent = new HashSet<>(); @@ -273,9 +255,15 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(AttachmentDatabase.VOICE_NOTE); mmsColumnsPresent.add(AttachmentDatabase.WIDTH); mmsColumnsPresent.add(AttachmentDatabase.HEIGHT); + mmsColumnsPresent.add(AttachmentDatabase.QUOTE); mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION); mmsColumnsPresent.add(AttachmentDatabase.NAME); mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE); + mmsColumnsPresent.add(AttachmentDatabase.ATTACHMENT_JSON_ALIAS); + mmsColumnsPresent.add(MmsDatabase.QUOTE_ID); + mmsColumnsPresent.add(MmsDatabase.QUOTE_AUTHOR); + mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY); + mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT); Set smsColumnsPresent = new HashSet<>(); smsColumnsPresent.add(MmsSmsColumns.ID); @@ -298,7 +286,7 @@ public class MmsSmsDatabase extends Database { smsColumnsPresent.add(SmsDatabase.STATUS); @SuppressWarnings("deprecation") - String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, null, null); + String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null); @SuppressWarnings("deprecation") String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null); diff --git a/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java b/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java index 8846bbd1ff..49d8f1028e 100644 --- a/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java +++ b/src/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java @@ -38,7 +38,7 @@ public class PagingMediaLoader extends AsyncLoader> { Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId); while (cursor != null && cursor.moveToNext()) { - AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ATTACHMENT_ID_ALIAS)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))); + AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))); Uri attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId); if (attachmentUri.equals(uri)) { diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index b1128c91fd..ec7390b692 100644 --- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.SpannableString; import org.thoughtcrime.securesms.R; @@ -52,11 +53,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { int partCount, long mailbox, List mismatches, List failures, int subscriptionId, - long expiresIn, long expireStarted, int readReceiptCount) + long expiresIn, long expireStarted, int readReceiptCount, + @Nullable Quote quote) { super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, - subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount); + subscriptionId, expiresIn, expireStarted, slideDeck, readReceiptCount, quote); this.context = context.getApplicationContext(); this.partCount = partCount; diff --git a/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 3b8e3032f4..09516ed257 100644 --- a/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; @@ -14,17 +15,21 @@ import java.util.List; public abstract class MmsMessageRecord extends MessageRecord { - private final @NonNull SlideDeck slideDeck; + private final @NonNull SlideDeck slideDeck; + private final @Nullable Quote quote; MmsMessageRecord(Context context, long id, String body, Recipient conversationRecipient, Recipient individualRecipient, int recipientDeviceId, long dateSent, long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, long type, List mismatches, List networkFailures, int subscriptionId, long expiresIn, - long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount) + long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, + @Nullable Quote quote) { super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount); + this.slideDeck = slideDeck; + this.quote = quote; } @Override @@ -52,5 +57,7 @@ public abstract class MmsMessageRecord extends MessageRecord { return slideDeck.containsMediaSlide(); } - + public @Nullable Quote getQuote() { + return quote; + } } diff --git a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index e678811fb9..f858129a1b 100644 --- a/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -55,7 +55,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, new LinkedList(), new LinkedList(), subscriptionId, - 0, 0, slideDeck, readReceiptCount); + 0, 0, slideDeck, readReceiptCount, null); this.contentLocation = contentLocation; this.messageSize = messageSize; diff --git a/src/org/thoughtcrime/securesms/database/model/Quote.java b/src/org/thoughtcrime/securesms/database/model/Quote.java new file mode 100644 index 0000000000..8a05765e47 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/model/Quote.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.database.model; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.mms.SlideDeck; + +public class Quote { + + private final long id; + private final Address author; + private final String text; + private final SlideDeck attachment; + + public Quote(long id, @NonNull Address author, @Nullable String text, @NonNull SlideDeck attachment) { + this.id = id; + this.author = author; + this.text = text; + this.attachment = attachment; + } + + public long getId() { + return id; + } + + public @NonNull Address getAuthor() { + return author; + } + + public @Nullable String getText() { + return text; + } + + public @NonNull SlideDeck getAttachment() { + return attachment; + } +} diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 8c4f88b3c3..7d8bacff53 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -111,10 +111,10 @@ public class GroupManager { if (avatar != null) { Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar); - avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false); + avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false); } - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); return new GroupActionResult(groupRecipient, threadId); diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 79176961c3..f00d24fdeb 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -212,7 +212,7 @@ public class GroupMessageProcessor { MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false)); Recipient recipient = Recipient.from(context, addres, false); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0, null); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 4b6d20431e..47285df361 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -220,7 +220,7 @@ public class MmsDownloadJob extends MasterSecretJob { attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), AttachmentDatabase.TRANSFER_PROGRESS_DONE, - part.getData().length, name, false)); + part.getData().length, name, false, false)); } } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index cd850ae38c..81edfe66ed 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -1,9 +1,9 @@ package org.thoughtcrime.securesms.jobs; -import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.os.Build; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; @@ -14,6 +14,7 @@ import android.util.Pair; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; @@ -27,13 +28,17 @@ import org.thoughtcrime.securesms.database.MessagingDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +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; @@ -53,6 +58,7 @@ import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.libsignal.DuplicateMessageException; import org.whispersystems.libsignal.IdentityKey; @@ -87,6 +93,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.security.MessageDigest; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -168,11 +175,11 @@ public class PushDecryptJob extends ContextJob { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId); - else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId); - else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId); - else if (message.getAttachments().isPresent()) handleMediaMessage(envelope, message, smsMessageId); - else if (message.getBody().isPresent()) handleTextMessage(envelope, message, smsMessageId); + if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId); + else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId); + else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId); + else if (message.getAttachments().isPresent() || message.getQuote().isPresent()) handleMediaMessage(envelope, message, smsMessageId); + else if (message.getBody().isPresent()) handleTextMessage(envelope, message, smsMessageId); if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) { handleUnknownGroupMessage(envelope, message.getGroupInfo().get()); @@ -398,7 +405,7 @@ public class PushDecryptJob extends ContextJob { message.getExpiresInSeconds() * 1000L, true, Optional.fromNullable(envelope.getRelay()), Optional.absent(), message.getGroupInfo(), - Optional.absent()); + Optional.absent(), Optional.absent()); @@ -429,7 +436,7 @@ public class PushDecryptJob extends ContextJob { threadId = GroupMessageProcessor.process(context, envelope, message.getMessage(), true); } else if (message.getMessage().isExpirationUpdate()) { threadId = handleSynchronizeSentExpirationUpdate(message); - } else if (message.getMessage().getAttachments().isPresent()) { + } else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent()) { threadId = handleSynchronizeSentMediaMessage(message); } else { threadId = handleSynchronizeSentTextMessage(message); @@ -517,13 +524,15 @@ public class PushDecryptJob extends ContextJob { { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Recipient recipient = getMessageDestination(envelope, message); + Optional quote = getValidatedQuote(message.getQuote()); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, envelope.getSource()), message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, Optional.fromNullable(envelope.getRelay()), message.getBody(), message.getGroupInfo(), - message.getAttachments()); + message.getAttachments(), + quote); if (message.getExpiresInSeconds() != recipient.getExpireMessages()) { handleExpirationUpdate(envelope, message, Optional.absent()); @@ -573,11 +582,12 @@ public class PushDecryptJob extends ContextJob { { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Recipient recipients = getSyncMessageDestination(message); + Optional quote = getValidatedQuote(message.getMessage().getQuote()); OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(), PointerAttachment.forPointers(message.getMessage().getAttachments()), message.getTimestamp(), -1, - message.getMessage().getExpiresInSeconds() * 1000L, - ThreadDatabase.DistributionTypes.DEFAULT); + message.getMessage().getExpiresInSeconds() * 1000, + ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull()); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); @@ -664,7 +674,7 @@ public class PushDecryptJob extends ContextJob { long messageId; if (recipient.getAddress().isGroup()) { - OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT); + OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null); outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage); messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null); @@ -842,6 +852,51 @@ public class PushDecryptJob extends ContextJob { } } + private Optional getValidatedQuote(Optional quote) { + if (!quote.isPresent()) return Optional.absent(); + + if (quote.get().getId() <= 0) { + Log.w(TAG, "Received quote without an ID! Ignoring..."); + return Optional.absent(); + } + + if (quote.get().getAuthor() == null) { + Log.w(TAG, "Received quote without an author! Ignoring..."); + return Optional.absent(); + } + + MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); + Address author = Address.fromExternal(context, quote.get().getAuthor().getNumber()); + + try (Cursor cursor = db.getMessagesFor(quote.get().getId())) { + MmsSmsDatabase.Reader reader = db.readerFor(cursor); + + MessageRecord messageRecord; + + while ((messageRecord = reader.getNext()) != null) { + if ((Util.isOwnNumber(context, author) && messageRecord.isOutgoing()) || + (!Util.isOwnNumber(context, author) && messageRecord.getIndividualRecipient().getAddress().equals(author))) + { + Log.w(TAG, "Found matching message record..."); + List attachments = new LinkedList<>(); + + if (messageRecord.isMms()) { + attachments = ((MmsMessageRecord)messageRecord).getSlideDeck().asAttachments(); + } + + return Optional.of(new QuoteModel(quote.get().getId(), author, messageRecord.getBody(), attachments)); + } + } + } + + Log.w(TAG, "Didn't find matching message record..."); + + return Optional.of(new QuoteModel(quote.get().getId(), + author, + quote.get().getText(), + PointerAttachment.forPointers(Optional.of(quote.get().getAttachments())))); + } + private Optional insertPlaceholder(@NonNull SignalServiceEnvelope envelope) { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, envelope.getSource()), @@ -861,6 +916,7 @@ public class PushDecryptJob extends ContextJob { } } + private Recipient getMessageDestination(SignalServiceEnvelope envelope, SignalServiceDataMessage message) { if (message.getGroupInfo().isPresent()) { return Recipient.from(context, Address.fromExternal(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 908ca6a1d3..7bff074b97 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; @@ -143,6 +144,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); List scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments()); List attachmentStreams = getAttachmentsFor(scaledAttachments); + Optional quote = getQuoteFor(message); List addresses; @@ -171,6 +173,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { .withExpiration((int)(message.getExpiresIn() / 1000)) .asExpirationUpdate(message.isExpirationUpdate()) .withProfileKey(profileKey.orNull()) + .withQuote(quote.orNull()) .build(); messageSender.sendMessage(addresses, groupMessage); diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 1e40186a2c..a05ffc3d46 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -17,14 +18,19 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; +import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; @@ -105,19 +111,21 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { } try { - SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress()); - MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); - List scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments()); - List attachmentStreams = getAttachmentsFor(scaledAttachments); - Optional profileKey = getProfileKey(message.getRecipient()); - SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() - .withBody(message.getBody()) - .withAttachments(attachmentStreams) - .withTimestamp(message.getSentTimeMillis()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) - .asExpirationUpdate(message.isExpirationUpdate()) - .build(); + SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress()); + MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); + List scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments()); + List attachmentStreams = getAttachmentsFor(scaledAttachments); + Optional profileKey = getProfileKey(message.getRecipient()); + Optional quote = getQuoteFor(message); + SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() + .withBody(message.getBody()) + .withAttachments(attachmentStreams) + .withTimestamp(message.getSentTimeMillis()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .withQuote(quote.orNull()) + .asExpirationUpdate(message.isExpirationUpdate()) + .build(); messageSender.sendMessage(address, mediaMessage); } catch (UnregisteredUserException e) { @@ -131,4 +139,5 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { throw new RetryLaterException(e); } } + } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index 806eadfe96..81a8a86cf4 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -14,17 +14,24 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.jobqueue.JobParameters; 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.SignalServiceAttachment.ProgressListener; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.LinkedList; @@ -110,5 +117,45 @@ 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<>(); + + for (Attachment attachment : message.getOutgoingQuote().getAttachments()) { + BitmapUtil.ScaleResult attachmentData = null; + + try { + if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) { + attachmentData = 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); + } + + 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())); + } + + } catch (BitmapDecodingException e) { + Log.w(TAG, e); + } + } + + return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments)); + } + + protected abstract void onPushSend() throws Exception; } diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index 5010e63624..b3b5b56150 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -34,11 +34,11 @@ import org.thoughtcrime.securesms.util.ResUtil; public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote, false)); } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote)); + super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java index 72d19a1a5e..a6fc529c9e 100644 --- a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java +++ b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java @@ -19,7 +19,7 @@ public class DocumentSlide extends Slide { @NonNull String contentType, long size, @Nullable String fileName) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/GifSlide.java b/src/org/thoughtcrime/securesms/mms/GifSlide.java index 2bdc688a31..bb8161a5d0 100644 --- a/src/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/src/org/thoughtcrime/securesms/mms/GifSlide.java @@ -14,7 +14,7 @@ public class GifSlide extends ImageSlide { } public GifSlide(Context context, Uri uri, long size, int width, int height) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index be15b339d7..ffe1fdbd77 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -37,7 +37,7 @@ public class ImageSlide extends Slide { } public ImageSlide(Context context, Uri uri, long size, int width, int height) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index c8809df183..61f4e71577 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -13,14 +13,15 @@ import java.util.List; public class IncomingMediaMessage { - private final Address from; - private final Address groupId; - private final String body; - private final boolean push; - private final long sentTimeMillis; - private final int subscriptionId; - private final long expiresIn; - private final boolean expirationUpdate; + private final Address from; + private final Address groupId; + private final String body; + private final boolean push; + private final long sentTimeMillis; + private final int subscriptionId; + private final long expiresIn; + private final boolean expirationUpdate; + private final QuoteModel quote; private final List attachments = new LinkedList<>(); @@ -41,6 +42,7 @@ public class IncomingMediaMessage { this.subscriptionId = subscriptionId; this.expiresIn = expiresIn; this.expirationUpdate = expirationUpdate; + this.quote = null; this.attachments.addAll(attachments); } @@ -53,7 +55,8 @@ public class IncomingMediaMessage { Optional relay, Optional body, Optional group, - Optional> attachments) + Optional> attachments, + Optional quote) { this.push = true; this.from = from; @@ -62,6 +65,7 @@ public class IncomingMediaMessage { this.subscriptionId = subscriptionId; this.expiresIn = expiresIn; this.expirationUpdate = expirationUpdate; + this.quote = quote.orNull(); if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false)); else this.groupId = null; @@ -108,4 +112,8 @@ public class IncomingMediaMessage { public boolean isGroupMessage() { return groupId != null; } + + public QuoteModel getQuote() { + return quote; + } } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java index a104793424..533a7029cd 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java @@ -10,7 +10,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) { super(recipient, "", new LinkedList(), sentTimeMillis, - ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn); + ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index 6cf0e2572c..02b394a816 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -21,11 +21,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @NonNull String encodedGroupContext, @NonNull List avatar, long sentTimeMillis, - long expiresIn) + long expiresIn, + @Nullable QuoteModel quote) throws IOException { super(recipient, encodedGroupContext, avatar, sentTimeMillis, - ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn); + ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote); this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); } @@ -34,12 +35,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @NonNull GroupContext group, @Nullable final Attachment avatar, long sentTimeMillis, - long expireIn) + long expireIn, + @Nullable QuoteModel quote) { super(recipient, Base64.encodeBytes(group.toByteArray()), new LinkedList() {{if (avatar != null) add(avatar);}}, System.currentTimeMillis(), - ThreadDatabase.DistributionTypes.CONVERSATION, expireIn); + ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote); this.group = group; } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java index 865bf866d6..e31bd428b0 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.mms; +import android.support.annotation.Nullable; import android.text.TextUtils; import org.thoughtcrime.securesms.attachments.Attachment; @@ -16,11 +17,12 @@ public class OutgoingMediaMessage { private final int distributionType; private final int subscriptionId; private final long expiresIn; + private final QuoteModel outgoingQuote; public OutgoingMediaMessage(Recipient recipient, String message, List attachments, long sentTimeMillis, int subscriptionId, long expiresIn, - int distributionType) + int distributionType, @Nullable QuoteModel outgoingQuote) { this.recipient = recipient; this.body = message; @@ -29,15 +31,16 @@ public class OutgoingMediaMessage { this.attachments = attachments; this.subscriptionId = subscriptionId; this.expiresIn = expiresIn; + this.outgoingQuote = outgoingQuote; } - public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType) + public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, long sentTimeMillis, int subscriptionId, long expiresIn, int distributionType, @Nullable QuoteModel outgoingQuote) { this(recipient, buildMessage(slideDeck, message), slideDeck.asAttachments(), sentTimeMillis, subscriptionId, - expiresIn, distributionType); + expiresIn, distributionType, outgoingQuote); } public OutgoingMediaMessage(OutgoingMediaMessage that) { @@ -48,6 +51,7 @@ public class OutgoingMediaMessage { this.sentTimeMillis = that.sentTimeMillis; this.subscriptionId = that.subscriptionId; this.expiresIn = that.expiresIn; + this.outgoingQuote = that.outgoingQuote; } public Recipient getRecipient() { @@ -90,6 +94,10 @@ public class OutgoingMediaMessage { return expiresIn; } + public @Nullable QuoteModel getOutgoingQuote() { + return outgoingQuote; + } + private static String buildMessage(SlideDeck slideDeck, String message) { if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) { return slideDeck.getBody() + "\n\n" + message; @@ -99,4 +107,5 @@ public class OutgoingMediaMessage { return slideDeck.getBody(); } } + } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java index 2956b1e467..097293c2f1 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.mms; +import android.support.annotation.Nullable; + import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.recipients.Recipient; @@ -11,9 +13,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage { List attachments, long sentTimeMillis, int distributionType, - long expiresIn) + long expiresIn, + @Nullable QuoteModel quote) { - super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType); + super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote); } public OutgoingSecureMediaMessage(OutgoingMediaMessage base) { diff --git a/src/org/thoughtcrime/securesms/mms/QuoteModel.java b/src/org/thoughtcrime/securesms/mms/QuoteModel.java new file mode 100644 index 0000000000..1ed405d6e1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/QuoteModel.java @@ -0,0 +1,40 @@ +package org.thoughtcrime.securesms.mms; + + +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.database.Address; + +import java.util.List; + +public class QuoteModel { + + private final long id; + private final Address author; + private final String text; + private final List attachments; + + public QuoteModel(long id, Address author, String text, @Nullable List attachments) { + this.id = id; + this.author = author; + this.text = text; + this.attachments = attachments; + } + + public long getId() { + return id; + } + + public Address getAuthor() { + return author; + } + + public String getText() { + return text; + } + + public List getAttachments() { + return attachments; + } +} diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 7e49b6484c..c3652fa7c7 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -137,7 +137,8 @@ public abstract class Slide { int height, boolean hasThumbnail, @Nullable String fileName, - boolean voiceNote) + boolean voiceNote, + boolean quote) { try { String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); @@ -151,7 +152,8 @@ public abstract class Slide { height, fileName, fastPreflightId, - voiceNote); + voiceNote, + quote); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java index b3328d48e1..46b6ac30dd 100644 --- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -31,14 +31,14 @@ public class SlideDeck { private final List slides = new LinkedList<>(); - public SlideDeck(Context context, List attachments) { + public SlideDeck(@NonNull Context context, @NonNull List attachments) { for (Attachment attachment : attachments) { Slide slide = MediaUtil.getSlideForAttachment(context, attachment); if (slide != null) slides.add(slide); } } - public SlideDeck(Context context, Attachment attachment) { + public SlideDeck(@NonNull Context context, @NonNull Attachment attachment) { Slide slide = MediaUtil.getSlideForAttachment(context, attachment); if (slide != null) slides.add(slide); } diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java index 278934f8c6..84750dd011 100644 --- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.ResUtil; public class VideoSlide extends Slide { public VideoSlide(Context context, Uri uri, long dataSize) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false, false)); } public VideoSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index bd8f5eea5c..7884b443fa 100644 --- a/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -75,7 +75,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { if (recipient.isGroupRecipient()) { Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); - OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0); + OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null); replyThreadId = MessageSender.send(context, reply, threadId, false, null); } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); diff --git a/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 674b08016a..1ac7cc6b04 100644 --- a/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -68,7 +68,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { long expiresIn = recipient.getExpireMessages() * 1000L; if (recipient.isGroupRecipient()) { - OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0); + OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null); threadId = MessageSender.send(context, reply, -1, false, null); } else { OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId); diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index 90ccb50479..7192e5a30c 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -44,9 +44,19 @@ public class BitmapUtil { private static final int MAX_COMPRESSION_ATTEMPTS = 5; private static final int MIN_COMPRESSION_QUALITY_DECREASE = 5; - @android.support.annotation.WorkerThread + @WorkerThread public static ScaleResult createScaledBytes(Context context, T model, MediaConstraints constraints) throws BitmapDecodingException + { + return createScaledBytes(context, model, + constraints.getImageMaxWidth(context), + constraints.getImageMaxHeight(context), + constraints.getImageMaxSize(context)); + } + + @WorkerThread + public static ScaleResult createScaledBytes(Context context, T model, int maxImageWidth, int maxImageHeight, int maxImageSize) + throws BitmapDecodingException { try { int quality = MAX_COMPRESSION_QUALITY; @@ -59,8 +69,7 @@ public class BitmapUtil { .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) .downsample(DownsampleStrategy.AT_MOST) - .submit(constraints.getImageMaxWidth(context), - constraints.getImageMaxWidth(context)) + .submit(maxImageWidth, maxImageHeight) .get(); if (scaledBitmap == null) { @@ -76,14 +85,14 @@ public class BitmapUtil { Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb"); if (quality == MIN_COMPRESSION_QUALITY) break; - int nextQuality = (int)Math.floor(quality * Math.sqrt((double)constraints.getImageMaxSize(context) / bytes.length)); + int nextQuality = (int)Math.floor(quality * Math.sqrt((double)maxImageSize / bytes.length)); if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) { nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE; } quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY); } - while (bytes.length > constraints.getImageMaxSize(context) && attempts++ < MAX_COMPRESSION_ATTEMPTS); - if (bytes.length > constraints.getImageMaxSize(context)) { + while (bytes.length > maxImageSize && attempts++ < MAX_COMPRESSION_ATTEMPTS); + if (bytes.length > maxImageSize) { throw new BitmapDecodingException("Unable to scale image below: " + bytes.length); } Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)"); diff --git a/src/org/thoughtcrime/securesms/util/JsonUtils.java b/src/org/thoughtcrime/securesms/util/JsonUtils.java index 4d4deef19b..1872f02ffb 100644 --- a/src/org/thoughtcrime/securesms/util/JsonUtils.java +++ b/src/org/thoughtcrime/securesms/util/JsonUtils.java @@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.util; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -39,4 +42,30 @@ public class JsonUtils { public static ObjectMapper getMapper() { return objectMapper; } + + public static class SaneJSONObject { + + private final JSONObject delegate; + + public SaneJSONObject(JSONObject delegate) { + this.delegate = delegate; + } + + public String getString(String name) throws JSONException { + if (delegate.isNull(name)) return null; + else return delegate.getString(name); + } + + public long getLong(String name) throws JSONException { + return delegate.getLong(name); + } + + public boolean isNull(String name) { + return delegate.isNull(name); + } + + public int getInt(String name) throws JSONException { + return delegate.getInt(name); + } + } } diff --git a/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java b/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java deleted file mode 100644 index 165110cdac..0000000000 --- a/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.net.Uri; - -import org.thoughtcrime.securesms.TextSecureTestCase; -import org.thoughtcrime.securesms.attachments.AttachmentId; -import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.util.BitmapDecodingException; - -import java.io.FileNotFoundException; -import java.io.InputStream; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class AttachmentDatabaseTest extends TextSecureTestCase { - private static final long ROW_ID = 1L; - private static final long UNIQUE_ID = 2L; - - private AttachmentDatabase database; - - @Override - public void setUp() { - super.setUp(); - database = spy(DatabaseFactory.getAttachmentDatabase(getInstrumentation().getTargetContext())); - } - - public void testThumbnailGenerationTaskNotRunWhenThumbnailExists() throws Exception { - final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); - - DatabaseAttachment mockAttachment = getMockAttachment("x/x"); - when(database.getAttachment(null, attachmentId)).thenReturn(mockAttachment); - - InputStream mockInputStream = mock(InputStream.class); - doReturn(mockInputStream).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); - database.getThumbnailStream(mock(MasterSecret.class), attachmentId); - - // Works as the Future#get() call in AttachmentDatabase#getThumbnailStream() makes updating synchronous - verify(database, never()).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat()); - } - - public void testThumbnailGenerationTaskRunWhenThumbnailMissing() throws Exception { - final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); - - DatabaseAttachment mockAttachment = getMockAttachment("image/png"); - when(database.getAttachment(null, attachmentId)).thenReturn(mockAttachment); - - doReturn(null).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); - doNothing().when(database).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat()); - - try { - database.new ThumbnailFetchCallable(mock(MasterSecret.class), attachmentId).call(); - throw new AssertionError("Didn't try to generate thumbnail"); - } catch (BitmapDecodingException bde) { - if (!(bde.getCause() instanceof FileNotFoundException)) { - throw new AssertionError("Thumbnail generation failed for another reason than a FileNotFoundException: " + bde.getMessage()); - } // else success - } - } - - private DatabaseAttachment getMockAttachment(String contentType) { - DatabaseAttachment attachment = mock(DatabaseAttachment.class); - when(attachment.getContentType()).thenReturn(contentType); - when(attachment.getDataUri()).thenReturn(Uri.EMPTY); - when(attachment.hasData()).thenReturn(true); - - return attachment; - } -} diff --git a/test/androidTest/java/org/thoughtcrime/securesms/database/CanonicalAddressDatabaseTest.java b/test/androidTest/java/org/thoughtcrime/securesms/database/CanonicalAddressDatabaseTest.java deleted file mode 100644 index 0906f27107..0000000000 --- a/test/androidTest/java/org/thoughtcrime/securesms/database/CanonicalAddressDatabaseTest.java +++ /dev/null @@ -1,119 +0,0 @@ -//package org.thoughtcrime.securesms.database; -// -//import org.thoughtcrime.securesms.TextSecureTestCase; -// -//import static org.assertj.core.api.Assertions.assertThat; -// -//public class CanonicalAddressDatabaseTest extends TextSecureTestCase { -// private static final String AMBIGUOUS_NUMBER = "222-3333"; -// private static final String SPECIFIC_NUMBER = "+49 444 222 3333"; -// private static final String EMAIL = "a@b.fom"; -// private static final String SIMILAR_EMAIL = "a@b.com"; -// private static final String GROUP = "__textsecure_group__!000111222333"; -// private static final String SIMILAR_GROUP = "__textsecure_group__!100111222333"; -// private static final String ALPHA = "T-Mobile"; -// private static final String SIMILAR_ALPHA = "T-Mobila"; -// -// private CanonicalAddressDatabase db; -// -// @Override -// public void setUp() { -// super.setUp(); -// this.db = CanonicalAddressDatabase.getInstance(getInstrumentation().getTargetContext()); -// } -// -// public void tearDown() throws Exception { -// -// } -// -// /** -// * Throw two equivalent numbers (one without locale info, one with full info) at the canonical -// * address db and see that the caching and DB operations work properly in revealing the right -// * addresses. This is run twice to ensure cache logic is hit. -// * -// * @throws Exception -// */ -// public void testNumberAddressUpdates() throws Exception { -// final long id = db.getCanonicalAddressId(AMBIGUOUS_NUMBER); -// -// assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER); -// assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id); -// assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER); -// assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id); -// -// assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id); -// assertThat(db.getAddressFromId(id)).isEqualTo(AMBIGUOUS_NUMBER); -// assertThat(db.getCanonicalAddressId(SPECIFIC_NUMBER)).isEqualTo(id); -// assertThat(db.getAddressFromId(id)).isEqualTo(SPECIFIC_NUMBER); -// assertThat(db.getCanonicalAddressId(AMBIGUOUS_NUMBER)).isEqualTo(id); -// } -// -// public void testSimilarNumbers() throws Exception { -// assertThat(db.getCanonicalAddressId("This is a phone number 222-333-444")) -// .isNotEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("222-333-444")) -// .isNotEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("222-333-44")) -// .isNotEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("222-333-4")) -// .isNotEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("+49 222-333-4444")) -// .isNotEqualTo(db.getCanonicalAddressId("+1 222-333-4444")); -// -// assertThat(db.getCanonicalAddressId("1 222-333-4444")) -// .isEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("1 (222) 333-4444")) -// .isEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("+12223334444")) -// .isEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("+1 (222) 333.4444")) -// .isEqualTo(db.getCanonicalAddressId("222-333-4444")); -// assertThat(db.getCanonicalAddressId("+49 (222) 333.4444")) -// .isEqualTo(db.getCanonicalAddressId("222-333-4444")); -// -// } -// -// public void testEmailAddresses() throws Exception { -// final long emailId = db.getCanonicalAddressId(EMAIL); -// final long similarEmailId = db.getCanonicalAddressId(SIMILAR_EMAIL); -// -// assertThat(emailId).isNotEqualTo(similarEmailId); -// -// assertThat(db.getAddressFromId(emailId)).isEqualTo(EMAIL); -// assertThat(db.getAddressFromId(similarEmailId)).isEqualTo(SIMILAR_EMAIL); -// } -// -// public void testGroups() throws Exception { -// final long groupId = db.getCanonicalAddressId(GROUP); -// final long similarGroupId = db.getCanonicalAddressId(SIMILAR_GROUP); -// -// assertThat(groupId).isNotEqualTo(similarGroupId); -// -// assertThat(db.getAddressFromId(groupId)).isEqualTo(GROUP); -// assertThat(db.getAddressFromId(similarGroupId)).isEqualTo(SIMILAR_GROUP); -// } -// -// public void testAlpha() throws Exception { -// final long id = db.getCanonicalAddressId(ALPHA); -// final long similarId = db.getCanonicalAddressId(SIMILAR_ALPHA); -// -// assertThat(id).isNotEqualTo(similarId); -// -// assertThat(db.getAddressFromId(id)).isEqualTo(ALPHA); -// assertThat(db.getAddressFromId(similarId)).isEqualTo(SIMILAR_ALPHA); -// } -// -// public void testIsNumber() throws Exception { -// assertThat(CanonicalAddressDatabase.isNumberAddress("+495556666777")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("(222) 333-4444")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("1 (222) 333-4444")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile123")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("333-4444")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("12345")).isTrue(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile")).isFalse(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("T-Mobile1")).isFalse(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("Wherever bank")).isFalse(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("__textsecure_group__!afafafafafaf")).isFalse(); -// assertThat(CanonicalAddressDatabase.isNumberAddress("email@domain.com")).isFalse(); -// } -//} \ No newline at end of file