This commit is contained in:
Moxie Marlinspike 2018-02-07 14:01:37 -08:00
parent 8bec5a96f5
commit d567534609
72 changed files with 1164 additions and 505 deletions

View File

@ -75,7 +75,7 @@ dependencies {
compile('org.whispersystems:libpastelog:1.1.2') { compile('org.whispersystems:libpastelog:1.1.2') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp' 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 'org.whispersystems:webrtc-android:M64'
compile "me.leolin:ShortcutBadger:1.1.16" compile "me.leolin:ShortcutBadger:1.1.16"
@ -164,7 +164,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718', 'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:fe56b4db9ec743c8b565e3e4caa9228fafe132dc0bf82000d6e359b97a81177c', 'org.whispersystems:libpastelog:fe56b4db9ec743c8b565e3e4caa9228fafe132dc0bf82000d6e359b97a81177c',
'org.whispersystems:signal-service-android:a7dfcb2f88ec69e8a1d31215cc7b67f0db50a96cd9d3832bfe75f56e67188537', 'org.whispersystems:signal-service-android:dd0c21b37b239ac9c3eaf0b290791a3708817daa13e82e24b0544631f948d8d3',
'org.whispersystems:webrtc-android:ed297e8b795dad9658cf306c2aa0f7d296c65f0997a2ac4353fd0157910acc12', 'org.whispersystems:webrtc-android:ed297e8b795dad9658cf306c2aa0f7d296c65f0997a2ac4353fd0157910acc12',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774', 'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -203,7 +203,7 @@ dependencyVerification {
'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15', 'com.github.bumptech.glide:gifdecoder:59ccf3bb0cec11dab4b857382cbe0b171111b6fc62bf141adce4e1180889af15',
'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da', 'com.android.support:support-annotations:af05330d997eb92a066534dbe0a3ea24347d26d7001221092113ae02a8f233da',
'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1', '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:disklrucache:c1b1b6f5bbd01e2fcdc9d7f60913c8d338bdb65ed4a93bfa02b56f19daaade4b',
'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512', 'com.github.bumptech.glide:annotations:bede99ef9f71517a4274bac18fd3e483e9f2b6108d7d6fe8f4949be4aa4d9512',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray5"/>
<stroke android:color="@color/gray10" android:width="1dp"/>
<corners android:radius="5dp" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white"/>
<stroke android:color="@color/transparent"/>
<corners android:topLeftRadius="5dp" android:bottomLeftRadius="5dp"/>
</shape>

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<org.thoughtcrime.securesms.components.InputPanel <org.thoughtcrime.securesms.components.InputPanel
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_panel" android:id="@+id/bottom_panel"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -27,6 +26,20 @@
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:background="@drawable/sent_bubble" android:background="@drawable/sent_bubble"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.QuoteView
android:id="@+id/quote_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:quote_dismissable="true"
tools:visibility="visible"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"> android:clipToPadding="false">
<org.thoughtcrime.securesms.components.emoji.EmojiToggle <org.thoughtcrime.securesms.components.emoji.EmojiToggle
@ -98,6 +111,8 @@
</org.thoughtcrime.securesms.components.MicrophoneRecorderView> </org.thoughtcrime.securesms.components.MicrophoneRecorderView>
</org.thoughtcrime.securesms.components.HidingLinearLayout> </org.thoughtcrime.securesms.components.HidingLinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout android:id="@+id/recording_container" <LinearLayout android:id="@+id/recording_container"
@ -174,4 +189,3 @@
</org.thoughtcrime.securesms.components.AnimatingToggle> </org.thoughtcrime.securesms.components.AnimatingToggle>
</org.thoughtcrime.securesms.components.InputPanel> </org.thoughtcrime.securesms.components.InputPanel>
</merge>

View File

@ -75,6 +75,14 @@
</LinearLayout> </LinearLayout>
<org.thoughtcrime.securesms.components.QuoteView
android:id="@+id/quote_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:quote_dismissable="false"
tools:visibility="visible"/>
<ViewStub <ViewStub
android:id="@+id/image_view_stub" android:id="@+id/image_view_stub"
android:layout="@layout/conversation_item_received_thumbnail" android:layout="@layout/conversation_item_received_thumbnail"

View File

@ -38,6 +38,14 @@
android:background="@drawable/sent_bubble" android:background="@drawable/sent_bubble"
android:orientation="vertical"> android:orientation="vertical">
<org.thoughtcrime.securesms.components.QuoteView
android:id="@+id/quote_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:quote_dismissable="false"
tools:visibility="visible"/>
<ViewStub <ViewStub
android:id="@+id/image_view_stub" android:id="@+id/image_view_stub"
android:layout_width="@dimen/media_bubble_default_dimens" android:layout_width="@dimen/media_bubble_default_dimens"

101
res/layout/quote_view.xml Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/quote_container"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="3dp"
android:background="@drawable/quote_background"
tools:visibility="visible"
tools:parentTag="android.widget.LinearLayout">
<ImageView android:id="@+id/quote_bar"
android:layout_width="5dp"
android:layout_height="match_parent"
android:src="@drawable/quote_bar"
tools:tint="@color/purple_400"/>
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:paddingBottom="10dp">
<TextView android:id="@+id/quote_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:maxLines="1"
tools:textColor="@color/purple_400"
tools:text="Riya"/>
<LinearLayout android:id="@+id/media_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<ImageView android:id="@+id/media_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/gray50"
android:src="@drawable/ic_insert_photo_white_18dp"/>
<TextView android:id="@+id/media_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:textSize="11sp"
tools:text="Photo"/>
</LinearLayout>
<TextView android:id="@+id/quote_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:maxLines="3"
android:ellipsize="end"
tools:text="Short text."
/>
</LinearLayout>
<FrameLayout android:layout_width="wrap_content"
android:layout_height="match_parent">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/quote_attachment"
android:layout_width="80dp"
android:layout_height="match_parent"
app:riv_corner_radius_top_right="5dp"
app:riv_corner_radius_bottom_right="5dp"
android:scaleType="centerCrop"
android:visibility="gone"
tools:src="@drawable/surfwalk2"
tools:visibility="visible"/>
<ImageView android:id="@+id/quote_dismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:src="@drawable/ic_close_white_18dp"
android:tint="@color/gray70"
android:background="@drawable/circle_alpha"
android:layout_marginTop="5dp"
android:layout_marginRight="5dp"
android:layout_marginEnd="5dp"/>
</FrameLayout>
</merge>

View File

@ -30,4 +30,11 @@
android:visible="false" android:visible="false"
android:icon="?menu_save_icon" android:icon="?menu_save_icon"
app:showAsAction="always" /> app:showAsAction="always" />
<item android:title="Reply"
android:id="@+id/menu_context_reply"
android:visible="true"
android:icon="?menu_reply_icon"
app:showAsAction="always" />
</menu> </menu>

View File

@ -108,6 +108,7 @@
<attr name="menu_info_icon" format="reference" /> <attr name="menu_info_icon" format="reference" />
<attr name="menu_forward_icon" format="reference" /> <attr name="menu_forward_icon" format="reference" />
<attr name="menu_save_icon" format="reference" /> <attr name="menu_save_icon" format="reference" />
<attr name="menu_reply_icon" format="reference" />
<attr name="pref_icon_tint" format="color"/> <attr name="pref_icon_tint" format="color"/>
@ -241,5 +242,9 @@
<attr name="vcv_textColor" format="color"/> <attr name="vcv_textColor" format="color"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="QuoteView">
<attr name="quote_dismissable" format="boolean"/>
</declare-styleable>
</resources> </resources>

View File

@ -23,6 +23,19 @@
<color name="gray78">#ff383838</color> <color name="gray78">#ff383838</color>
<color name="gray95">#ff111111</color> <color name="gray95">#ff111111</color>
<color name="transparent_black_05">#05000000</color>
<color name="transparent_black_10">#10000000</color>
<color name="transparent_black_20">#20000000</color>
<color name="transparent_black_30">#30000000</color>
<color name="transparent_black_40">#40000000</color>
<color name="transparent_white_05">#05ffffff</color>
<color name="transparent_white_10">#10ffffff</color>
<color name="transparent_white_20">#20ffffff</color>
<color name="transparent_white_30">#30ffffff</color>
<color name="transparent_white_40">#40ffffff</color>
<color name="transparent_white_aa">#aaffffff</color>
<color name="conversation_compose_divider">#32000000</color> <color name="conversation_compose_divider">#32000000</color>
<color name="action_mode_status_bar">@color/gray65</color> <color name="action_mode_status_bar">@color/gray65</color>

View File

@ -219,6 +219,7 @@
<item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item> <item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item>
<item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item> <item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item>
<item name="menu_save_icon">@drawable/ic_save_white_24dp</item> <item name="menu_save_icon">@drawable/ic_save_white_24dp</item>
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
<item name="conversation_icon_attach_audio">@drawable/ic_audio_light</item> <item name="conversation_icon_attach_audio">@drawable/ic_audio_light</item>
<item name="conversation_icon_attach_video">@drawable/ic_video_light</item> <item name="conversation_icon_attach_video">@drawable/ic_video_light</item>
@ -343,6 +344,7 @@
<item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item> <item name="menu_info_icon">@drawable/ic_info_outline_white_24dp</item>
<item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item> <item name="menu_forward_icon">@drawable/ic_forward_white_24dp</item>
<item name="menu_save_icon">@drawable/ic_save_white_24dp</item> <item name="menu_save_icon">@drawable/ic_save_white_24dp</item>
<item name="menu_reply_icon">@drawable/ic_reply_white_24dp</item>
<item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item> <item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item>
<item name="conversation_icon_attach_video">@drawable/ic_video_dark</item> <item name="conversation_icon_attach_video">@drawable/ic_video_dark</item>

View File

@ -65,6 +65,7 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.annimon.stream.Stream;
import com.google.android.gms.location.places.ui.PlacePicker; import com.google.android.gms.location.places.ui.PlacePicker;
import com.google.protobuf.ByteString; 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.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList; 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.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
@ -766,7 +769,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.setType(GroupContext.Type.QUIT) .setType(GroupContext.Type.QUIT)
.build(); .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); MessageSender.send(self, outgoingMessage, threadId, false, null);
DatabaseFactory.getGroupDatabase(self).remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(self))); DatabaseFactory.getGroupDatabase(self).remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(self)));
initializeEnabledCheck(); initializeEnabledCheck();
@ -1648,7 +1651,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
handleUnverifiedRecipients(); handleUnverifiedRecipients();
} else if (!forceSms && identityRecords.isUntrusted()) { } else if (!forceSms && identityRecords.isUntrusted()) {
handleUntrustedRecipients(); 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); sendMediaMessage(forceSms, expiresIn, subscriptionId, initiating);
} else { } else {
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating); 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) private void sendMediaMessage(final boolean forceSms, final long expiresIn, final int subscriptionId, boolean initiating)
throws InvalidMessageException throws InvalidMessageException
{ {
Log.w(TAG, "Sending media message...");
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId, initiating); sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), expiresIn, subscriptionId, initiating);
} }
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, final long expiresIn, final int subscriptionId, final boolean initiating) { private ListenableFuture<Void> 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, inputPanel.getQuote().orNull());
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType);
final SettableFuture<Void> future = new SettableFuture<>(); final SettableFuture<Void> future = new SettableFuture<>();
final Context context = getApplicationContext(); final Context context = getApplicationContext();
@ -1691,6 +1694,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.ifNecessary(!isSecureText || forceSms) .ifNecessary(!isSecureText || forceSms)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms)) .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms))
.onAllGranted(() -> { .onAllGranted(() -> {
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false); attachmentManager.clear(glideRequests, false);
composeText.setText(""); composeText.setText("");
final long id = fragment.stageOutgoingMessage(outgoingMessage); final long id = fragment.stageOutgoingMessage(outgoingMessage);
@ -2048,6 +2052,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
this.threadId = threadId; 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 @Override
public void onAttachmentChanged() { public void onAttachmentChanged() {
handleSecurityChange(isSecureText, isDefaultSms); handleSecurityChange(isSecureText, isDefaultSms);

View File

@ -29,7 +29,10 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.FastCursorRecyclerViewAdapter;
@ -54,6 +57,7 @@ import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -260,10 +264,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override @Override
public long getItemId(@NonNull Cursor cursor) { public long getItemId(@NonNull Cursor cursor) {
String fastPreflightId = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.FAST_PREFLIGHT_ID)); List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(cursor);
List<DatabaseAttachment> messageAttachments = Stream.of(attachments).filterNot(DatabaseAttachment::isQuote).toList();
if (fastPreflightId != null) { if (messageAttachments.size() > 0 && messageAttachments.get(0).getFastPreflightId() != null) {
return Long.valueOf(fastPreflightId); return Long.valueOf(messageAttachments.get(0).getFastPreflightId());
} }
final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID)); final String unique = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));

View File

@ -386,6 +386,10 @@ public class ConversationFragment extends Fragment
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
} }
private void handleReplyMessage(final MessageRecord message) {
listener.handleReplyMessage(message);
}
private void handleSaveAttachment(final MediaMmsMessageRecord message) { private void handleSaveAttachment(final MediaMmsMessageRecord message) {
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() { SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
@ -493,6 +497,7 @@ public class ConversationFragment extends Fragment
public interface ConversationFragmentListener { public interface ConversationFragmentListener {
void setThreadId(long threadId); void setThreadId(long threadId);
void handleReplyMessage(MessageRecord messageRecord);
} }
private class ConversationScrollListener extends OnScrollListener { private class ConversationScrollListener extends OnScrollListener {
@ -668,6 +673,10 @@ public class ConversationFragment extends Fragment
handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord()); handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord());
actionMode.finish(); actionMode.finish();
return true; return true;
case R.id.menu_context_reply:
handleReplyMessage(getSelectedMessageRecord());
actionMode.finish();
return true;
} }
return false; return false;

View File

@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.ExpirationTimerView; import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; 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.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; 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.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob; import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob; import org.thoughtcrime.securesms.jobs.MmsSendJob;
@ -111,6 +113,7 @@ public class ConversationItem extends LinearLayout
private GlideRequests glideRequests; private GlideRequests glideRequests;
protected View bodyBubble; protected View bodyBubble;
private QuoteView quoteView;
private TextView bodyText; private TextView bodyText;
private TextView dateText; private TextView dateText;
private TextView simInfoText; private TextView simInfoText;
@ -173,6 +176,7 @@ public class ConversationItem extends LinearLayout
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
this.expirationTimer = findViewById(R.id.expiration_indicator); this.expirationTimer = findViewById(R.id.expiration_indicator);
this.groupSenderHolder = findViewById(R.id.group_sender_holder); this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
setOnClickListener(new ClickListener(null)); setOnClickListener(new ClickListener(null));
@ -210,6 +214,7 @@ public class ConversationItem extends LinearLayout
setMinimumWidth(); setMinimumWidth();
setSimInfo(messageRecord); setSimInfo(messageRecord);
setExpiration(messageRecord); setExpiration(messageRecord);
setQuote(messageRecord);
} }
@Override @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() { private void setFailedStatusIcons() {
alertView.setFailed(); alertView.setFailed();
deliveryStatusIndicator.setNone(); deliveryStatusIndicator.setNone();

View File

@ -35,10 +35,12 @@ public abstract class Attachment {
private final int width; private final int width;
private final int height; private final int height;
private final boolean quote;
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName, public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
@Nullable String location, @Nullable String key, @Nullable String relay, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height) int width, int height, boolean quote)
{ {
this.contentType = contentType; this.contentType = contentType;
this.transferState = transferState; this.transferState = transferState;
@ -52,6 +54,7 @@ public abstract class Attachment {
this.voiceNote = voiceNote; this.voiceNote = voiceNote;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.quote = quote;
} }
@Nullable @Nullable
@ -119,4 +122,8 @@ public abstract class Attachment {
public int getHeight() { public int getHeight() {
return height; return height;
} }
public boolean isQuote() {
return quote;
}
} }

View File

@ -17,9 +17,9 @@ public class DatabaseAttachment extends Attachment {
String contentType, int transferProgress, long size, String contentType, int transferProgress, long size,
String fileName, String location, String key, String relay, String fileName, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote, 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.attachmentId = attachmentId;
this.hasData = hasData; this.hasData = hasData;
this.hasThumbnail = hasThumbnail; this.hasThumbnail = hasThumbnail;

View File

@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment { public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) { 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 @Nullable

View File

@ -16,11 +16,11 @@ public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size, private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, @NonNull String location, @Nullable String fileName, @NonNull String location,
@NonNull String key, @NonNull String relay, @Nullable String key, @NonNull String relay,
@Nullable byte[] digest, boolean voiceNote, @Nullable byte[] digest, boolean voiceNote,
int width, int height) 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 @Nullable
@ -41,23 +41,36 @@ public class PointerAttachment extends Attachment {
if (pointers.isPresent()) { if (pointers.isPresent()) {
for (SignalServiceAttachment pointer : pointers.get()) { for (SignalServiceAttachment pointer : pointers.get()) {
if (pointer.isPointer()) { Optional<Attachment> result = forPointer(Optional.of(pointer));
String encodedKey = Base64.encodeBytes(pointer.asPointer().getKey());
results.add(new PointerAttachment(pointer.getContentType(), if (result.isPresent()) {
AttachmentDatabase.TRANSFER_PROGRESS_PENDING, results.add(result.get());
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()));
} }
} }
} }
return results; return results;
} }
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> 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()));
}
} }

View File

@ -10,17 +10,17 @@ public class UriAttachment extends Attachment {
private final @Nullable Uri thumbnailUri; private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, 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, public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height, @NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId, @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.dataUri = dataUri;
this.thumbnailUri = thumbnailUri; this.thumbnailUri = thumbnailUri;
} }

View File

@ -23,12 +23,17 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle; 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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -43,6 +48,7 @@ public class InputPanel extends LinearLayout
private static final int FADE_TIME = 150; private static final int FADE_TIME = 150;
private QuoteView quoteView;
private EmojiToggle emojiToggle; private EmojiToggle emojiToggle;
private ComposeText composeText; private ComposeText composeText;
private View quickCameraToggle; private View quickCameraToggle;
@ -74,15 +80,18 @@ public class InputPanel extends LinearLayout
public void onFinishInflate() { public void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle); View quoteDismiss = findViewById(R.id.quote_dismiss);
this.composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
this.quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); this.quoteView = findViewById(R.id.quote_view);
this.quickAudioToggle = ViewUtil.findById(this, R.id.quick_audio_toggle); this.emojiToggle = findViewById(R.id.emoji_toggle);
this.buttonToggle = ViewUtil.findById(this, R.id.button_toggle); this.composeText = findViewById(R.id.embedded_text_editor);
this.recordingContainer = ViewUtil.findById(this, R.id.recording_container); this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
this.recordTime = new RecordTime((TextView) ViewUtil.findById(this, R.id.record_time)); this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
this.slideToCancel = new SlideToCancel(ViewUtil.findById(this, R.id.slide_to_cancel)); this.buttonToggle = findViewById(R.id.button_toggle);
this.microphoneRecorderView = ViewUtil.findById(this, R.id.recorder_view); 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); this.microphoneRecorderView.setListener(this);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
@ -97,6 +106,8 @@ public class InputPanel extends LinearLayout
emojiToggle.setVisibility(View.VISIBLE); emojiToggle.setVisibility(View.VISIBLE);
emojiVisible = true; emojiVisible = true;
} }
quoteDismiss.setOnClickListener(v -> clearQuote());
} }
public void setListener(final @NonNull Listener listener) { public void setListener(final @NonNull Listener listener) {
@ -109,6 +120,23 @@ public class InputPanel extends LinearLayout
composeText.setMediaListener(listener); 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<QuoteModel> 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) { public void setEmojiDrawer(@NonNull EmojiDrawer emojiDrawer) {
emojiToggle.attach(emojiDrawer); emojiToggle.attach(emojiDrawer);
} }
@ -210,6 +238,7 @@ public class InputPanel extends LinearLayout
composeText.insertEmoji(emoji); composeText.insertEmoji(emoji);
} }
public interface Listener { public interface Listener {
void onRecorderStarted(); void onRecorderStarted();
void onRecorderFinished(); void onRecorderFinished();

View File

@ -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<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
List<Slide> 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<Slide> 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<Attachment> getAttachments() {
return attachments.asAttachments();
}
}

View File

@ -33,6 +33,8 @@ import android.util.Pair;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
import org.json.JSONArray;
import org.json.JSONException;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; 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.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.StorageUtil;
@ -55,6 +58,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; 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 TABLE_NAME = "part";
public static final String ROW_ID = "_id"; 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 MMS_ID = "mid";
static final String CONTENT_TYPE = "ct"; static final String CONTENT_TYPE = "ct";
static final String NAME = "name"; static final String NAME = "name";
@ -82,7 +86,8 @@ public class AttachmentDatabase extends Database {
public static final String UNIQUE_ID = "unique_id"; public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest"; static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note"; 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"; public static final String DATA_RANDOM = "data_random";
private static final String THUMBNAIL_RANDOM = "thumbnail_random"; private static final String THUMBNAIL_RANDOM = "thumbnail_random";
static final String WIDTH = "width"; 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 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, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, 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, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + 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, " + FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " + UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " + 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
@ -181,9 +186,15 @@ public class AttachmentDatabase extends Database {
try { try {
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor); if (cursor != null && cursor.moveToFirst()) {
else return null; List<DatabaseAttachment> list = getAttachment(cursor);
if (list != null && list.size() > 0) {
return list.get(0);
}
}
return null;
} finally { } finally {
if (cursor != null) if (cursor != null)
cursor.close(); cursor.close();
@ -200,7 +211,7 @@ public class AttachmentDatabase extends Database {
null, null, null); null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
results.add(getAttachment(cursor)); results.addAll(getAttachment(cursor));
} }
return results; return results;
@ -218,7 +229,7 @@ public class AttachmentDatabase extends Database {
try { try {
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
attachments.add(getAttachment(cursor)); attachments.addAll(getAttachment(cursor));
} }
} finally { } finally {
if (cursor != null) cursor.close(); if (cursor != null) cursor.close();
@ -327,15 +338,19 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
} }
void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments) void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
throws MmsException throws MmsException
{ {
Log.w(TAG, "insertParts(" + attachments.size() + ")"); Log.w(TAG, "insertParts(" + attachments.size() + ")");
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
AttachmentId attachmentId = insertAttachment(mmsId, attachment); AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote());
Log.w(TAG, "Inserted attachment at ID: " + attachmentId); Log.w(TAG, "Inserted attachment at ID: " + attachmentId);
} }
for (Attachment attachment : quoteAttachment) {
insertAttachment(mmsId, attachment, true);
}
} }
public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment, public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment,
@ -376,7 +391,8 @@ public class AttachmentDatabase extends Database {
databaseAttachment.getFastPreflightId(), databaseAttachment.getFastPreflightId(),
databaseAttachment.isVoiceNote(), databaseAttachment.isVoiceNote(),
mediaStream.getWidth(), mediaStream.getWidth(),
mediaStream.getHeight()); mediaStream.getHeight(),
databaseAttachment.isQuote());
} }
@ -519,8 +535,43 @@ public class AttachmentDatabase extends Database {
} }
} }
DatabaseAttachment getAttachment(@NonNull Cursor cursor) { public List<DatabaseAttachment> getAttachment(@NonNull Cursor cursor) {
return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)), try {
if (cursor.getColumnIndex(AttachmentDatabase.ATTACHMENT_JSON_ALIAS) != -1) {
if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) {
return new LinkedList<>();
}
List<DatabaseAttachment> result = new LinkedList<>();
JSONArray array = new JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS)));
for (int i=0;i<array.length();i++) {
JsonUtils.SaneJSONObject object = new JsonUtils.SaneJSONObject(array.getJSONObject(i));
if (!object.isNull(ROW_ID)) {
result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)),
object.getLong(MMS_ID),
!TextUtils.isEmpty(object.getString(DATA)),
!TextUtils.isEmpty(object.getString(THUMBNAIL)),
object.getString(CONTENT_TYPE),
object.getInt(TRANSFER_STATE),
object.getLong(SIZE),
object.getString(FILE_NAME),
object.getString(CONTENT_LOCATION),
object.getString(CONTENT_DISPOSITION),
object.getString(NAME),
null,
object.getString(FAST_PREFLIGHT_ID),
object.getInt(VOICE_NOTE) == 1,
object.getInt(WIDTH),
object.getInt(HEIGHT),
object.getInt(QUOTE) == 1));
}
}
return result;
} else {
return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)),
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
@ -528,7 +579,7 @@ public class AttachmentDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
StorageUtil.getCleanFileName(cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME))), cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)), cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
@ -536,11 +587,16 @@ public class AttachmentDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT))); cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1));
}
} catch (JSONException e) {
throw new AssertionError(e);
}
} }
private AttachmentId insertAttachment(long mmsId, Attachment attachment) private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote)
throws MmsException throws MmsException
{ {
Log.w(TAG, "Inserting attachment for mms id: " + mmsId); Log.w(TAG, "Inserting attachment for mms id: " + mmsId);
@ -569,6 +625,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0); contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0);
contentValues.put(WIDTH, attachment.getWidth()); contentValues.put(WIDTH, attachment.getWidth());
contentValues.put(HEIGHT, attachment.getHeight()); contentValues.put(HEIGHT, attachment.getHeight());
contentValues.put(QUOTE, quote);
if (dataInfo != null) { if (dataInfo != null) {
contentValues.put(DATA, dataInfo.file.getAbsolutePath()); contentValues.put(DATA, dataInfo.file.getAbsolutePath());

View File

@ -13,9 +13,11 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.List;
public class MediaDatabase extends Database { public class MediaDatabase extends Database {
private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS + ", " private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
@ -32,6 +34,7 @@ public class MediaDatabase extends Database {
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", "
@ -48,7 +51,7 @@ public class MediaDatabase extends Database {
private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'"); private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'");
private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%'"); private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " + AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%'");
public MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) { MediaDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -90,7 +93,7 @@ public class MediaDatabase extends Database {
public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) { public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment attachment = attachmentDatabase.getAttachment(cursor); List<DatabaseAttachment> attachments = attachmentDatabase.getAttachment(cursor);
String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)); String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX))); boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)));
Address address = null; Address address = null;
@ -107,7 +110,7 @@ public class MediaDatabase extends Database {
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)); 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() { public DatabaseAttachment getAttachment() {

View File

@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; 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.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; 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 PART_COUNT = "part_count";
static final String NETWORK_FAILURE = "network_failures"; 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, " + 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, " + THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " + 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, " + NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER, " +
SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " + SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + EXPIRES_IN + " INTEGER DEFAULT 0, " +
EXPIRE_STARTED + " INTEGER DEFAULT 0, " + NOTIFIED + " 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "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, MESSAGE_SIZE, STATUS, TRANSACTION_ID,
BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT,
AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS, "json_group_array(json_object(" +
AttachmentDatabase.UNIQUE_ID, "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
AttachmentDatabase.MMS_ID, "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
AttachmentDatabase.SIZE, "'" + AttachmentDatabase.MMS_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " +
AttachmentDatabase.FILE_NAME, "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
AttachmentDatabase.DATA, "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
AttachmentDatabase.THUMBNAIL, "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
AttachmentDatabase.CONTENT_TYPE, "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
AttachmentDatabase.CONTENT_LOCATION, "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
AttachmentDatabase.DIGEST, "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
AttachmentDatabase.FAST_PREFLIGHT_ID, "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," +
AttachmentDatabase.VOICE_NOTE, "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," +
AttachmentDatabase.WIDTH, "'" + AttachmentDatabase.WIDTH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + "," +
AttachmentDatabase.HEIGHT, "'" + AttachmentDatabase.HEIGHT + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + "," +
AttachmentDatabase.CONTENT_DISPOSITION, "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " +
AttachmentDatabase.NAME, "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " +
AttachmentDatabase.TRANSFER_STATE "'" + 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 = ?"; 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, ",") + return database.rawQuery("SELECT " + Util.join(MMS_PROJECTION, ",") +
" FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " FROM " + MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME +
" ON (" + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + " 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) { public Cursor getMessage(long messageId) {
@ -537,25 +547,37 @@ public class MmsDatabase extends MessagingDatabase {
cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)});
if (cursor != null && cursor.moveToNext()) { if (cursor != null && cursor.moveToNext()) {
List<DatabaseAttachment> associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId);
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)); int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)); long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
List<Attachment> attachments = new LinkedList<>(attachmentDatabase.getAttachmentsForMessage(messageId));
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID));
int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId); int distributionType = DatabaseFactory.getThreadDatabase(context).getDistributionType(threadId);
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote).map(a -> (Attachment)a).toList();
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<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
Recipient recipient = Recipient.from(context, Address.fromSerialized(address), false); 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))) { 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)) { } else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn); 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)) { if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message); return new OutgoingSecureMediaMessage(message);
@ -604,11 +626,13 @@ public class MmsDatabase extends MessagingDatabase {
databaseAttachment.getFastPreflightId(), databaseAttachment.getFastPreflightId(),
databaseAttachment.isVoiceNote(), databaseAttachment.isVoiceNote(),
databaseAttachment.getWidth(), databaseAttachment.getWidth(),
databaseAttachment.getHeight())); databaseAttachment.getHeight(),
databaseAttachment.isQuote()));
} }
return insertMediaMessage(request.getBody(), return insertMediaMessage(request.getBody(),
attachments, attachments,
new LinkedList<>(),
contentValues, contentValues,
null); null);
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
@ -651,12 +675,22 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED));
} }
List<Attachment> 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)) { if (retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) {
Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")"); Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")");
return Optional.absent(); 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)) { if (!Types.isExpirationTimerUpdate(mailbox)) {
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1); 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(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum());
contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.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<Attachment> 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()) { if (message.getRecipient().getAddress().isGroup()) {
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().getAddress().toGroupString(), false); List<Recipient> 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, private long insertMediaMessage(@Nullable String body,
@NonNull List<Attachment> attachments, @NonNull List<Attachment> attachments,
@NonNull List<Attachment> quoteAttachments,
@NonNull ContentValues contentValues, @NonNull ContentValues contentValues,
@Nullable SmsDatabase.InsertListener insertListener) @Nullable SmsDatabase.InsertListener insertListener)
throws MmsException throws MmsException
@ -820,7 +865,7 @@ public class MmsDatabase extends MessagingDatabase {
try { try {
long messageId = db.insert(TABLE_NAME, null, contentValues); long messageId = db.insert(TABLE_NAME, null, contentValues);
partsDatabase.insertAttachmentsForMessage(messageId, attachments); partsDatabase.insertAttachmentsForMessage(messageId, attachments, quoteAttachments);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
return messageId; return messageId;
@ -870,7 +915,6 @@ public class MmsDatabase extends MessagingDatabase {
} }
} }
/*package*/ void deleteThreads(Set<Long> threadIds) { /*package*/ void deleteThreads(Set<Long> threadIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = ""; String where = "";
@ -1021,7 +1065,13 @@ public class MmsDatabase extends MessagingDatabase {
new LinkedList<NetworkFailure>(), new LinkedList<NetworkFailure>(),
message.getSubscriptionId(), message.getSubscriptionId(),
message.getExpiresIn(), 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<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument); List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
List<NetworkFailure> networkFailures = getFailures(networkDocument); List<NetworkFailure> networkFailures = getFailures(networkDocument);
SlideDeck slideDeck = getSlideDeck(cursor); SlideDeck slideDeck = getSlideDeck(cursor);
Quote quote = getQuote(cursor);
return new MediaMmsMessageRecord(context, id, recipient, recipient, return new MediaMmsMessageRecord(context, id, recipient, recipient,
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, addressDeviceId, dateSent, dateReceived, deliveryReceiptCount,
threadId, body, slideDeck, partCount, box, mismatches, threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted, networkFailures, subscriptionId, expiresIn, expireStarted,
readReceiptCount); readReceiptCount, quote);
} }
private Recipient getRecipientFor(String serialized) { private Recipient getRecipientFor(String serialized) {
@ -1163,8 +1214,24 @@ public class MmsDatabase extends MessagingDatabase {
} }
private SlideDeck getSlideDeck(@NonNull Cursor cursor) { private SlideDeck getSlideDeck(@NonNull Cursor cursor) {
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor); List<DatabaseAttachment> attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
return new SlideDeck(context, attachment); List<? extends Attachment> 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<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
List<? extends Attachment> 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() { public void close() {

View File

@ -60,28 +60,20 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.EXPIRE_STARTED, MmsSmsColumns.EXPIRE_STARTED,
MmsSmsColumns.NOTIFIED, MmsSmsColumns.NOTIFIED,
TRANSPORT, TRANSPORT,
AttachmentDatabase.ATTACHMENT_ID_ALIAS, AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
AttachmentDatabase.UNIQUE_ID, MmsDatabase.QUOTE_ID,
AttachmentDatabase.MMS_ID, MmsDatabase.QUOTE_AUTHOR,
AttachmentDatabase.SIZE, MmsDatabase.QUOTE_BODY,
AttachmentDatabase.FILE_NAME, MmsDatabase.QUOTE_ATTACHMENT};
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};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, 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) { public Cursor getConversation(long threadId, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@ -155,7 +147,25 @@ public class MmsSmsDatabase extends Database {
"'MMS::' || " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID "'MMS::' || " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID
+ " || '::' || " + MmsDatabase.DATE_SENT + " || '::' || " + MmsDatabase.DATE_SENT
+ " AS " + MmsSmsColumns.UNIQUE_ROW_ID, + " 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.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, 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.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsSmsColumns.NOTIFIED, MmsSmsColumns.NOTIFIED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT, MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID, MmsDatabase.QUOTE_ID,
AttachmentDatabase.MMS_ID, MmsDatabase.QUOTE_AUTHOR,
AttachmentDatabase.SIZE, MmsDatabase.QUOTE_BODY,
AttachmentDatabase.FILE_NAME, MmsDatabase.QUOTE_ATTACHMENT};
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};
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -189,7 +187,7 @@ public class MmsSmsDatabase extends Database {
"'SMS::' || " + MmsSmsColumns.ID "'SMS::' || " + MmsSmsColumns.ID
+ " || '::' || " + SmsDatabase.DATE_SENT + " || '::' || " + SmsDatabase.DATE_SENT
+ " AS " + MmsSmsColumns.UNIQUE_ROW_ID, + " 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.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, 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.SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED,
MmsSmsColumns.NOTIFIED, MmsSmsColumns.NOTIFIED,
MmsDatabase.NETWORK_FAILURE, TRANSPORT, MmsDatabase.NETWORK_FAILURE, TRANSPORT,
AttachmentDatabase.UNIQUE_ID, MmsDatabase.QUOTE_ID,
AttachmentDatabase.MMS_ID, MmsDatabase.QUOTE_AUTHOR,
AttachmentDatabase.SIZE, MmsDatabase.QUOTE_BODY,
AttachmentDatabase.FILE_NAME, MmsDatabase.QUOTE_ATTACHMENT};
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};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@ -226,11 +212,7 @@ public class MmsSmsDatabase extends Database {
smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME); smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME);
mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " +
AttachmentDatabase.TABLE_NAME + AttachmentDatabase.TABLE_NAME +
" ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " = " + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.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)");
Set<String> mmsColumnsPresent = new HashSet<>(); Set<String> mmsColumnsPresent = new HashSet<>();
@ -273,9 +255,15 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(AttachmentDatabase.VOICE_NOTE); mmsColumnsPresent.add(AttachmentDatabase.VOICE_NOTE);
mmsColumnsPresent.add(AttachmentDatabase.WIDTH); mmsColumnsPresent.add(AttachmentDatabase.WIDTH);
mmsColumnsPresent.add(AttachmentDatabase.HEIGHT); mmsColumnsPresent.add(AttachmentDatabase.HEIGHT);
mmsColumnsPresent.add(AttachmentDatabase.QUOTE);
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION); mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION);
mmsColumnsPresent.add(AttachmentDatabase.NAME); mmsColumnsPresent.add(AttachmentDatabase.NAME);
mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE); 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<String> smsColumnsPresent = new HashSet<>(); Set<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.ID);
@ -298,7 +286,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(SmsDatabase.STATUS); smsColumnsPresent.add(SmsDatabase.STATUS);
@SuppressWarnings("deprecation") @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") @SuppressWarnings("deprecation")
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null); String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null);

View File

@ -38,7 +38,7 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId); Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
while (cursor != null && cursor.moveToNext()) { 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); Uri attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId);
if (attachmentUri.equals(uri)) { if (attachmentUri.equals(uri)) {

View File

@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableString; import android.text.SpannableString;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -52,11 +53,12 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
int partCount, long mailbox, int partCount, long mailbox,
List<IdentityKeyMismatch> mismatches, List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> failures, int subscriptionId, List<NetworkFailure> 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, super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, 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.context = context.getApplicationContext();
this.partCount = partCount; this.partCount = partCount;

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
@ -15,16 +16,20 @@ import java.util.List;
public abstract class MmsMessageRecord extends MessageRecord { 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, MmsMessageRecord(Context context, long id, String body, Recipient conversationRecipient,
Recipient individualRecipient, int recipientDeviceId, long dateSent, Recipient individualRecipient, int recipientDeviceId, long dateSent,
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
long type, List<IdentityKeyMismatch> mismatches, long type, List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures, int subscriptionId, long expiresIn, List<NetworkFailure> 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); super(context, id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount);
this.slideDeck = slideDeck; this.slideDeck = slideDeck;
this.quote = quote;
} }
@Override @Override
@ -52,5 +57,7 @@ public abstract class MmsMessageRecord extends MessageRecord {
return slideDeck.containsMediaSlide(); return slideDeck.containsMediaSlide();
} }
public @Nullable Quote getQuote() {
return quote;
}
} }

View File

@ -55,7 +55,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId, super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId, new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
0, 0, slideDeck, readReceiptCount); 0, 0, slideDeck, readReceiptCount, null);
this.contentLocation = contentLocation; this.contentLocation = contentLocation;
this.messageSize = messageSize; this.messageSize = messageSize;

View File

@ -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;
}
}

View File

@ -111,10 +111,10 @@ public class GroupManager {
if (avatar != null) { if (avatar != null) {
Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar); 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); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId); return new GroupActionResult(groupRecipient, threadId);

View File

@ -212,7 +212,7 @@ public class GroupMessageProcessor {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false)); Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
Recipient recipient = Recipient.from(context, addres, 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 threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);

View File

@ -220,7 +220,7 @@ public class MmsDownloadJob extends MasterSecretJob {
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE, AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length, name, false)); part.getData().length, name, false, false));
} }
} }
} }

View File

@ -1,9 +1,9 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
@ -14,6 +14,7 @@ import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; 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.InsertResult;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; 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.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; 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.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.DuplicateMessageException; import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.IdentityKey; 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 org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -171,7 +178,7 @@ public class PushDecryptJob extends ContextJob {
if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId); if (message.isEndSession()) handleEndSessionMessage(envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(envelope, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(envelope, message, smsMessageId); else if (message.getAttachments().isPresent() || message.getQuote().isPresent()) handleMediaMessage(envelope, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(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))) { if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
@ -398,7 +405,7 @@ public class PushDecryptJob extends ContextJob {
message.getExpiresInSeconds() * 1000L, true, message.getExpiresInSeconds() * 1000L, true,
Optional.fromNullable(envelope.getRelay()), Optional.fromNullable(envelope.getRelay()),
Optional.absent(), message.getGroupInfo(), 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); threadId = GroupMessageProcessor.process(context, envelope, message.getMessage(), true);
} else if (message.getMessage().isExpirationUpdate()) { } else if (message.getMessage().isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(message); threadId = handleSynchronizeSentExpirationUpdate(message);
} else if (message.getMessage().getAttachments().isPresent()) { } else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(message); threadId = handleSynchronizeSentMediaMessage(message);
} else { } else {
threadId = handleSynchronizeSentTextMessage(message); threadId = handleSynchronizeSentTextMessage(message);
@ -517,13 +524,15 @@ public class PushDecryptJob extends ContextJob {
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipient = getMessageDestination(envelope, message); Recipient recipient = getMessageDestination(envelope, message);
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, envelope.getSource()), IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, envelope.getSource()),
message.getTimestamp(), -1, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, message.getExpiresInSeconds() * 1000L, false,
Optional.fromNullable(envelope.getRelay()), Optional.fromNullable(envelope.getRelay()),
message.getBody(), message.getBody(),
message.getGroupInfo(), message.getGroupInfo(),
message.getAttachments()); message.getAttachments(),
quote);
if (message.getExpiresInSeconds() != recipient.getExpireMessages()) { if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
handleExpirationUpdate(envelope, message, Optional.absent()); handleExpirationUpdate(envelope, message, Optional.absent());
@ -573,11 +582,12 @@ public class PushDecryptJob extends ContextJob {
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipients = getSyncMessageDestination(message); Recipient recipients = getSyncMessageDestination(message);
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(), OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
PointerAttachment.forPointers(message.getMessage().getAttachments()), PointerAttachment.forPointers(message.getMessage().getAttachments()),
message.getTimestamp(), -1, message.getTimestamp(), -1,
message.getMessage().getExpiresInSeconds() * 1000L, message.getMessage().getExpiresInSeconds() * 1000,
ThreadDatabase.DistributionTypes.DEFAULT); ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull());
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
@ -664,7 +674,7 @@ public class PushDecryptJob extends ContextJob {
long messageId; long messageId;
if (recipient.getAddress().isGroup()) { 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); outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null); messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
@ -842,6 +852,51 @@ public class PushDecryptJob extends ContextJob {
} }
} }
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> 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<Attachment> 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<InsertResult> insertPlaceholder(@NonNull SignalServiceEnvelope envelope) { private Optional<InsertResult> insertPlaceholder(@NonNull SignalServiceEnvelope envelope) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromExternal(context, envelope.getSource()), 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) { private Recipient getMessageDestination(SignalServiceEnvelope envelope, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) { if (message.getGroupInfo().isPresent()) {
return Recipient.from(context, Address.fromExternal(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); return Recipient.from(context, Address.fromExternal(context, GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false);

View File

@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; 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.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
@ -143,6 +144,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments()); List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments); List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
Optional<Quote> quote = getQuoteFor(message);
List<SignalServiceAddress> addresses; List<SignalServiceAddress> addresses;
@ -171,6 +173,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate()) .asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orNull()) .withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.build(); .build();
messageSender.sendMessage(addresses, groupMessage); messageSender.sendMessage(addresses, groupMessage);

View File

@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; 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.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException; 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.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; 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.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -110,12 +116,14 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments()); List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments); List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient()); Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody()) .withBody(message.getBody())
.withAttachments(attachmentStreams) .withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull()) .withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.asExpirationUpdate(message.isExpirationUpdate()) .asExpirationUpdate(message.isExpirationUpdate())
.build(); .build();
@ -131,4 +139,5 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
throw new RetryLaterException(e); throw new RetryLaterException(e);
} }
} }
} }

View File

@ -14,17 +14,24 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; 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.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient; 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.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; 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 org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.LinkedList; import java.util.LinkedList;
@ -110,5 +117,45 @@ public abstract class PushSendJob extends SendJob {
} }
} }
protected Optional<SignalServiceDataMessage.Quote> 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<SignalServiceAttachment> 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; protected abstract void onPushSend() throws Exception;
} }

View File

@ -34,11 +34,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { 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) { 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) { public AudioSlide(Context context, Attachment attachment) {

View File

@ -19,7 +19,7 @@ public class DocumentSlide extends Slide {
@NonNull String contentType, long size, @NonNull String contentType, long size,
@Nullable String fileName) @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 @Override

View File

@ -14,7 +14,7 @@ public class GifSlide extends ImageSlide {
} }
public GifSlide(Context context, Uri uri, long size, int width, int height) { 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 @Override

View File

@ -37,7 +37,7 @@ public class ImageSlide extends Slide {
} }
public ImageSlide(Context context, Uri uri, long size, int width, int height) { 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 @Override

View File

@ -21,6 +21,7 @@ public class IncomingMediaMessage {
private final int subscriptionId; private final int subscriptionId;
private final long expiresIn; private final long expiresIn;
private final boolean expirationUpdate; private final boolean expirationUpdate;
private final QuoteModel quote;
private final List<Attachment> attachments = new LinkedList<>(); private final List<Attachment> attachments = new LinkedList<>();
@ -41,6 +42,7 @@ public class IncomingMediaMessage {
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate; this.expirationUpdate = expirationUpdate;
this.quote = null;
this.attachments.addAll(attachments); this.attachments.addAll(attachments);
} }
@ -53,7 +55,8 @@ public class IncomingMediaMessage {
Optional<String> relay, Optional<String> relay,
Optional<String> body, Optional<String> body,
Optional<SignalServiceGroup> group, Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments) Optional<List<SignalServiceAttachment>> attachments,
Optional<QuoteModel> quote)
{ {
this.push = true; this.push = true;
this.from = from; this.from = from;
@ -62,6 +65,7 @@ public class IncomingMediaMessage {
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn; this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate; this.expirationUpdate = expirationUpdate;
this.quote = quote.orNull();
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false)); if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false));
else this.groupId = null; else this.groupId = null;
@ -108,4 +112,8 @@ public class IncomingMediaMessage {
public boolean isGroupMessage() { public boolean isGroupMessage() {
return groupId != null; return groupId != null;
} }
public QuoteModel getQuote() {
return quote;
}
} }

View File

@ -10,7 +10,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) { public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis, super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn); ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null);
} }
@Override @Override

View File

@ -21,11 +21,12 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@NonNull String encodedGroupContext, @NonNull String encodedGroupContext,
@NonNull List<Attachment> avatar, @NonNull List<Attachment> avatar,
long sentTimeMillis, long sentTimeMillis,
long expiresIn) long expiresIn,
@Nullable QuoteModel quote)
throws IOException throws IOException
{ {
super(recipient, encodedGroupContext, avatar, sentTimeMillis, super(recipient, encodedGroupContext, avatar, sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn); ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
} }
@ -34,12 +35,13 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@NonNull GroupContext group, @NonNull GroupContext group,
@Nullable final Attachment avatar, @Nullable final Attachment avatar,
long sentTimeMillis, long sentTimeMillis,
long expireIn) long expireIn,
@Nullable QuoteModel quote)
{ {
super(recipient, Base64.encodeBytes(group.toByteArray()), super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}}, new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(), System.currentTimeMillis(),
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn); ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote);
this.group = group; this.group = group;
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
@ -16,11 +17,12 @@ public class OutgoingMediaMessage {
private final int distributionType; private final int distributionType;
private final int subscriptionId; private final int subscriptionId;
private final long expiresIn; private final long expiresIn;
private final QuoteModel outgoingQuote;
public OutgoingMediaMessage(Recipient recipient, String message, public OutgoingMediaMessage(Recipient recipient, String message,
List<Attachment> attachments, long sentTimeMillis, List<Attachment> attachments, long sentTimeMillis,
int subscriptionId, long expiresIn, int subscriptionId, long expiresIn,
int distributionType) int distributionType, @Nullable QuoteModel outgoingQuote)
{ {
this.recipient = recipient; this.recipient = recipient;
this.body = message; this.body = message;
@ -29,15 +31,16 @@ public class OutgoingMediaMessage {
this.attachments = attachments; this.attachments = attachments;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn; 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, this(recipient,
buildMessage(slideDeck, message), buildMessage(slideDeck, message),
slideDeck.asAttachments(), slideDeck.asAttachments(),
sentTimeMillis, subscriptionId, sentTimeMillis, subscriptionId,
expiresIn, distributionType); expiresIn, distributionType, outgoingQuote);
} }
public OutgoingMediaMessage(OutgoingMediaMessage that) { public OutgoingMediaMessage(OutgoingMediaMessage that) {
@ -48,6 +51,7 @@ public class OutgoingMediaMessage {
this.sentTimeMillis = that.sentTimeMillis; this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId; this.subscriptionId = that.subscriptionId;
this.expiresIn = that.expiresIn; this.expiresIn = that.expiresIn;
this.outgoingQuote = that.outgoingQuote;
} }
public Recipient getRecipient() { public Recipient getRecipient() {
@ -90,6 +94,10 @@ public class OutgoingMediaMessage {
return expiresIn; return expiresIn;
} }
public @Nullable QuoteModel getOutgoingQuote() {
return outgoingQuote;
}
private static String buildMessage(SlideDeck slideDeck, String message) { private static String buildMessage(SlideDeck slideDeck, String message) {
if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) { if (!TextUtils.isEmpty(message) && !TextUtils.isEmpty(slideDeck.getBody())) {
return slideDeck.getBody() + "\n\n" + message; return slideDeck.getBody() + "\n\n" + message;
@ -99,4 +107,5 @@ public class OutgoingMediaMessage {
return slideDeck.getBody(); return slideDeck.getBody();
} }
} }
} }

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -11,9 +13,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
List<Attachment> attachments, List<Attachment> attachments,
long sentTimeMillis, long sentTimeMillis,
int distributionType, 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) { public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {

View File

@ -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<Attachment> attachments;
public QuoteModel(long id, Address author, String text, @Nullable List<Attachment> 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<Attachment> getAttachments() {
return attachments;
}
}

View File

@ -137,7 +137,8 @@ public abstract class Slide {
int height, int height,
boolean hasThumbnail, boolean hasThumbnail,
@Nullable String fileName, @Nullable String fileName,
boolean voiceNote) boolean voiceNote,
boolean quote)
{ {
try { try {
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
@ -151,7 +152,8 @@ public abstract class Slide {
height, height,
fileName, fileName,
fastPreflightId, fastPreflightId,
voiceNote); voiceNote,
quote);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@ -31,14 +31,14 @@ public class SlideDeck {
private final List<Slide> slides = new LinkedList<>(); private final List<Slide> slides = new LinkedList<>();
public SlideDeck(Context context, List<Attachment> attachments) { public SlideDeck(@NonNull Context context, @NonNull List<? extends Attachment> attachments) {
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
Slide slide = MediaUtil.getSlideForAttachment(context, attachment); Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
if (slide != null) slides.add(slide); 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); Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
if (slide != null) slides.add(slide); if (slide != null) slides.add(slide);
} }

View File

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class VideoSlide extends Slide { public class VideoSlide extends Slide {
public VideoSlide(Context context, Uri uri, long dataSize) { 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) { public VideoSlide(Context context, Attachment attachment) {

View File

@ -75,7 +75,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
if (recipient.isGroupRecipient()) { if (recipient.isGroupRecipient()) {
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); 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); replyThreadId = MessageSender.send(context, reply, threadId, false, null);
} else { } else {
Log.w("AndroidAutoReplyReceiver", "Sending regular message "); Log.w("AndroidAutoReplyReceiver", "Sending regular message ");

View File

@ -68,7 +68,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
long expiresIn = recipient.getExpireMessages() * 1000L; long expiresIn = recipient.getExpireMessages() * 1000L;
if (recipient.isGroupRecipient()) { 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); threadId = MessageSender.send(context, reply, -1, false, null);
} else { } else {
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId); OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);

View File

@ -44,9 +44,19 @@ public class BitmapUtil {
private static final int MAX_COMPRESSION_ATTEMPTS = 5; private static final int MAX_COMPRESSION_ATTEMPTS = 5;
private static final int MIN_COMPRESSION_QUALITY_DECREASE = 5; private static final int MIN_COMPRESSION_QUALITY_DECREASE = 5;
@android.support.annotation.WorkerThread @WorkerThread
public static <T> ScaleResult createScaledBytes(Context context, T model, MediaConstraints constraints) public static <T> ScaleResult createScaledBytes(Context context, T model, MediaConstraints constraints)
throws BitmapDecodingException throws BitmapDecodingException
{
return createScaledBytes(context, model,
constraints.getImageMaxWidth(context),
constraints.getImageMaxHeight(context),
constraints.getImageMaxSize(context));
}
@WorkerThread
public static <T> ScaleResult createScaledBytes(Context context, T model, int maxImageWidth, int maxImageHeight, int maxImageSize)
throws BitmapDecodingException
{ {
try { try {
int quality = MAX_COMPRESSION_QUALITY; int quality = MAX_COMPRESSION_QUALITY;
@ -59,8 +69,7 @@ public class BitmapUtil {
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.downsample(DownsampleStrategy.AT_MOST) .downsample(DownsampleStrategy.AT_MOST)
.submit(constraints.getImageMaxWidth(context), .submit(maxImageWidth, maxImageHeight)
constraints.getImageMaxWidth(context))
.get(); .get();
if (scaledBitmap == null) { if (scaledBitmap == null) {
@ -76,14 +85,14 @@ public class BitmapUtil {
Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb"); Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb");
if (quality == MIN_COMPRESSION_QUALITY) break; 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) { if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) {
nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE; nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE;
} }
quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY); quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY);
} }
while (bytes.length > constraints.getImageMaxSize(context) && attempts++ < MAX_COMPRESSION_ATTEMPTS); while (bytes.length > maxImageSize && attempts++ < MAX_COMPRESSION_ATTEMPTS);
if (bytes.length > constraints.getImageMaxSize(context)) { if (bytes.length > maxImageSize) {
throw new BitmapDecodingException("Unable to scale image below: " + bytes.length); 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)"); Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)");

View File

@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.util;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -39,4 +42,30 @@ public class JsonUtils {
public static ObjectMapper getMapper() { public static ObjectMapper getMapper() {
return objectMapper; 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);
}
}
} }

View File

@ -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;
}
}

View File

@ -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();
// }
//}