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

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"?>
<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
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:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -27,77 +26,93 @@
android:paddingBottom="8dp"
android:background="@drawable/sent_bubble"
android:clipChildren="false"
android:clipToPadding="false">
android:clipToPadding="false"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
android:id="@+id/emoji_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
<org.thoughtcrime.securesms.components.ComposeText
style="@style/ComposeEditText"
android:id="@+id/embedded_text_editor"
android:layout_width="0dp"
<org.thoughtcrime.securesms.components.QuoteView
android:id="@+id/quote_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="37dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:nextFocusForward="@+id/send_button"
android:nextFocusRight="@+id/send_button"
tools:visibility="invisible"
tools:hint="Send TextSecure message" >
<requestFocus />
</org.thoughtcrime.securesms.components.ComposeText>
android:visibility="gone"
app:quote_dismissable="true"
tools:visibility="visible"/>
<org.thoughtcrime.securesms.components.HidingLinearLayout
android:id="@+id/quick_attachment_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<ImageButton
android:id="@+id/quick_camera_toggle"
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
android:id="@+id/emoji_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:src="?quick_camera_icon"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_camera_description"
android:padding="10dp"/>
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
android:id="@+id/recorder_view"
android:layout_width="37dp"
android:layout_height="37dp"
<org.thoughtcrime.securesms.components.ComposeText
style="@style/ComposeEditText"
android:id="@+id/embedded_text_editor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="37dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:nextFocusForward="@+id/send_button"
android:nextFocusRight="@+id/send_button"
tools:visibility="invisible"
tools:hint="Send TextSecure message" >
<requestFocus />
</org.thoughtcrime.securesms.components.ComposeText>
<org.thoughtcrime.securesms.components.HidingLinearLayout
android:id="@+id/quick_attachment_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<ImageButton
android:id="@+id/quick_audio_toggle"
android:id="@+id/quick_camera_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:src="?quick_mic_icon"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_record_and_send_audio_description"
android:src="?quick_camera_icon"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_camera_description"
android:padding="10dp"/>
<ImageView android:id="@+id/quick_audio_fab"
android:layout_width="74dp"
android:layout_height="74dp"
android:src="@drawable/ic_mic_white_48dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/red_400"
android:visibility="gone"
android:scaleType="center"/>
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
android:id="@+id/recorder_view"
android:layout_width="37dp"
android:layout_height="37dp"
android:clipChildren="false"
android:clipToPadding="false">
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
<ImageButton
android:id="@+id/quick_audio_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:src="?quick_mic_icon"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_record_and_send_audio_description"
android:padding="10dp"/>
</org.thoughtcrime.securesms.components.HidingLinearLayout>
<ImageView android:id="@+id/quick_audio_fab"
android:layout_width="74dp"
android:layout_height="74dp"
android:src="@drawable/ic_mic_white_48dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/red_400"
android:visibility="gone"
android:scaleType="center"/>
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
</org.thoughtcrime.securesms.components.HidingLinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/recording_container"
@ -174,4 +189,3 @@
</org.thoughtcrime.securesms.components.AnimatingToggle>
</org.thoughtcrime.securesms.components.InputPanel>
</merge>

View File

@ -75,6 +75,14 @@
</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
android:id="@+id/image_view_stub"
android:layout="@layout/conversation_item_received_thumbnail"

View File

@ -38,6 +38,14 @@
android:background="@drawable/sent_bubble"
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
android:id="@+id/image_view_stub"
android:layout_width="@dimen/media_bubble_default_dimens"

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:icon="?menu_save_icon"
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>

View File

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

View File

@ -23,6 +23,19 @@
<color name="gray78">#ff383838</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="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_forward_icon">@drawable/ic_forward_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_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_forward_icon">@drawable/ic_forward_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_video">@drawable/ic_video_dark</item>

View File

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

View File

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

View File

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

View File

@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.QuoteView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
@ -111,6 +113,7 @@ public class ConversationItem extends LinearLayout
private GlideRequests glideRequests;
protected View bodyBubble;
private QuoteView quoteView;
private TextView bodyText;
private TextView dateText;
private TextView simInfoText;
@ -173,6 +176,7 @@ public class ConversationItem extends LinearLayout
this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub));
this.expirationTimer = findViewById(R.id.expiration_indicator);
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
setOnClickListener(new ClickListener(null));
@ -210,6 +214,7 @@ public class ConversationItem extends LinearLayout
setMinimumWidth();
setSimInfo(messageRecord);
setExpiration(messageRecord);
setQuote(messageRecord);
}
@Override
@ -506,6 +511,17 @@ public class ConversationItem extends LinearLayout
}
}
private void setQuote(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms() && !messageRecord.isMmsNotification() && ((MediaMmsMessageRecord)messageRecord).getQuote() != null) {
Quote quote = ((MediaMmsMessageRecord)messageRecord).getQuote();
assert quote != null;
quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quote.getText(), quote.getAttachment());
quoteView.setVisibility(View.VISIBLE);
} else {
quoteView.dismiss();
}
}
private void setFailedStatusIcons() {
alertView.setFailed();
deliveryStatusIndicator.setNone();

View File

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

View File

@ -17,9 +17,9 @@ public class DatabaseAttachment extends Attachment {
String contentType, int transferProgress, long size,
String fileName, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote,
int width, int height)
int width, int height, boolean quote)
{
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height);
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;

View File

@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0);
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false);
}
@Nullable

View File

@ -15,12 +15,12 @@ import java.util.List;
public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, @NonNull String location,
@NonNull String key, @NonNull String relay,
@Nullable byte[] digest, boolean voiceNote,
@Nullable String fileName, @NonNull String location,
@Nullable String key, @NonNull String relay,
@Nullable byte[] digest, boolean voiceNote,
int width, int height)
{
super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height);
super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false);
}
@Nullable
@ -41,23 +41,36 @@ public class PointerAttachment extends Attachment {
if (pointers.isPresent()) {
for (SignalServiceAttachment pointer : pointers.get()) {
if (pointer.isPointer()) {
String encodedKey = Base64.encodeBytes(pointer.asPointer().getKey());
Optional<Attachment> result = forPointer(Optional.of(pointer));
results.add(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.asPointer().getSize().or(0),
pointer.asPointer().getFileName().orNull(),
String.valueOf(pointer.asPointer().getId()),
encodedKey, pointer.asPointer().getRelay().orNull(),
pointer.asPointer().getDigest().orNull(),
pointer.asPointer().getVoiceNote(),
pointer.asPointer().getWidth(),
pointer.asPointer().getHeight()));
if (result.isPresent()) {
results.add(result.get());
}
}
}
return results;
}
public static Optional<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;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
@Nullable String fileName, boolean voiceNote)
@Nullable String fileName, boolean voiceNote, boolean quote)
{
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote);
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote);
}
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId,
boolean voiceNote)
boolean voiceNote, boolean quote)
{
super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height);
super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}

View File

@ -23,12 +23,17 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@ -43,12 +48,13 @@ public class InputPanel extends LinearLayout
private static final int FADE_TIME = 150;
private EmojiToggle emojiToggle;
private ComposeText composeText;
private View quickCameraToggle;
private View quickAudioToggle;
private View buttonToggle;
private View recordingContainer;
private QuoteView quoteView;
private EmojiToggle emojiToggle;
private ComposeText composeText;
private View quickCameraToggle;
private View quickAudioToggle;
private View buttonToggle;
private View recordingContainer;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
@ -74,15 +80,18 @@ public class InputPanel extends LinearLayout
public void onFinishInflate() {
super.onFinishInflate();
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
this.composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
this.quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
this.quickAudioToggle = ViewUtil.findById(this, R.id.quick_audio_toggle);
this.buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
this.recordingContainer = ViewUtil.findById(this, R.id.recording_container);
this.recordTime = new RecordTime((TextView) ViewUtil.findById(this, R.id.record_time));
this.slideToCancel = new SlideToCancel(ViewUtil.findById(this, R.id.slide_to_cancel));
this.microphoneRecorderView = ViewUtil.findById(this, R.id.recorder_view);
View quoteDismiss = findViewById(R.id.quote_dismiss);
this.quoteView = findViewById(R.id.quote_view);
this.emojiToggle = findViewById(R.id.emoji_toggle);
this.composeText = findViewById(R.id.embedded_text_editor);
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
this.buttonToggle = findViewById(R.id.button_toggle);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordTime = new RecordTime(findViewById(R.id.record_time));
this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel));
this.microphoneRecorderView = findViewById(R.id.recorder_view);
this.microphoneRecorderView.setListener(this);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
@ -97,6 +106,8 @@ public class InputPanel extends LinearLayout
emojiToggle.setVisibility(View.VISIBLE);
emojiVisible = true;
}
quoteDismiss.setOnClickListener(v -> clearQuote());
}
public void setListener(final @NonNull Listener listener) {
@ -109,6 +120,23 @@ public class InputPanel extends LinearLayout
composeText.setMediaListener(listener);
}
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments) {
this.quoteView.setQuote(glideRequests, id, author, body, attachments);
this.quoteView.setVisibility(View.VISIBLE);
}
public void clearQuote() {
this.quoteView.dismiss();
}
public Optional<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) {
emojiToggle.attach(emojiDrawer);
}
@ -210,6 +238,7 @@ public class InputPanel extends LinearLayout
composeText.insertEmoji(emoji);
}
public interface Listener {
void onRecorderStarted();
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 org.json.JSONArray;
import org.json.JSONException;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@ -44,6 +46,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.StorageUtil;
@ -55,6 +58,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
@ -67,7 +71,7 @@ public class AttachmentDatabase extends Database {
public static final String TABLE_NAME = "part";
public static final String ROW_ID = "_id";
public static final String ATTACHMENT_ID_ALIAS = "attachment_id";
static final String ATTACHMENT_JSON_ALIAS = "attachment_json";
static final String MMS_ID = "mid";
static final String CONTENT_TYPE = "ct";
static final String NAME = "name";
@ -82,7 +86,8 @@ public class AttachmentDatabase extends Database {
public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note";
public static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
static final String QUOTE = "quote";
static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
public static final String DATA_RANDOM = "data_random";
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
static final String WIDTH = "width";
@ -97,12 +102,12 @@ public class AttachmentDatabase extends Database {
private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?";
private static final String[] PROJECTION = new String[] {ROW_ID + " AS " + ATTACHMENT_ID_ALIAS,
private static final String[] PROJECTION = new String[] {ROW_ID,
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE,
DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT};
QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT};
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " +
@ -114,7 +119,7 @@ public class AttachmentDatabase extends Database {
FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " +
WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);";
QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
@ -181,9 +186,15 @@ public class AttachmentDatabase extends Database {
try {
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor);
else return null;
if (cursor != null && cursor.moveToFirst()) {
List<DatabaseAttachment> list = getAttachment(cursor);
if (list != null && list.size() > 0) {
return list.get(0);
}
}
return null;
} finally {
if (cursor != null)
cursor.close();
@ -200,7 +211,7 @@ public class AttachmentDatabase extends Database {
null, null, null);
while (cursor != null && cursor.moveToNext()) {
results.add(getAttachment(cursor));
results.addAll(getAttachment(cursor));
}
return results;
@ -218,7 +229,7 @@ public class AttachmentDatabase extends Database {
try {
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
attachments.add(getAttachment(cursor));
attachments.addAll(getAttachment(cursor));
}
} finally {
if (cursor != null) cursor.close();
@ -327,15 +338,19 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
}
void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments)
void insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
throws MmsException
{
Log.w(TAG, "insertParts(" + attachments.size() + ")");
for (Attachment attachment : attachments) {
AttachmentId attachmentId = insertAttachment(mmsId, attachment);
AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote());
Log.w(TAG, "Inserted attachment at ID: " + attachmentId);
}
for (Attachment attachment : quoteAttachment) {
insertAttachment(mmsId, attachment, true);
}
}
public @NonNull Attachment updateAttachmentData(@NonNull Attachment attachment,
@ -376,7 +391,8 @@ public class AttachmentDatabase extends Database {
databaseAttachment.getFastPreflightId(),
databaseAttachment.isVoiceNote(),
mediaStream.getWidth(),
mediaStream.getHeight());
mediaStream.getHeight(),
databaseAttachment.isQuote());
}
@ -519,28 +535,68 @@ public class AttachmentDatabase extends Database {
}
}
DatabaseAttachment getAttachment(@NonNull Cursor cursor) {
return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)),
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
StorageUtil.getCleanFileName(cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME))),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)),
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)));
public List<DatabaseAttachment> getAttachment(@NonNull Cursor cursor) {
try {
if (cursor.getColumnIndex(AttachmentDatabase.ATTACHMENT_JSON_ALIAS) != -1) {
if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) {
return new LinkedList<>();
}
List<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(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)),
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
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
{
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(WIDTH, attachment.getWidth());
contentValues.put(HEIGHT, attachment.getHeight());
contentValues.put(QUOTE, quote);
if (dataInfo != null) {
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.database.helpers.SQLCipherOpenHelper;
import java.util.List;
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.THUMBNAIL_ASPECT_RATIO + ", "
+ 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.WIDTH + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
+ 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 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);
}
@ -89,11 +92,11 @@ public class MediaDatabase extends Database {
}
public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment attachment = attachmentDatabase.getAttachment(cursor);
String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)));
Address address = null;
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
List<DatabaseAttachment> attachments = attachmentDatabase.getAttachment(cursor);
String serializedAddress = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)));
Address address = null;
if (serializedAddress != null) {
address = Address.fromSerialized(serializedAddress);
@ -107,7 +110,7 @@ public class MediaDatabase extends Database {
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED));
}
return new MediaRecord(attachment, address, date, outgoing);
return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, address, date, outgoing);
}
public DatabaseAttachment getAttachment() {

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

View File

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

View File

@ -38,7 +38,7 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
while (cursor != null && cursor.moveToNext()) {
AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ATTACHMENT_ID_ALIAS)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)));
AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)));
Uri attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId);
if (attachmentUri.equals(uri)) {

View File

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

View File

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

View File

@ -55,7 +55,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
super(context, id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>(), subscriptionId,
0, 0, slideDeck, readReceiptCount);
0, 0, slideDeck, readReceiptCount, null);
this.contentLocation = contentLocation;
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) {
Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar);
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false);
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false);
}
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null);
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId);

View File

@ -212,7 +212,7 @@ public class GroupMessageProcessor {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
Recipient recipient = Recipient.from(context, addres, false);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, envelope.getTimestamp(), 0, null);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@ -17,14 +18,19 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
@ -105,19 +111,21 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
}
try {
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.asExpirationUpdate(message.isExpirationUpdate())
.build();
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
List<SignalServiceAttachment> attachmentStreams = getAttachmentsFor(scaledAttachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(attachmentStreams)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.asExpirationUpdate(message.isExpirationUpdate())
.build();
messageSender.sendMessage(address, mediaMessage);
} catch (UnregisteredUserException e) {
@ -131,4 +139,5 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
throw new RetryLaterException(e);
}
}
}

View File

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

View File

@ -34,11 +34,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote, false));
}
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote));
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false));
}
public AudioSlide(Context context, Attachment attachment) {

View File

@ -19,7 +19,7 @@ public class DocumentSlide extends Slide {
@NonNull String contentType, long size,
@Nullable String fileName)
{
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false));
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false, false));
}
@Override

View File

@ -14,7 +14,7 @@ public class GifSlide extends ImageSlide {
}
public GifSlide(Context context, Uri uri, long size, int width, int height) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false, false));
}
@Override

View File

@ -37,7 +37,7 @@ public class ImageSlide extends Slide {
}
public ImageSlide(Context context, Uri uri, long size, int width, int height) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false, false));
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.mms;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -11,9 +13,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
List<Attachment> attachments,
long sentTimeMillis,
int distributionType,
long expiresIn)
long expiresIn,
@Nullable QuoteModel quote)
{
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType);
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote);
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {

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

View File

@ -31,14 +31,14 @@ public class SlideDeck {
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) {
Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
if (slide != null) slides.add(slide);
}
}
public SlideDeck(Context context, Attachment attachment) {
public SlideDeck(@NonNull Context context, @NonNull Attachment attachment) {
Slide slide = MediaUtil.getSlideForAttachment(context, attachment);
if (slide != null) slides.add(slide);
}

View File

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class VideoSlide extends Slide {
public VideoSlide(Context context, Uri uri, long dataSize) {
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false));
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false, false));
}
public VideoSlide(Context context, Attachment attachment) {

View File

@ -75,7 +75,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
if (recipient.isGroupRecipient()) {
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0);
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null);
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
} else {
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");

View File

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

View File

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

View File

@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.util;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -39,4 +42,30 @@ public class JsonUtils {
public static ObjectMapper getMapper() {
return objectMapper;
}
public static class SaneJSONObject {
private final JSONObject delegate;
public SaneJSONObject(JSONObject delegate) {
this.delegate = delegate;
}
public String getString(String name) throws JSONException {
if (delegate.isNull(name)) return null;
else return delegate.getString(name);
}
public long getLong(String name) throws JSONException {
return delegate.getLong(name);
}
public boolean isNull(String name) {
return delegate.isNull(name);
}
public int getInt(String name) throws JSONException {
return delegate.getInt(name);
}
}
}

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