Support for receiving arbitrary attachment types

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-03-28 12:05:30 -07:00
parent c69efbffd2
commit f67eb5f9f3
60 changed files with 1251 additions and 423 deletions

View File

@ -59,7 +59,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2' compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7' compile 'org.whispersystems:libpastelog:1.0.7'
compile 'org.whispersystems:signal-service-android:2.5.3' compile 'org.whispersystems:signal-service-android:2.5.5'
compile 'org.whispersystems:webrtc-android:M57-S2' compile 'org.whispersystems:webrtc-android:M57-S2'
compile "me.leolin:ShortcutBadger:1.10-WS1" compile "me.leolin:ShortcutBadger:1.10-WS1"
@ -129,7 +129,7 @@ dependencyVerification {
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b', 'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88', 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
'org.whispersystems:signal-service-android:28a5368cb1336106ba7732aeaf0c5a33ef8fb22500c41f38ad8147375f59073b', 'org.whispersystems:signal-service-android:3d7859b194e518fbaf5a082daf22ca345411705e825791f751eb388f149583c3',
'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5', 'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
'me.leolin:ShortcutBadger:e8e39df8a59d8211a30f40b1eeab21b3fa57b3f3e0f03abb995f82d66588778c', 'me.leolin:ShortcutBadger:e8e39df8a59d8211a30f40b1eeab21b3fa57b3f3e0f03abb995f82d66588778c',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@ -165,7 +165,7 @@ dependencyVerification {
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d', 'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70', 'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19', 'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
'org.whispersystems:signal-service-java:969b4e1fb0b87e553d8b231a090002a03748e0444fa23afa1bc6f7065e8039ff', 'org.whispersystems:signal-service-java:4d51d423510bcc3f3a0db1a2c5c7164e379af7ad7f9c20cf0faa753eef9f3f27',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

View File

@ -41,6 +41,18 @@
app:foregroundTintColor="@color/grey_500" app:foregroundTintColor="@color/grey_500"
app:backgroundTintColor="?conversation_item_bubble_background"/> app:backgroundTintColor="?conversation_item_bubble_background"/>
<org.thoughtcrime.securesms.components.DocumentView
android:id="@+id/attachment_document"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingTop="15dp"
android:paddingBottom="15dp"
app:documentWidgetBackground="?conversation_item_bubble_background"
app:documentForegroundTintColor="@color/grey_500"
app:documentBackgroundTintColor="?conversation_item_bubble_background"/>
</org.thoughtcrime.securesms.components.RemovableEditableMediaView> </org.thoughtcrime.securesms.components.RemovableEditableMediaView>
</FrameLayout> </FrameLayout>

View File

@ -61,6 +61,11 @@
android:layout_width="210dp" android:layout_width="210dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<ViewStub android:id="@+id/document_view_stub"
android:layout="@layout/conversation_item_received_document"
android:layout_width="210dp"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.DocumentView
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/document_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:documentForegroundTintColor="@color/white"
app:documentBackgroundTintColor="@color/blue_500"
tools:visibility="visible"/>

View File

@ -50,6 +50,11 @@
android:layout_width="210dp" android:layout_width="210dp"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<ViewStub android:id="@+id/document_view_stub"
android:layout="@layout/conversation_item_sent_document"
android:layout_width="210dp"
android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.components.emoji.EmojiTextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/conversation_item_body" android:id="@+id/conversation_item_body"
android:autoLink="all" android:autoLink="all"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.DocumentView
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/document_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
app:documentForegroundTintColor="@color/grey_500"
app:documentBackgroundTintColor="@color/white"
android:visibility="gone"
tools:visibility="visible"/>

View File

@ -0,0 +1,105 @@
<?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"
tools:context="org.thoughtcrime.securesms.components.DocumentView">
<LinearLayout android:id="@+id/document_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/control_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
android:focusable="false"
android:gravity="center">
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/download_progress"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="gone"
android:clickable="false"
android:layout_gravity="center"
app:matProg_barColor="@color/white"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.333"
tools:visibility="gone"/>
<FrameLayout android:id="@+id/document_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:background="@drawable/ic_circle_fill_white_48dp"
android:visibility="visible"
android:clickable="false"
android:focusable="false"
tools:backgroundTint="@color/blue_400">
<TextView android:id="@+id/document"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:clickable="false"
android:visibility="visible"
android:background="@drawable/ic_insert_drive_file_white_24dp"
android:textAlignment="center"
android:scaleType="centerInside"
android:textAllCaps="true"
android:textSize="8sp"
android:paddingTop="8dp"
android:typeface="monospace"
tools:visibility="visible"
tools:text="PDF"
tools:textColor="@color/blue_400"/>
</FrameLayout>
<ImageView android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:clickable="true"
android:visibility="gone"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_download_circle_fill_white_48dp"
android:contentDescription="@string/audio_view__download_accessibility_description"/>
</org.thoughtcrime.securesms.components.AnimatingToggle>
<LinearLayout android:orientation="vertical"
android:layout_marginLeft="7dp"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:focusable="false"
android:clickable="false"
android:layout_height="wrap_content">
<TextView android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:singleLine="true"
android:maxLines="1"
android:clickable="false"
android:ellipsize="end"
tools:text="The-Anarchist-Tension-by-Alfredo-Bonanno.pdf"/>
<TextView android:id="@+id/file_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="12sp"
android:clickable="false"
tools:text="24kb"/>
</LinearLayout>
</LinearLayout>
</merge>

View File

@ -206,22 +206,26 @@
<item>image</item> <item>image</item>
<item>audio</item> <item>audio</item>
<item>video</item> <item>video</item>
<item>documents</item>
</string-array> </string-array>
<string-array name="pref_media_download_values"> <string-array name="pref_media_download_values">
<item>@string/arrays__images</item> <item>@string/arrays__images</item>
<item>@string/arrays__audio</item> <item>@string/arrays__audio</item>
<item>@string/arrays__video</item> <item>@string/arrays__video</item>
<item>@string/arrays__documents</item>
</string-array> </string-array>
<string-array name="pref_media_download_mobile_data_default"> <string-array name="pref_media_download_mobile_data_default">
<item>image</item> <item>image</item>
<item>audio</item>
</string-array> </string-array>
<string-array name="pref_media_download_wifi_default"> <string-array name="pref_media_download_wifi_default">
<item>image</item> <item>image</item>
<item>audio</item> <item>audio</item>
<item>video</item> <item>video</item>
<item>documents</item>
</string-array> </string-array>
<string-array name="pref_media_download_roaming_default" /> <string-array name="pref_media_download_roaming_default" />

View File

@ -197,4 +197,10 @@
<attr name="pickerColors" format="reference" /> <attr name="pickerColors" format="reference" />
</declare-styleable> </declare-styleable>
<declare-styleable name="DocumentView">
<attr name="documentWidgetBackground" format="color"/>
<attr name="documentForegroundTintColor" format="color" />
<attr name="documentBackgroundTintColor" format="color" />
</declare-styleable>
</resources> </resources>

View File

@ -1063,6 +1063,7 @@
<string name="arrays__images">Images</string> <string name="arrays__images">Images</string>
<string name="arrays__audio">Audio</string> <string name="arrays__audio">Audio</string>
<string name="arrays__video">Video</string> <string name="arrays__video">Video</string>
<string name="arrays__documents">Documents</string>
<!-- plurals.xml --> <!-- plurals.xml -->
<plurals name="hours_ago"> <plurals name="hours_ago">
@ -1343,6 +1344,8 @@
<!-- transport_selection_list_item --> <!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string> <string name="transport_selection_list_item__transport_icon">Transport icon</string>
<string name="SaveAttachmentTask_open_directory">Open Directory</string>
<string name="DocumentView_unknown_file">unknown file</string>
<!-- EOF --> <!-- EOF -->

View File

@ -83,6 +83,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5; private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5;
private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6; private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7;
private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8;
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>()); private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
@ -223,9 +225,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
switch (viewType) { switch (viewType) {
case MESSAGE_TYPE_AUDIO_OUTGOING: case MESSAGE_TYPE_AUDIO_OUTGOING:
case MESSAGE_TYPE_THUMBNAIL_OUTGOING: case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
case MESSAGE_TYPE_DOCUMENT_OUTGOING:
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent; case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
case MESSAGE_TYPE_AUDIO_INCOMING: case MESSAGE_TYPE_AUDIO_INCOMING:
case MESSAGE_TYPE_THUMBNAIL_INCOMING: case MESSAGE_TYPE_THUMBNAIL_INCOMING:
case MESSAGE_TYPE_DOCUMENT_INCOMING:
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received; case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update; case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
@ -242,6 +246,9 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
} else if (hasAudio(messageRecord)) { } else if (hasAudio(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING; if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
else return MESSAGE_TYPE_AUDIO_INCOMING; else return MESSAGE_TYPE_AUDIO_INCOMING;
} else if (hasDocument(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_DOCUMENT_OUTGOING;
else return MESSAGE_TYPE_DOCUMENT_INCOMING;
} else if (hasThumbnail(messageRecord)) { } else if (hasThumbnail(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING; if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING;
else return MESSAGE_TYPE_THUMBNAIL_INCOMING; else return MESSAGE_TYPE_THUMBNAIL_INCOMING;
@ -315,6 +322,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null; return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
} }
private boolean hasDocument(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
private boolean hasThumbnail(MessageRecord messageRecord) { private boolean hasThumbnail(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null; return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
} }

View File

@ -389,9 +389,9 @@ public class ConversationFragment extends Fragment
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() { SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
for (Slide slide : message.getSlideDeck().getSlides()) { for (Slide slide : message.getSlideDeck().getSlides()) {
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio()) && slide.getUri() != null) { if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) && slide.getUri() != null) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret); SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret, list);
saveTask.execute(new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived())); saveTask.execute(new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
return; return;
} }
} }

View File

@ -23,9 +23,11 @@ import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.util.Linkify; import android.text.util.Linkify;
@ -43,6 +45,7 @@ import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.DeliveryStatusView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.ExpirationTimerView; import org.thoughtcrime.securesms.components.ExpirationTimerView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -112,6 +115,7 @@ public class ConversationItem extends LinearLayout
private @Nullable Recipients conversationRecipients; private @Nullable Recipients conversationRecipients;
private @NonNull Stub<ThumbnailView> mediaThumbnailStub; private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
private @NonNull Stub<AudioView> audioViewStub; private @NonNull Stub<AudioView> audioViewStub;
private @NonNull Stub<DocumentView> documentViewStub;
private @NonNull ExpirationTimerView expirationTimer; private @NonNull ExpirationTimerView expirationTimer;
private int defaultBubbleColor; private int defaultBubbleColor;
@ -153,6 +157,7 @@ public class ConversationItem extends LinearLayout
this.bodyBubble = findViewById(R.id.body_bubble); this.bodyBubble = findViewById(R.id.body_bubble);
this.mediaThumbnailStub = new Stub<>((ViewStub) findViewById(R.id.image_view_stub)); this.mediaThumbnailStub = new Stub<>((ViewStub) findViewById(R.id.image_view_stub));
this.audioViewStub = new Stub<>((ViewStub) findViewById(R.id.audio_view_stub)); this.audioViewStub = new Stub<>((ViewStub) findViewById(R.id.audio_view_stub));
this.documentViewStub = new Stub<>((ViewStub) findViewById(R.id.document_view_stub));
this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator); this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator);
setOnClickListener(new ClickListener(null)); setOnClickListener(new ClickListener(null));
@ -229,6 +234,10 @@ public class ConversationItem extends LinearLayout
if (audioViewStub.resolved()) { if (audioViewStub.resolved()) {
setAudioViewTint(messageRecord, conversationRecipients); setAudioViewTint(messageRecord, conversationRecipients);
} }
if (documentViewStub.resolved()) {
setDocumentViewTint(messageRecord, conversationRecipients);
}
} }
private void setAudioViewTint(MessageRecord messageRecord, Recipients recipients) { private void setAudioViewTint(MessageRecord messageRecord, Recipients recipients) {
@ -243,6 +252,18 @@ public class ConversationItem extends LinearLayout
} }
} }
private void setDocumentViewTint(MessageRecord messageRecord, Recipients recipients) {
if (messageRecord.isOutgoing()) {
if (DynamicTheme.LIGHT.equals(TextSecurePreferences.getTheme(context))) {
documentViewStub.get().setTint(recipients.getColor().toConversationColor(context), defaultBubbleColor);
} else {
documentViewStub.get().setTint(Color.WHITE, defaultBubbleColor);
}
} else {
documentViewStub.get().setTint(Color.WHITE, recipients.getColor().toConversationColor(context));
}
}
private void setInteractionState(MessageRecord messageRecord) { private void setInteractionState(MessageRecord messageRecord) {
setSelected(batchSelected.contains(messageRecord)); setSelected(batchSelected.contains(messageRecord));
bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0); bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0);
@ -258,6 +279,11 @@ public class ConversationItem extends LinearLayout
audioViewStub.get().setClickable(batchSelected.isEmpty()); audioViewStub.get().setClickable(batchSelected.isEmpty());
audioViewStub.get().setEnabled(batchSelected.isEmpty()); audioViewStub.get().setEnabled(batchSelected.isEmpty());
} }
if (documentViewStub.resolved()) {
documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
documentViewStub.get().setClickable(batchSelected.isEmpty());
}
} }
private boolean isCaptionlessMms(MessageRecord messageRecord) { private boolean isCaptionlessMms(MessageRecord messageRecord) {
@ -272,6 +298,10 @@ public class ConversationItem extends LinearLayout
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null; return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
} }
private boolean hasDocument(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
}
private void setBodyText(MessageRecord messageRecord) { private void setBodyText(MessageRecord messageRecord) {
bodyText.setClickable(false); bodyText.setClickable(false);
bodyText.setFocusable(false); bodyText.setFocusable(false);
@ -290,6 +320,7 @@ public class ConversationItem extends LinearLayout
if (hasAudio(messageRecord)) { if (hasAudio(messageRecord)) {
audioViewStub.get().setVisibility(View.VISIBLE); audioViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
audioViewStub.get().setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls); audioViewStub.get().setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
@ -297,9 +328,22 @@ public class ConversationItem extends LinearLayout
audioViewStub.get().setOnLongClickListener(passthroughClickListener); audioViewStub.get().setOnLongClickListener(passthroughClickListener);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
} else if (hasDocument(messageRecord)) {
documentViewStub.get().setVisibility(View.VISIBLE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions
documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls);
documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
documentViewStub.get().setDownloadClickListener(downloadClickListener);
documentViewStub.get().setOnLongClickListener(passthroughClickListener);
bodyText.setLayoutParams(new ActionBar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
} else if (hasThumbnail(messageRecord)) { } else if (hasThumbnail(messageRecord)) {
mediaThumbnailStub.get().setVisibility(View.VISIBLE); mediaThumbnailStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
//noinspection ConstantConditions //noinspection ConstantConditions
mediaThumbnailStub.get().setImageResource(masterSecret, mediaThumbnailStub.get().setImageResource(masterSecret,
@ -314,6 +358,7 @@ public class ConversationItem extends LinearLayout
} else { } else {
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE); if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
} }
} }
@ -498,19 +543,6 @@ public class ConversationItem extends LinearLayout
} }
private class ThumbnailClickListener implements SlideClickListener { private class ThumbnailClickListener implements SlideClickListener {
private void fireIntent(Slide slide) {
Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
try {
context.startActivity(intent);
} catch (ActivityNotFoundException anfe) {
Log.w(TAG, "No activity existed to view the media.");
Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
}
}
public void onClick(final View v, final Slide slide) { public void onClick(final View v, final Slide slide) {
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
performClick(); performClick();
@ -525,18 +557,18 @@ public class ConversationItem extends LinearLayout
context.startActivity(intent); context.startActivity(intent);
} else if (slide.getUri() != null) { } else if (slide.getUri() != null) {
AlertDialog.Builder builder = new AlertDialog.Builder(context); Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
builder.setTitle(R.string.ConversationItem_view_secure_media_question); Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());
builder.setIconAttribute(R.attr.dialog_alert_icon); Log.w(TAG, "Public URI: " + publicUri);
builder.setCancelable(true); Intent intent = new Intent(Intent.ACTION_VIEW);
builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
public void onClick(DialogInterface dialog, int which) { try {
fireIntent(slide); context.startActivity(intent);
} } catch (ActivityNotFoundException anfe) {
}); Log.w(TAG, "No activity existed to view the media.");
builder.setNegativeButton(R.string.no, null); Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
builder.show(); }
} }
} }
} }
@ -554,6 +586,7 @@ public class ConversationItem extends LinearLayout
performClick(); performClick();
} }
} }
private class ClickListener implements View.OnClickListener { private class ClickListener implements View.OnClickListener {
private OnClickListener parent; private OnClickListener parent;

View File

@ -32,10 +32,10 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase.Reader; import org.thoughtcrime.securesms.database.MmsDatabase.Reader;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob; import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker; import org.thoughtcrime.securesms.util.VersionTracker;
@ -244,7 +243,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
private void schedulePendingIncomingParts(Context context) { private void schedulePendingIncomingParts(Context context) {
final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(context); final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(context);
final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context); final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context);
final List<DatabaseAttachment> pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(); final List<DatabaseAttachment> pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(masterSecret);
Log.w(TAG, pendingAttachments.size() + " pending parts."); Log.w(TAG, pendingAttachments.size() + " pending parts.");
for (DatabaseAttachment attachment : pendingAttachments) { for (DatabaseAttachment attachment : pendingAttachments) {

View File

@ -67,7 +67,7 @@ public class MediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
@Override @Override
public void onBindItemViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) { public void onBindItemViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) {
final ThumbnailView imageView = viewHolder.imageView; final ThumbnailView imageView = viewHolder.imageView;
final MediaRecord mediaRecord = MediaRecord.from(cursor); final MediaRecord mediaRecord = MediaRecord.from(getContext(), masterSecret, cursor);
Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());

View File

@ -166,10 +166,11 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
List<SaveAttachmentTask.Attachment> attachments = new ArrayList<>(cursor.getCount()); List<SaveAttachmentTask.Attachment> attachments = new ArrayList<>(cursor.getCount());
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
MediaRecord record = MediaRecord.from(cursor); MediaRecord record = MediaRecord.from(c, masterSecret, cursor);
attachments.add(new SaveAttachmentTask.Attachment(record.getAttachment().getDataUri(), attachments.add(new SaveAttachmentTask.Attachment(record.getAttachment().getDataUri(),
record.getContentType(), record.getContentType(),
record.getDate())); record.getDate(),
null));
} }
return attachments; return attachments;
@ -179,7 +180,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) { protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
super.onPostExecute(attachments); super.onPostExecute(attachments);
SaveAttachmentTask saveTask = new SaveAttachmentTask(c, masterSecret, attachments.size()); SaveAttachmentTask saveTask = new SaveAttachmentTask(c, masterSecret, gridView, attachments.size());
saveTask.execute(attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()])); saveTask.execute(attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
} }
}.execute(); }.execute();

View File

@ -207,9 +207,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
SaveAttachmentTask.showWarningDialog(this, new DialogInterface.OnClickListener() { SaveAttachmentTask.showWarningDialog(this, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialogInterface, int i) { public void onClick(DialogInterface dialogInterface, int i) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret); SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret, image);
long saveDate = (date > 0) ? date : System.currentTimeMillis(); long saveDate = (date > 0) ? date : System.currentTimeMillis();
saveTask.execute(new Attachment(mediaUri, mediaType, saveDate)); saveTask.execute(new Attachment(mediaUri, mediaType, saveDate, null));
} }
}); });
} }

View File

@ -13,6 +13,9 @@ public abstract class Attachment {
private final int transferState; private final int transferState;
private final long size; private final long size;
@Nullable
private final String fileName;
@Nullable @Nullable
private final String location; private final String location;
@ -25,13 +28,14 @@ public abstract class Attachment {
@Nullable @Nullable
private final byte[] digest; private final byte[] digest;
public Attachment(@NonNull String contentType, int transferState, long size, public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
@Nullable String location, @Nullable String key, @Nullable String relay, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest) @Nullable byte[] digest)
{ {
this.contentType = contentType; this.contentType = contentType;
this.transferState = transferState; this.transferState = transferState;
this.size = size; this.size = size;
this.fileName = fileName;
this.location = location; this.location = location;
this.key = key; this.key = key;
this.relay = relay; this.relay = relay;
@ -57,6 +61,11 @@ public abstract class Attachment {
return size; return size;
} }
@Nullable
public String getFileName() {
return fileName;
}
@NonNull @NonNull
public String getContentType() { public String getContentType() {
return contentType; return contentType;

View File

@ -15,9 +15,10 @@ public class DatabaseAttachment extends Attachment {
public DatabaseAttachment(AttachmentId attachmentId, long mmsId, public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
boolean hasData, boolean hasThumbnail, boolean hasData, boolean hasThumbnail,
String contentType, int transferProgress, long size, String contentType, int transferProgress, long size,
String location, String key, String relay, byte[] digest) String fileName, String location, String key, String relay,
byte[] digest)
{ {
super(contentType, transferProgress, size, location, key, relay, digest); super(contentType, transferProgress, size, fileName, location, key, relay, digest);
this.attachmentId = attachmentId; this.attachmentId = attachmentId;
this.hasData = hasData; this.hasData = hasData;
this.hasThumbnail = hasThumbnail; this.hasThumbnail = hasThumbnail;

View File

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

View File

@ -16,10 +16,11 @@ import java.util.List;
public class PointerAttachment extends Attachment { public class PointerAttachment extends Attachment {
public PointerAttachment(@NonNull String contentType, int transferState, long size, public PointerAttachment(@NonNull String contentType, int transferState, long size,
@NonNull String location, @NonNull String key, @NonNull String relay, @Nullable String fileName, @NonNull String location,
@NonNull String key, @NonNull String relay,
@Nullable byte[] digest) @Nullable byte[] digest)
{ {
super(contentType, transferState, size, location, key, relay, digest); super(contentType, transferState, size, fileName, location, key, relay, digest);
} }
@Nullable @Nullable
@ -45,6 +46,7 @@ public class PointerAttachment extends Attachment {
results.add(new PointerAttachment(pointer.getContentType(), results.add(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING, AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING,
pointer.asPointer().getSize().or(0), pointer.asPointer().getSize().or(0),
pointer.asPointer().getFileName().orNull(),
String.valueOf(pointer.asPointer().getId()), String.valueOf(pointer.asPointer().getId()),
encryptedKey, pointer.asPointer().getRelay().orNull(), encryptedKey, pointer.asPointer().getRelay().orNull(),
pointer.asPointer().getDigest().orNull())); pointer.asPointer().getDigest().orNull()));

View File

@ -9,14 +9,17 @@ public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri; private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri; private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size) { public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
this(uri, uri, contentType, transferState, size); @Nullable String fileName)
{
this(uri, uri, contentType, transferState, size, fileName);
} }
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size) @NonNull String contentType, int transferState, long size,
@Nullable String fileName)
{ {
super(contentType, transferState, size, null, null, null, null); super(contentType, transferState, size, fileName, null, null, null, null);
this.dataUri = dataUri; this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri; this.thumbnailUri = thumbnailUri;
} }

View File

@ -0,0 +1,196 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
public class DocumentView extends FrameLayout {
private static final String TAG = DocumentView.class.getSimpleName();
private final @NonNull AnimatingToggle controlToggle;
private final @NonNull ImageView downloadButton;
private final @NonNull ProgressWheel downloadProgress;
private final @NonNull View documentBackground;
private final @NonNull View container;
private final @NonNull TextView fileName;
private final @NonNull TextView fileSize;
private final @NonNull TextView document;
private @Nullable SlideClickListener downloadListener;
private @Nullable SlideClickListener viewListener;
private @Nullable DocumentSlide documentSlide;
public DocumentView(@NonNull Context context) {
this(context, null);
}
public DocumentView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DocumentView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.document_view, this);
this.container = findViewById(R.id.document_container);
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
this.downloadButton = (ImageView) findViewById(R.id.download);
this.downloadProgress = (ProgressWheel) findViewById(R.id.download_progress);
this.fileName = (TextView) findViewById(R.id.file_name);
this.fileSize = (TextView) findViewById(R.id.file_size);
this.documentBackground = findViewById(R.id.document_background);
this.document = (TextView) findViewById(R.id.document);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DocumentView, 0, 0);
setTint(typedArray.getColor(R.styleable.DocumentView_documentForegroundTintColor, Color.WHITE),
typedArray.getColor(R.styleable.DocumentView_documentBackgroundTintColor, Color.WHITE));
container.setBackgroundColor(typedArray.getColor(R.styleable.DocumentView_documentWidgetBackground, Color.TRANSPARENT));
typedArray.recycle();
}
}
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
this.downloadListener = listener;
}
public void setDocumentClickListener(@Nullable SlideClickListener listener) {
this.viewListener = listener;
}
public void setDocument(final @NonNull DocumentSlide documentSlide,
final boolean showControls)
{
if (showControls && documentSlide.isPendingDownload()) {
controlToggle.displayQuick(downloadButton);
downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide));
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
} else if (showControls && documentSlide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(downloadProgress);
downloadProgress.spin();
} else {
controlToggle.displayQuick(documentBackground);
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
}
this.documentSlide = documentSlide;
this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.DocumentView_unknown_file)));
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
this.document.setText(getFileType(documentSlide.getFileName()));
this.setOnClickListener(new OpenClickedListener(documentSlide));
}
public void setTint(int foregroundTint, int backgroundTint) {
DrawableCompat.setTint(this.document.getBackground(), backgroundTint);
DrawableCompat.setTint(this.documentBackground.getBackground(), foregroundTint);
this.document.setTextColor(foregroundTint);
this.fileName.setTextColor(foregroundTint);
this.fileSize.setTextColor(foregroundTint);
this.downloadButton.setColorFilter(foregroundTint);
this.downloadProgress.setBarColor(foregroundTint);
}
@Override
public void setFocusable(boolean focusable) {
super.setFocusable(focusable);
this.downloadButton.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
super.setClickable(clickable);
this.downloadButton.setClickable(clickable);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
this.downloadButton.setEnabled(enabled);
}
private @NonNull String getFileType(Optional<String> fileName) {
if (!fileName.isPresent()) return "";
String[] parts = fileName.get().split("\\.");
if (parts.length < 2) {
return "";
}
String suffix = parts[parts.length - 1];
if (suffix.length() <= 3) {
return suffix;
}
return "";
}
@Subscribe(sticky = true, threadMode = ThreadMode.ASYNC)
public void onEventAsync(final PartProgressEvent event) {
if (documentSlide != null && event.attachment.equals(this.documentSlide.asAttachment())) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
downloadProgress.setInstantProgress(((float) event.progress) / event.total);
}
});
}
}
private class DownloadClickedListener implements View.OnClickListener {
private final @NonNull DocumentSlide slide;
private DownloadClickedListener(@NonNull DocumentSlide slide) {
this.slide = slide;
}
@Override
public void onClick(View v) {
if (downloadListener != null) downloadListener.onClick(v, slide);
}
}
private class OpenClickedListener implements View.OnClickListener {
private final @NonNull DocumentSlide slide;
private OpenClickedListener(@NonNull DocumentSlide slide) {
this.slide = slide;
}
@Override
public void onClick(View v) {
if (!slide.isPendingDownload() && !slide.isInProgress() && viewListener != null) {
viewListener.onClick(v, slide);
}
}
}
}

View File

@ -16,213 +16,100 @@
*/ */
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import org.thoughtcrime.securesms.util.LimitedInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.lang.System;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.CipherInputStream;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import android.util.Log; public class DecryptingPartInputStream {
/**
* Class for streaming an encrypted MMS "part" off the disk.
*
* @author Moxie Marlinspike
*/
public class DecryptingPartInputStream extends FileInputStream {
private static final String TAG = DecryptingPartInputStream.class.getSimpleName(); private static final String TAG = DecryptingPartInputStream.class.getSimpleName();
private static final int IV_LENGTH = 16; private static final int IV_LENGTH = 16;
private static final int MAC_LENGTH = 20; private static final int MAC_LENGTH = 20;
private Cipher cipher; public static InputStream createFor(MasterSecret masterSecret, File file)
private Mac mac; throws IOException
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
public DecryptingPartInputStream(File file, MasterSecret masterSecret) throws FileNotFoundException {
super(file);
try {
if (file.length() <= IV_LENGTH + MAC_LENGTH)
throw new FileNotFoundException("Part shorter than crypto overhead!");
done = false;
mac = initializeMac(masterSecret.getMacKey());
cipher = initializeCipher(masterSecret.getEncryptionKey());
totalDataSize = file.length() - cipher.getBlockSize() - mac.getMacLength();
totalRead = 0;
} catch (InvalidKeyException ike) {
Log.w(TAG, ike);
throw new FileNotFoundException("Invalid key!");
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
throw new AssertionError(e);
} catch (IOException e) {
Log.w(TAG, e);
throw new FileNotFoundException("IOException while reading IV!");
}
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize)
return readIncremental(buffer, offset, length);
else if (!done)
return readFinal(buffer, offset, length);
else
return -1;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public long skip(long byteCount) throws IOException {
long skipped = 0L;
while (skipped < byteCount) {
byte[] buf = new byte[Math.min(4096, (int)(byteCount-skipped))];
int read = read(buf);
skipped += read;
}
return skipped;
}
private int readFinal(byte[] buffer, int offset, int length) throws IOException {
try {
int flourish = cipher.doFinal(buffer, offset);
//mac.update(buffer, offset, flourish);
byte[] ourMac = mac.doFinal();
byte[] theirMac = new byte[mac.getMacLength()];
readFully(theirMac);
if (!Arrays.equals(ourMac, theirMac))
throw new IOException("MAC doesn't match! Potential tampering?");
done = true;
return flourish;
} catch (IllegalBlockSizeException e) {
Log.w(TAG, e);
throw new IOException("Illegal block size exception!");
} catch (ShortBufferException e) {
Log.w(TAG, e);
throw new IOException("Short buffer exception!");
} catch (BadPaddingException e) {
Log.w(TAG, e);
throw new IOException("Bad padding exception!");
}
}
private int readIncremental(byte[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (null != overflowBuffer) {
if (overflowBuffer.length > length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(overflowBuffer, length, overflowBuffer.length);
return length;
} else if (overflowBuffer.length == length) {
System.arraycopy(overflowBuffer, 0, buffer, offset, length);
overflowBuffer = null;
return length;
} else {
System.arraycopy(overflowBuffer, 0, buffer, offset, overflowBuffer.length);
readLength += overflowBuffer.length;
offset += readLength;
length -= readLength;
overflowBuffer = null;
}
}
if (length + totalRead > totalDataSize)
length = (int)(totalDataSize - totalRead);
byte[] internalBuffer = new byte[length];
int read = super.read(internalBuffer, 0, internalBuffer.length <= cipher.getBlockSize() ? internalBuffer.length : internalBuffer.length - cipher.getBlockSize());
totalRead += read;
try {
mac.update(internalBuffer, 0, read);
int outputLen = cipher.getOutputSize(read);
if (outputLen <= length) {
readLength += cipher.update(internalBuffer, 0, read, buffer, offset);
return readLength;
}
byte[] transientBuffer = new byte[outputLen];
outputLen = cipher.update(internalBuffer, 0, read, transientBuffer, 0);
if (outputLen <= length) {
System.arraycopy(transientBuffer, 0, buffer, offset, outputLen);
readLength += outputLen;
} else {
System.arraycopy(transientBuffer, 0, buffer, offset, length);
overflowBuffer = Arrays.copyOfRange(transientBuffer, length, outputLen);
readLength += length;
}
return readLength;
} catch (ShortBufferException e) {
throw new AssertionError(e);
}
}
private Mac initializeMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
}
private Cipher initializeCipher(SecretKeySpec key)
throws InvalidKeyException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException, IOException
{ {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); try {
IvParameterSpec iv = readIv(cipher.getBlockSize()); if (file.length() <= IV_LENGTH + MAC_LENGTH) {
cipher.init(Cipher.DECRYPT_MODE, key, iv); throw new IOException("File too short");
}
return cipher; verifyMac(masterSecret, file);
FileInputStream fileStream = new FileInputStream(file);
byte[] ivBytes = new byte[IV_LENGTH];
readFully(fileStream, ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, masterSecret.getEncryptionKey(), iv);
return new CipherInputStream(new LimitedInputStream(fileStream, file.length() - MAC_LENGTH - IV_LENGTH), cipher);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
} }
private IvParameterSpec readIv(int size) throws IOException { private static void verifyMac(MasterSecret masterSecret, File file) throws IOException {
byte[] iv = new byte[size]; Mac mac = initializeMac(masterSecret.getMacKey());
readFully(iv); FileInputStream macStream = new FileInputStream(file);
InputStream dataStream = new LimitedInputStream(new FileInputStream(file), file.length() - MAC_LENGTH);
byte[] theirMac = new byte[MAC_LENGTH];
mac.update(iv); if (macStream.skip(file.length() - MAC_LENGTH) != file.length() - MAC_LENGTH) {
return new IvParameterSpec(iv); throw new IOException("Unable to seek");
}
readFully(macStream, theirMac);
byte[] buffer = new byte[4096];
int read;
while ((read = dataStream.read(buffer)) != -1) {
mac.update(buffer, 0, read);
}
byte[] ourMac = mac.doFinal();
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new IOException("Bad MAC");
}
macStream.close();
dataStream.close();
} }
private void readFully(byte[] buffer) throws IOException { private static Mac initializeMac(SecretKeySpec key) {
try {
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0; int offset = 0;
for (;;) { for (;;) {
int read = super.read(buffer, offset, buffer.length-offset); int read = in.read(buffer, offset, buffer.length-offset);
if (read + offset < buffer.length) offset += read; if (read + offset < buffer.length) offset += read;
else return; else return;

View File

@ -36,8 +36,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUnion; import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.mms.MediaStream;
@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
import org.whispersystems.libsignal.InvalidMessageException;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -76,6 +79,7 @@ public class AttachmentDatabase extends Database {
static final String DATA = "_data"; static final String DATA = "_data";
static final String TRANSFER_STATE = "pending_push"; static final String TRANSFER_STATE = "pending_push";
static final String SIZE = "data_size"; static final String SIZE = "data_size";
static final String FILE_NAME = "file_name";
static final String THUMBNAIL = "thumbnail"; static final String THUMBNAIL = "thumbnail";
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio"; static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
static final String UNIQUE_ID = "unique_id"; static final String UNIQUE_ID = "unique_id";
@ -91,7 +95,7 @@ public class AttachmentDatabase extends Database {
private static final String[] PROJECTION = new String[] {ROW_ID + " AS " + ATTACHMENT_ID_ALIAS, private static final String[] PROJECTION = new String[] {ROW_ID + " AS " + ATTACHMENT_ID_ALIAS,
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
SIZE, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
UNIQUE_ID, DIGEST}; UNIQUE_ID, DIGEST};
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
@ -101,8 +105,8 @@ public class AttachmentDatabase extends Database {
CONTENT_LOCATION + " TEXT, " + "ctt_s" + " INTEGER, " + CONTENT_LOCATION + " TEXT, " + "ctt_s" + " INTEGER, " +
"ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " + "ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " +
TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " +
THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL, " + FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
DIGEST + " BLOB);"; UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB);";
public static final String[] CREATE_INDEXS = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
@ -158,14 +162,15 @@ public class AttachmentDatabase extends Database {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId)); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
} }
public @Nullable DatabaseAttachment getAttachment(AttachmentId attachmentId) { public @Nullable DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, AttachmentId attachmentId)
{
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor); if (cursor != null && cursor.moveToFirst()) return getAttachment(masterSecret, cursor);
else return null; else return null;
} finally { } finally {
@ -174,7 +179,7 @@ public class AttachmentDatabase extends Database {
} }
} }
public @NonNull List<DatabaseAttachment> getAttachmentsForMessage(long mmsId) { public @NonNull List<DatabaseAttachment> getAttachmentsForMessage(@Nullable MasterSecret masterSecret, long mmsId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<DatabaseAttachment> results = new LinkedList<>(); List<DatabaseAttachment> results = new LinkedList<>();
Cursor cursor = null; Cursor cursor = null;
@ -184,7 +189,7 @@ public class AttachmentDatabase extends Database {
null, null, null); null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
results.add(getAttachment(cursor)); results.add(getAttachment(masterSecret, cursor));
} }
return results; return results;
@ -194,7 +199,7 @@ public class AttachmentDatabase extends Database {
} }
} }
public @NonNull List<DatabaseAttachment> getPendingAttachments() { public @NonNull List<DatabaseAttachment> getPendingAttachments(@NonNull MasterSecret masterSecret) {
final SQLiteDatabase database = databaseHelper.getReadableDatabase(); final SQLiteDatabase database = databaseHelper.getReadableDatabase();
final List<DatabaseAttachment> attachments = new LinkedList<>(); final List<DatabaseAttachment> attachments = new LinkedList<>();
@ -202,7 +207,7 @@ public class AttachmentDatabase extends Database {
try { try {
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
attachments.add(getAttachment(cursor)); attachments.add(getAttachment(masterSecret, cursor));
} }
} finally { } finally {
if (cursor != null) cursor.close(); if (cursor != null) cursor.close();
@ -282,7 +287,6 @@ public class AttachmentDatabase extends Database {
return partData.second; return partData.second;
} }
void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret, void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret,
long mmsId, long mmsId,
@NonNull List<Attachment> attachments) @NonNull List<Attachment> attachments)
@ -324,6 +328,7 @@ public class AttachmentDatabase extends Database {
mediaStream.getMimeType(), mediaStream.getMimeType(),
databaseAttachment.getTransferState(), databaseAttachment.getTransferState(),
dataSize, dataSize,
databaseAttachment.getFileName(),
databaseAttachment.getLocation(), databaseAttachment.getLocation(),
databaseAttachment.getKey(), databaseAttachment.getKey(),
databaseAttachment.getRelay(), databaseAttachment.getRelay(),
@ -331,6 +336,22 @@ public class AttachmentDatabase extends Database {
} }
public void updateAttachmentFileName(@NonNull MasterSecret masterSecret,
@NonNull AttachmentId attachmentId,
@Nullable String fileName)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
if (fileName != null) {
fileName = new MasterCipher(masterSecret).encryptBody(fileName);
}
ContentValues contentValues = new ContentValues(1);
contentValues.put(FILE_NAME, fileName);
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings());
}
public void markAttachmentUploaded(long messageId, Attachment attachment) { public void markAttachmentUploaded(long messageId, Attachment attachment) {
ContentValues values = new ContentValues(1); ContentValues values = new ContentValues(1);
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -365,9 +386,9 @@ public class AttachmentDatabase extends Database {
File dataFile = getAttachmentDataFile(attachmentId, dataType); File dataFile = getAttachmentDataFile(attachmentId, dataType);
try { try {
if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret); if (dataFile != null) return DecryptingPartInputStream.createFor(masterSecret, dataFile);
else return null; else return null;
} catch (FileNotFoundException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
} }
@ -438,7 +459,18 @@ public class AttachmentDatabase extends Database {
} }
} }
DatabaseAttachment getAttachment(Cursor cursor) { DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, Cursor cursor) {
String encryptedFileName = cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME));
String fileName = null;
if (masterSecret != null && !TextUtils.isEmpty(encryptedFileName)) {
try {
fileName = new MasterCipher(masterSecret).decryptBody(encryptedFileName);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
}
}
return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)), return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)),
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
@ -447,6 +479,7 @@ public class AttachmentDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
fileName,
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)), cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
@ -462,12 +495,17 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = null; Pair<File, Long> partData = null;
long uniqueId = System.currentTimeMillis(); long uniqueId = System.currentTimeMillis();
String fileName = null;
if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) { if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) {
partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri()); partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri());
Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath()); Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath());
} }
if (masterSecret.getMasterSecret().isPresent() && !TextUtils.isEmpty(attachment.getFileName())) {
fileName = new MasterCipher(masterSecret.getMasterSecret().get()).encryptBody(attachment.getFileName());
}
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(MMS_ID, mmsId); contentValues.put(MMS_ID, mmsId);
contentValues.put(CONTENT_TYPE, attachment.getContentType()); contentValues.put(CONTENT_TYPE, attachment.getContentType());
@ -477,6 +515,8 @@ public class AttachmentDatabase extends Database {
contentValues.put(DIGEST, attachment.getDigest()); contentValues.put(DIGEST, attachment.getDigest());
contentValues.put(CONTENT_DISPOSITION, attachment.getKey()); contentValues.put(CONTENT_DISPOSITION, attachment.getKey());
contentValues.put(NAME, attachment.getRelay()); contentValues.put(NAME, attachment.getRelay());
contentValues.put(FILE_NAME, fileName);
contentValues.put(SIZE, attachment.getSize());
if (partData != null) { if (partData != null) {
contentValues.put(DATA, partData.first.getAbsolutePath()); contentValues.put(DATA, partData.first.getAbsolutePath());
@ -543,7 +583,7 @@ public class AttachmentDatabase extends Database {
return stream; return stream;
} }
DatabaseAttachment attachment = getAttachment(attachmentId); DatabaseAttachment attachment = getAttachment(masterSecret, attachmentId);
if (attachment == null || !attachment.hasData()) { if (attachment == null || !attachment.hasData()) {
return null; return null;

View File

@ -76,7 +76,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_LAST_SEEN = 29; private static final int INTRODUCED_LAST_SEEN = 29;
private static final int INTRODUCED_DIGEST = 30; private static final int INTRODUCED_DIGEST = 30;
private static final int INTRODUCED_NOTIFIED = 31; private static final int INTRODUCED_NOTIFIED = 31;
private static final int DATABASE_VERSION = 31; private static final int INTRODUCED_DOCUMENTS = 32;
private static final int DATABASE_VERSION = 32;
private static final String DATABASE_NAME = "messages.db"; private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -388,7 +389,7 @@ public class DatabaseFactory {
InputStream is; InputStream is;
if (encrypted) is = new DecryptingPartInputStream(dataFile, masterSecret); if (encrypted) is = DecryptingPartInputStream.createFor(masterSecret, dataFile);
else is = new FileInputStream(dataFile); else is = new FileInputStream(dataFile);
body = (body == null) ? Util.readFullyAsString(is) : body + " " + Util.readFullyAsString(is); body = (body == null) ? Util.readFullyAsString(is) : body + " " + Util.readFullyAsString(is);
@ -853,6 +854,10 @@ public class DatabaseFactory {
db.execSQL("CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON mms(read,notified,thread_id)"); db.execSQL("CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON mms(read,notified,thread_id)");
} }
if (oldVersion < INTRODUCED_DOCUMENTS) {
db.execSQL("ALTER TABLE part ADD COLUMN file_name TEXT");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }

View File

@ -4,22 +4,30 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
public class MediaDatabase extends Database { public class MediaDatabase extends Database {
private final static String MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " private final static String MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ATTACHMENT_ID_ALIAS + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", "
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", "
@ -46,35 +54,21 @@ public class MediaDatabase extends Database {
} }
public static class MediaRecord { public static class MediaRecord {
private final AttachmentId attachmentId;
private final long mmsId;
private final boolean hasData;
private final boolean hasThumbnail;
private final String contentType;
private final String address;
private final long date;
private final int transferState;
private final long size;
private MediaRecord(AttachmentId attachmentId, long mmsId, private final DatabaseAttachment attachment;
boolean hasData, boolean hasThumbnail, private final String address;
String contentType, String address, long date, private final long date;
int transferState, long size)
{ private MediaRecord(DatabaseAttachment attachment, String address, long date) {
this.attachmentId = attachmentId; this.attachment = attachment;
this.mmsId = mmsId; this.address = address;
this.hasData = hasData; this.date = date;
this.hasThumbnail = hasThumbnail;
this.contentType = contentType;
this.address = address;
this.date = date;
this.transferState = transferState;
this.size = size;
} }
public static MediaRecord from(Cursor cursor) { public static MediaRecord from(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Cursor cursor) {
AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)), AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))); DatabaseAttachment attachment = attachmentDatabase.getAttachment(masterSecret, cursor);
String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
long date; long date;
@ -84,23 +78,15 @@ public class MediaDatabase extends Database {
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED)); date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED));
} }
return new MediaRecord(attachmentId, return new MediaRecord(attachment, address, date);
cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA)),
!cursor.isNull(cursor.getColumnIndexOrThrow(AttachmentDatabase.THUMBNAIL)),
cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.CONTENT_TYPE)),
cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)),
date,
cursor.getInt(cursor.getColumnIndexOrThrow(AttachmentDatabase.TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE)));
} }
public Attachment getAttachment() { public Attachment getAttachment() {
return new DatabaseAttachment(attachmentId, mmsId, hasData, hasThumbnail, contentType, transferState, size, null, null, null, null); return attachment;
} }
public String getContentType() { public String getContentType() {
return contentType; return attachment.getContentType();
} }
public String getAddress() { public String getAddress() {

View File

@ -138,6 +138,7 @@ public class MmsDatabase extends MessagingDatabase {
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE, AttachmentDatabase.SIZE,
AttachmentDatabase.FILE_NAME,
AttachmentDatabase.DATA, AttachmentDatabase.DATA,
AttachmentDatabase.THUMBNAIL, AttachmentDatabase.THUMBNAIL,
AttachmentDatabase.CONTENT_TYPE, AttachmentDatabase.CONTENT_TYPE,
@ -630,7 +631,7 @@ public class MmsDatabase extends MessagingDatabase {
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID)); int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)); long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(messageId)); List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(masterSecret, messageId));
MmsAddresses addresses = addr.getAddressesForId(messageId); MmsAddresses addresses = addr.getAddressesForId(messageId);
List<String> destinations = new LinkedList<>(); List<String> destinations = new LinkedList<>();
String body = getDecryptedBody(masterSecret, messageText, outboxType); String body = getDecryptedBody(masterSecret, messageText, outboxType);
@ -689,6 +690,7 @@ public class MmsDatabase extends MessagingDatabase {
databaseAttachment.getContentType(), databaseAttachment.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_DONE, AttachmentDatabase.TRANSFER_PROGRESS_DONE,
databaseAttachment.getSize(), databaseAttachment.getSize(),
databaseAttachment.getFileName(),
databaseAttachment.getLocation(), databaseAttachment.getLocation(),
databaseAttachment.getKey(), databaseAttachment.getKey(),
databaseAttachment.getRelay(), databaseAttachment.getRelay(),
@ -1267,7 +1269,7 @@ public class MmsDatabase extends MessagingDatabase {
} }
private SlideDeck getSlideDeck(@NonNull Cursor cursor) { private SlideDeck getSlideDeck(@NonNull Cursor cursor) {
Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor); Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, cursor);
return new SlideDeck(context, attachment); return new SlideDeck(context, attachment);
} }

View File

@ -25,6 +25,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -63,6 +64,7 @@ public class MmsSmsDatabase extends Database {
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE, AttachmentDatabase.SIZE,
AttachmentDatabase.FILE_NAME,
AttachmentDatabase.DATA, AttachmentDatabase.DATA,
AttachmentDatabase.THUMBNAIL, AttachmentDatabase.THUMBNAIL,
AttachmentDatabase.CONTENT_TYPE, AttachmentDatabase.CONTENT_TYPE,
@ -157,6 +159,7 @@ public class MmsSmsDatabase extends Database {
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE, AttachmentDatabase.SIZE,
AttachmentDatabase.FILE_NAME,
AttachmentDatabase.DATA, AttachmentDatabase.DATA,
AttachmentDatabase.THUMBNAIL, AttachmentDatabase.THUMBNAIL,
AttachmentDatabase.CONTENT_TYPE, AttachmentDatabase.CONTENT_TYPE,
@ -185,6 +188,7 @@ public class MmsSmsDatabase extends Database {
AttachmentDatabase.UNIQUE_ID, AttachmentDatabase.UNIQUE_ID,
AttachmentDatabase.MMS_ID, AttachmentDatabase.MMS_ID,
AttachmentDatabase.SIZE, AttachmentDatabase.SIZE,
AttachmentDatabase.FILE_NAME,
AttachmentDatabase.DATA, AttachmentDatabase.DATA,
AttachmentDatabase.THUMBNAIL, AttachmentDatabase.THUMBNAIL,
AttachmentDatabase.CONTENT_TYPE, AttachmentDatabase.CONTENT_TYPE,
@ -239,6 +243,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID); mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID);
mmsColumnsPresent.add(AttachmentDatabase.MMS_ID); mmsColumnsPresent.add(AttachmentDatabase.MMS_ID);
mmsColumnsPresent.add(AttachmentDatabase.SIZE); mmsColumnsPresent.add(AttachmentDatabase.SIZE);
mmsColumnsPresent.add(AttachmentDatabase.FILE_NAME);
mmsColumnsPresent.add(AttachmentDatabase.DATA); mmsColumnsPresent.add(AttachmentDatabase.DATA);
mmsColumnsPresent.add(AttachmentDatabase.THUMBNAIL); mmsColumnsPresent.add(AttachmentDatabase.THUMBNAIL);
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_TYPE); mmsColumnsPresent.add(AttachmentDatabase.CONTENT_TYPE);

View File

@ -104,7 +104,7 @@ public class GroupManager {
if (avatar != null) { if (avatar != null) {
Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar); Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar);
avatarAttachment = new UriAttachment(avatarUri, ContentType.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length); avatarAttachment = new UriAttachment(avatarUri, ContentType.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null);
} }
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0);

View File

@ -70,7 +70,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
@Override @Override
public void onRun(MasterSecret masterSecret) throws IOException { public void onRun(MasterSecret masterSecret) throws IOException {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(attachmentId); final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, attachmentId);
if (attachment == null) { if (attachment == null) {
Log.w(TAG, "attachment no longer exists."); Log.w(TAG, "attachment no longer exists.");
@ -158,7 +158,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
Log.w(TAG, "Downloading attachment with no digest..."); Log.w(TAG, "Downloading attachment with no digest...");
} }
return new SignalServiceAttachmentPointer(id, null, key, relay, Optional.fromNullable(attachment.getDigest())); return new SignalServiceAttachmentPointer(id, null, key, relay, Optional.fromNullable(attachment.getDigest()), Optional.fromNullable(attachment.getFileName()));
} catch (InvalidMessageException | IOException e) { } catch (InvalidMessageException | IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
throw new InvalidPartException(e); throw new InvalidPartException(e);

View File

@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.InvalidMessageException;
import java.io.IOException;
import java.util.Arrays;
public class AttachmentFileNameJob extends MasterSecretJob {
private static final long serialVersionUID = 1L;
private final long attachmentRowId;
private final long attachmentUniqueId;
private final String encryptedFileName;
public AttachmentFileNameJob(@NonNull Context context, @NonNull AsymmetricMasterSecret asymmetricMasterSecret,
@NonNull DatabaseAttachment attachment, @NonNull IncomingMediaMessage message)
{
super(context, new JobParameters.Builder().withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.attachmentRowId = attachment.getAttachmentId().getRowId();
this.attachmentUniqueId = attachment.getAttachmentId().getUniqueId();
this.encryptedFileName = getEncryptedFileName(asymmetricMasterSecret, attachment, message);
}
@Override
public void onRun(MasterSecret masterSecret) throws IOException, InvalidMessageException {
if (encryptedFileName == null) return;
AttachmentId attachmentId = new AttachmentId(attachmentRowId, attachmentUniqueId);
String plaintextFileName = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret)).decryptBody(encryptedFileName);
DatabaseFactory.getAttachmentDatabase(context).updateAttachmentFileName(masterSecret, attachmentId, plaintextFileName);
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onAdded() {
}
@Override
public void onCanceled() {
}
private @Nullable String getEncryptedFileName(@NonNull AsymmetricMasterSecret asymmetricMasterSecret,
@NonNull DatabaseAttachment attachment,
@NonNull IncomingMediaMessage mediaMessage)
{
for (Attachment messageAttachment : mediaMessage.getAttachments()) {
if (mediaMessage.getAttachments().size() == 1 ||
(messageAttachment.getDigest() != null && Arrays.equals(messageAttachment.getDigest(), attachment.getDigest())))
{
if (messageAttachment.getFileName() == null) return null;
else return new AsymmetricMasterCipher(asymmetricMasterSecret).encryptBody(messageAttachment.getFileName());
}
}
return null;
}
}

View File

@ -65,6 +65,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
byte[] key = record.getAvatarKey(); byte[] key = record.getAvatarKey();
String relay = record.getRelay(); String relay = record.getRelay();
Optional<byte[]> digest = Optional.fromNullable(record.getAvatarDigest()); Optional<byte[]> digest = Optional.fromNullable(record.getAvatarDigest());
Optional<String> fileName = Optional.absent();
if (avatarId == -1 || key == null) { if (avatarId == -1 || key == null) {
return; return;
@ -77,7 +78,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir()); attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
attachment.deleteOnExit(); attachment.deleteOnExit();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest); SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest, fileName);
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500);

View File

@ -185,10 +185,14 @@ public class MmsDownloadJob extends MasterSecretJob {
PduPart part = media.getPart(i); PduPart part = media.getPart(i);
if (part.getData() != null) { if (part.getData() != null) {
Uri uri = provider.createUri(part.getData()); Uri uri = provider.createUri(part.getData());
String name = null;
if (part.getName() != null) name = Util.toIsoString(part.getName());
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE, AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length)); part.getData().length, name));
} }
} }
} }

View File

@ -7,6 +7,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
@ -76,6 +77,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -483,13 +485,19 @@ public class PushDecryptJob extends ContextJob {
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, insertResult.get().getMessageId());
for (DatabaseAttachment attachment : attachments) { for (DatabaseAttachment attachment : attachments) {
ApplicationContext.getInstance(context) ApplicationContext.getInstance(context)
.getJobManager() .getJobManager()
.add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(), .add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(),
attachment.getAttachmentId())); attachment.getAttachmentId()));
if (!masterSecret.getMasterSecret().isPresent()) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentFileNameJob(context, masterSecret.getAsymmetricMasterSecret().get(), attachment, mediaMessage));
}
} }
if (smsMessageId.isPresent()) { if (smsMessageId.isPresent()) {
@ -550,7 +558,7 @@ public class PushDecryptJob extends ContextJob {
database.markAsSent(messageId, true); database.markAsSent(messageId, true);
for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) { for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, messageId)) {
ApplicationContext.getInstance(context) ApplicationContext.getInstance(context)
.getJobManager() .getJobManager()
.add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId())); .add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId()));

View File

@ -74,27 +74,23 @@ public abstract class PushSendJob extends SendJob {
List<SignalServiceAttachment> attachments = new LinkedList<>(); List<SignalServiceAttachment> attachments = new LinkedList<>();
for (final Attachment attachment : parts) { for (final Attachment attachment : parts) {
if (ContentType.isImageType(attachment.getContentType()) || try {
ContentType.isAudioType(attachment.getContentType()) || if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
ContentType.isVideoType(attachment.getContentType())) InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
{ attachments.add(SignalServiceAttachment.newStreamBuilder()
try { .withStream(is)
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); .withContentType(attachment.getContentType())
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri()); .withLength(attachment.getSize())
attachments.add(SignalServiceAttachment.newStreamBuilder() .withFileName(attachment.getFileName())
.withStream(is) .withListener(new ProgressListener() {
.withContentType(attachment.getContentType()) @Override
.withLength(attachment.getSize()) public void onAttachmentProgress(long total, long progress) {
.withListener(new ProgressListener() { EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
@Override }
public void onAttachmentProgress(long total, long progress) { })
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)); .build());
} } catch (IOException ioe) {
}) Log.w(TAG, "Couldn't open attachment", ioe);
.build());
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
} }
} }

View File

@ -19,6 +19,8 @@ import org.whispersystems.jobqueue.requirements.Requirement;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import ws.com.google.android.mms.ContentType;
public class MediaNetworkRequirement implements Requirement, ContextDependent { public class MediaNetworkRequirement implements Requirement, ContextDependent {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
private static final String TAG = MediaNetworkRequirement.class.getSimpleName(); private static final String TAG = MediaNetworkRequirement.class.getSimpleName();
@ -76,7 +78,7 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
public boolean isPresent() { public boolean isPresent() {
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context); final AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context);
final Attachment attachment = db.getAttachment(attachmentId); final Attachment attachment = db.getAttachment(null, attachmentId);
if (attachment == null) { if (attachment == null) {
Log.w(TAG, "attachment was null, returning vacuous true"); Log.w(TAG, "attachment was null, returning vacuous true");
@ -89,7 +91,15 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
return true; return true;
case AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING: case AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING:
final Set<String> allowedTypes = getAllowedAutoDownloadTypes(); final Set<String> allowedTypes = getAllowedAutoDownloadTypes();
final boolean isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(attachment.getContentType())); final String contentType = attachment.getContentType();
boolean isAllowed;
if (isNonDocumentType(contentType)) {
isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
} else {
isAllowed = allowedTypes.contains("documents");
}
/// XXX WTF -- This is *hella* gross. A requirement shouldn't have the side effect of /// XXX WTF -- This is *hella* gross. A requirement shouldn't have the side effect of
// *modifying the database* just by calling isPresent(). // *modifying the database* just by calling isPresent().
@ -99,4 +109,11 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
return false; return false;
} }
} }
private boolean isNonDocumentType(String contentType) {
return
ContentType.isImageType(contentType) ||
ContentType.isVideoType(contentType) ||
ContentType.isAudioType(contentType);
}
} }

View File

@ -20,12 +20,14 @@ import android.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
@ -40,6 +42,7 @@ import com.google.android.gms.location.places.ui.PlacePicker;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalMapView;
@ -76,6 +79,7 @@ public class AttachmentManager {
private RemovableEditableMediaView removableMediaView; private RemovableEditableMediaView removableMediaView;
private ThumbnailView thumbnail; private ThumbnailView thumbnail;
private AudioView audioView; private AudioView audioView;
private DocumentView documentView;
private SignalMapView mapView; private SignalMapView mapView;
private @NonNull List<Uri> garbage = new LinkedList<>(); private @NonNull List<Uri> garbage = new LinkedList<>();
@ -94,6 +98,7 @@ public class AttachmentManager {
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
this.audioView = ViewUtil.findById(root, R.id.attachment_audio); this.audioView = ViewUtil.findById(root, R.id.attachment_audio);
this.documentView = ViewUtil.findById(root, R.id.attachment_document);
this.mapView = ViewUtil.findById(root, R.id.attachment_location); this.mapView = ViewUtil.findById(root, R.id.attachment_location);
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
@ -195,7 +200,7 @@ public class AttachmentManager {
{ {
inflateStub(); inflateStub();
new AsyncTask<Void, Void, Slide>() { new AsyncTask<Void, Void, Slide>() {
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
thumbnail.clear(); thumbnail.clear();
@ -205,16 +210,33 @@ public class AttachmentManager {
@Override @Override
protected @Nullable Slide doInBackground(Void... params) { protected @Nullable Slide doInBackground(Void... params) {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Cursor cursor = null;
try { try {
final long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri); if (PartAuthority.isLocalUri(uri)) {
final Slide slide = mediaType.createSlide(context, uri, mediaSize); long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
Log.w(TAG, "slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
return slide; return mediaType.createSlide(context, uri, null, null, mediaSize);
} catch (IOException ioe) { } else {
Log.w(TAG, ioe); cursor = context.getContentResolver().query(uri, null, null, null, null);
return null;
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
return mediaType.createSlide(context, uri, fileName, mimeType, fileSize);
}
}
} catch (IOException e) {
Log.w(TAG, e);
} finally {
if (cursor != null) cursor.close();
} }
return null;
} }
@Override @Override
@ -234,8 +256,11 @@ public class AttachmentManager {
attachmentViewStub.get().setVisibility(View.VISIBLE); attachmentViewStub.get().setVisibility(View.VISIBLE);
if (slide.hasAudio()) { if (slide.hasAudio()) {
audioView.setAudio(masterSecret, (AudioSlide)slide, false); audioView.setAudio(masterSecret, (AudioSlide) slide, false);
removableMediaView.display(audioView, false); removableMediaView.display(audioView, false);
} else if (slide.hasDocument()) {
documentView.setDocument((DocumentSlide)slide, false);
removableMediaView.display(documentView, false);
} else { } else {
thumbnail.setImageResource(masterSecret, slide, false); thumbnail.setImageResource(masterSecret, slide, false);
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
@ -386,18 +411,25 @@ public class AttachmentManager {
} }
public enum MediaType { public enum MediaType {
IMAGE, GIF, AUDIO, VIDEO; IMAGE, GIF, AUDIO, VIDEO, DOCUMENT;
public @NonNull Slide createSlide(@NonNull Context context, public @NonNull Slide createSlide(@NonNull Context context,
@NonNull Uri uri, @NonNull Uri uri,
long dataSize) @Nullable String fileName,
@Nullable String mimeType,
long dataSize)
{ {
if (mimeType == null) {
mimeType = "application/octet-stream";
}
switch (this) { switch (this) {
case IMAGE: return new ImageSlide(context, uri, dataSize); case IMAGE: return new ImageSlide(context, uri, dataSize);
case GIF: return new GifSlide(context, uri, dataSize); case GIF: return new GifSlide(context, uri, dataSize);
case AUDIO: return new AudioSlide(context, uri, dataSize); case AUDIO: return new AudioSlide(context, uri, dataSize);
case VIDEO: return new VideoSlide(context, uri, dataSize); case VIDEO: return new VideoSlide(context, uri, dataSize);
default: throw new AssertionError("unrecognized enum"); case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
default: throw new AssertionError("unrecognized enum");
} }
} }
@ -409,5 +441,6 @@ public class AttachmentManager {
if (ContentType.isVideoType(mimeType)) return VIDEO; if (ContentType.isVideoType(mimeType)) return VIDEO;
return null; return null;
} }
} }
} }

View File

@ -37,11 +37,11 @@ import ws.com.google.android.mms.pdu.PduPart;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize) { public AudioSlide(Context context, Uri uri, long dataSize) {
super(context, constructAttachmentFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize, false)); super(context, constructAttachmentFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize, false, null));
} }
public AudioSlide(Context context, Uri uri, long dataSize, String contentType) { public AudioSlide(Context context, Uri uri, long dataSize, String contentType) {
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize)); super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, null));
} }
public AudioSlide(Context context, Attachment attachment) { public AudioSlide(Context context, Attachment attachment) {

View File

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
public class DocumentSlide extends Slide {
public DocumentSlide(@NonNull Context context, @NonNull Attachment attachment) {
super(context, attachment);
}
public DocumentSlide(@NonNull Context context, @NonNull Uri uri,
@NonNull String contentType, long size,
@Nullable String fileName)
{
super(context, constructAttachmentFromUri(context, uri, contentType, size, true, fileName));
}
@Override
public boolean hasDocument() {
return true;
}
}

View File

@ -20,7 +20,7 @@ public class GifSlide extends ImageSlide {
} }
public GifSlide(Context context, Uri uri, long size) { public GifSlide(Context context, Uri uri, long size) {
super(context, constructAttachmentFromUri(context, uri, ContentType.IMAGE_GIF, size, true)); super(context, constructAttachmentFromUri(context, uri, ContentType.IMAGE_GIF, size, true, null));
} }
@Override @Override

View File

@ -36,7 +36,7 @@ public class ImageSlide extends Slide {
} }
public ImageSlide(Context context, Uri uri, long size) { public ImageSlide(Context context, Uri uri, long size) {
super(context, constructAttachmentFromUri(context, uri, ContentType.IMAGE_JPEG, size, true)); super(context, constructAttachmentFromUri(context, uri, ContentType.IMAGE_JPEG, size, true, null));
} }
@Override @Override

View File

@ -60,6 +60,15 @@ public abstract class Slide {
return Optional.absent(); return Optional.absent();
} }
@NonNull
public Optional<String> getFileName() {
return Optional.fromNullable(attachment.getFileName());
}
public long getFileSize() {
return attachment.getSize();
}
public boolean hasImage() { public boolean hasImage() {
return false; return false;
} }
@ -72,6 +81,10 @@ public abstract class Slide {
return false; return false;
} }
public boolean hasDocument() {
return false;
}
public boolean hasLocation() { public boolean hasLocation() {
return false; return false;
} }
@ -107,14 +120,15 @@ public abstract class Slide {
return false; return false;
} }
protected static Attachment constructAttachmentFromUri(@NonNull Context context, protected static Attachment constructAttachmentFromUri(@NonNull Context context,
@NonNull Uri uri, @NonNull Uri uri,
@NonNull String defaultMime, @NonNull String defaultMime,
long size, long size,
boolean hasThumbnail) boolean hasThumbnail,
@Nullable String fileName)
{ {
Optional<String> resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)); Optional<String> resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri));
return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size); return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size, fileName);
} }
@Override @Override

View File

@ -86,7 +86,7 @@ public class SlideDeck {
public boolean containsMediaSlide() { public boolean containsMediaSlide() {
for (Slide slide : slides) { for (Slide slide : slides) {
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) { if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) {
return true; return true;
} }
} }
@ -112,4 +112,14 @@ public class SlideDeck {
return null; return null;
} }
public @Nullable DocumentSlide getDocumentSlide() {
for (Slide slide: slides) {
if (slide.hasDocument()) {
return (DocumentSlide)slide;
}
}
return null;
}
} }

View File

@ -31,7 +31,7 @@ import ws.com.google.android.mms.ContentType;
public class VideoSlide extends Slide { public class VideoSlide extends Slide {
public VideoSlide(Context context, Uri uri, long dataSize) { public VideoSlide(Context context, Uri uri, long dataSize) {
super(context, constructAttachmentFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize, false)); super(context, constructAttachmentFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize, false, null));
} }
public VideoSlide(Context context, Attachment attachment) { public VideoSlide(Context context, Attachment attachment) {

View File

@ -21,24 +21,30 @@ import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.UriMatcher; import android.content.UriMatcher;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri; import android.net.Uri;
import android.os.MemoryFile;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.PartUriParser; import org.thoughtcrime.securesms.mms.PartUriParser;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MemoryFileUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
public class PartProvider extends ContentProvider { public class PartProvider extends ContentProvider {
private static final String TAG = PartProvider.class.getSimpleName(); private static final String TAG = PartProvider.class.getSimpleName();
private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.provider.securesms/part"; private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.provider.securesms/part";
@ -63,27 +69,9 @@ public class PartProvider extends ContentProvider {
return ContentUris.withAppendedId(uri, attachmentId.getRowId()); return ContentUris.withAppendedId(uri, attachmentId.getRowId());
} }
@SuppressWarnings("ConstantConditions")
private File copyPartToTemporaryFile(MasterSecret masterSecret, AttachmentId attachmentId) throws IOException {
InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId);
File tmpDir = getContext().getDir("tmp", 0);
File tmpFile = File.createTempFile("test", ".jpg", tmpDir);
FileOutputStream fout = new FileOutputStream(tmpFile);
byte[] buffer = new byte[512];
int read;
while ((read = in.read(buffer)) != -1)
fout.write(buffer, 0, read);
in.close();
return tmpFile;
}
@Override @Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
MasterSecret masterSecret = KeyCachingService.getMasterSecret(getContext()); final MasterSecret masterSecret = KeyCachingService.getMasterSecret(getContext());
Log.w(TAG, "openFile() called!"); Log.w(TAG, "openFile() called!");
if (masterSecret == null) { if (masterSecret == null) {
@ -95,15 +83,8 @@ public class PartProvider extends ContentProvider {
case SINGLE_ROW: case SINGLE_ROW:
Log.w(TAG, "Parting out a single row..."); Log.w(TAG, "Parting out a single row...");
try { try {
PartUriParser partUri = new PartUriParser(uri); final PartUriParser partUri = new PartUriParser(uri);
File tmpFile = copyPartToTemporaryFile(masterSecret, partUri.getPartId()); return getParcelStreamForAttachment(masterSecret, partUri.getPartId());
ParcelFileDescriptor pdf = ParcelFileDescriptor.open(tmpFile, ParcelFileDescriptor.MODE_READ_ONLY);
if (!tmpFile.delete()) {
Log.w(TAG, "Failed to delete temp file.");
}
return pdf;
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w(TAG, ioe); Log.w(TAG, ioe);
throw new FileNotFoundException("Error opening file"); throw new FileNotFoundException("Error opening file");
@ -115,26 +96,81 @@ public class PartProvider extends ContentProvider {
@Override @Override
public int delete(@NonNull Uri arg0, String arg1, String[] arg2) { public int delete(@NonNull Uri arg0, String arg1, String[] arg2) {
Log.w(TAG, "delete() called");
return 0; return 0;
} }
@Override @Override
public String getType(@NonNull Uri arg0) { public String getType(@NonNull Uri uri) {
Log.w(TAG, "getType() called: " + uri);
switch (uriMatcher.match(uri)) {
case SINGLE_ROW:
PartUriParser partUriParser = new PartUriParser(uri);
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext())
.getAttachment(null, partUriParser.getPartId());
if (attachment != null) {
return attachment.getContentType();
}
}
return null; return null;
} }
@Override @Override
public Uri insert(@NonNull Uri arg0, ContentValues arg1) { public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
Log.w(TAG, "insert() called");
return null; return null;
} }
@Override @Override
public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) { public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.w(TAG, "query() called: " + url);
MasterSecret masterSecret = KeyCachingService.getMasterSecret(getContext());
if (projection == null || projection.length <= 0) return null;
switch (uriMatcher.match(url)) {
case SINGLE_ROW:
PartUriParser partUri = new PartUriParser(url);
DatabaseAttachment attachment = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachment(masterSecret, partUri.getPartId());
if (attachment == null) return null;
MatrixCursor matrixCursor = new MatrixCursor(projection, 1);
Object[] resultRow = new Object[projection.length];
for (int i=0;i<projection.length;i++) {
if (OpenableColumns.DISPLAY_NAME.equals(projection[i])) {
resultRow[i] = attachment.getFileName();
}
}
matrixCursor.addRow(resultRow);
return matrixCursor;
}
return null; return null;
} }
@Override @Override
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) { public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
Log.w(TAG, "update() called");
return 0; return 0;
} }
private ParcelFileDescriptor getParcelStreamForAttachment(MasterSecret masterSecret, AttachmentId attachmentId) throws IOException {
long plaintextLength = Util.getStreamLength(DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId));
MemoryFile memoryFile = new MemoryFile(attachmentId.toString(), Util.toIntExact(plaintextLength));
InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId);
OutputStream out = memoryFile.getOutputStream();
Util.copy(in, out);
Util.close(out);
Util.close(in);
return MemoryFileUtil.getParcelFileDescriptor(memoryFile);
}
} }

View File

@ -125,7 +125,7 @@ public class PersistentBlobProvider {
public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException { public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
final byte[] cached = cache.get(id); final byte[] cached = cache.get(id);
return cached != null ? new ByteArrayInputStream(cached) return cached != null ? new ByteArrayInputStream(cached)
: new DecryptingPartInputStream(getFile(id), masterSecret); : DecryptingPartInputStream.createFor(masterSecret, getFile(id));
} }
private File getFile(long id) { private File getFile(long id) {

View File

@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.util;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* An input stream, which limits its data size. This stream is
* used, if the content length is unknown.
*/
public class LimitedInputStream extends FilterInputStream {
/**
* The maximum size of an item, in bytes.
*/
private long sizeMax;
/**
* The current number of bytes.
*/
private long count;
/**
* Whether this stream is already closed.
*/
private boolean closed;
/**
* Creates a new instance.
* @param pIn The input stream, which shall be limited.
* @param pSizeMax The limit; no more than this number of bytes
* shall be returned by the source stream.
*/
public LimitedInputStream(InputStream pIn, long pSizeMax) {
super(pIn);
sizeMax = pSizeMax;
}
/**
* Reads the next byte of data from this input stream. The value
* byte is returned as an <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned. This method blocks until input data
* is available, the end of the stream is detected, or an exception
* is thrown.
*
* This method
* simply performs <code>in.read()</code> and returns the result.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
public int read() throws IOException {
if (count >= sizeMax) return -1;
int res = super.read();
if (res != -1) {
count++;
}
return res;
}
/**
* Reads up to <code>len</code> bytes of data from this input stream
* into an array of bytes. If <code>len</code> is not zero, the method
* blocks until some input is available; otherwise, no
* bytes are read and <code>0</code> is returned.
*
* This method simply performs <code>in.read(b, off, len)</code>
* and returns the result.
*
* @param b the buffer into which the data is read.
* @param off The start offset in the destination array
* <code>b</code>.
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @exception IOException if an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
public int read(byte[] b, int off, int len) throws IOException {
if (count >= sizeMax) return -1;
long correctLength = Math.min(len, sizeMax - count);
int res = super.read(b, off, Util.toIntExact(correctLength));
if (res > 0) {
count += res;
}
return res;
}
}

View File

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GifSlide; import org.thoughtcrime.securesms.mms.GifSlide;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MmsSlide; import org.thoughtcrime.securesms.mms.MmsSlide;
@ -82,6 +83,8 @@ public class MediaUtil {
slide = new AudioSlide(context, attachment); slide = new AudioSlide(context, attachment);
} else if (isMms(attachment.getContentType())) { } else if (isMms(attachment.getContentType())) {
slide = new MmsSlide(context, attachment); slide = new MmsSlide(context, attachment);
} else {
slide = new DocumentSlide(context, attachment);
} }
return slide; return slide;

View File

@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.util;
import android.os.Build;
import android.os.MemoryFile;
import android.os.ParcelFileDescriptor;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MemoryFileUtil {
public static ParcelFileDescriptor getParcelFileDescriptor(MemoryFile file) throws IOException {
try {
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(file);
Field field = fileDescriptor.getClass().getDeclaredField("descriptor");
field.setAccessible(true);
int fd = field.getInt(fileDescriptor);
if (Build.VERSION.SDK_INT >= 13) {
return ParcelFileDescriptor.adoptFd(fd);
} else {
return ParcelFileDescriptor.dup(fileDescriptor);
}
} catch (IllegalAccessException e) {
throw new IOException(e);
} catch (InvocationTargetException e) {
throw new IOException(e);
} catch (NoSuchMethodException e) {
throw new IOException(e);
} catch (NoSuchFieldException e) {
throw new IOException(e);
}
}
}

View File

@ -2,12 +2,16 @@ package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import android.os.Environment; import android.os.Environment;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
@ -15,6 +19,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.Pair;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -24,69 +29,76 @@ import java.io.OutputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Integer> { public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, File>> {
private static final String TAG = SaveAttachmentTask.class.getSimpleName(); private static final String TAG = SaveAttachmentTask.class.getSimpleName();
private static final int SUCCESS = 0; private static final int SUCCESS = 0;
private static final int FAILURE = 1; private static final int FAILURE = 1;
private static final int WRITE_ACCESS_FAILURE = 2; private static final int WRITE_ACCESS_FAILURE = 2;
private final WeakReference<Context> contextReference; private final WeakReference<Context> contextReference;
private final WeakReference<MasterSecret> masterSecretReference; private final WeakReference<MasterSecret> masterSecretReference;
private final WeakReference<View> view;
private final int attachmentCount; private final int attachmentCount;
public SaveAttachmentTask(Context context, MasterSecret masterSecret) { public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) {
this(context, masterSecret, 1); this(context, masterSecret, view, 1);
} }
public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) { public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view, int count) {
super(context, super(context,
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count), context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count)); context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
this.contextReference = new WeakReference<>(context); this.contextReference = new WeakReference<>(context);
this.masterSecretReference = new WeakReference<>(masterSecret); this.masterSecretReference = new WeakReference<>(masterSecret);
this.view = new WeakReference<>(view);
this.attachmentCount = count; this.attachmentCount = count;
} }
@Override @Override
protected Integer doInBackground(SaveAttachmentTask.Attachment... attachments) { protected Pair<Integer, File> doInBackground(SaveAttachmentTask.Attachment... attachments) {
if (attachments == null || attachments.length == 0) { if (attachments == null || attachments.length == 0) {
throw new AssertionError("must pass in at least one attachment"); throw new AssertionError("must pass in at least one attachment");
} }
try { try {
Context context = contextReference.get(); Context context = contextReference.get();
MasterSecret masterSecret = masterSecretReference.get(); MasterSecret masterSecret = masterSecretReference.get();
File directory = null;
if (!Environment.getExternalStorageDirectory().canWrite()) { if (!Environment.getExternalStorageDirectory().canWrite()) {
return WRITE_ACCESS_FAILURE; return new Pair<>(WRITE_ACCESS_FAILURE, null);
} }
if (context == null) { if (context == null) {
return FAILURE; return new Pair<>(FAILURE, null);
} }
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
if (attachment != null && !saveAttachment(context, masterSecret, attachment)) { if (attachment != null) {
return FAILURE; directory = saveAttachment(context, masterSecret, attachment);
if (directory == null) return new Pair<>(FAILURE, null);
} }
} }
return SUCCESS; if (attachments.length > 1) return new Pair<>(SUCCESS, null);
else return new Pair<>(SUCCESS, directory);
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w(TAG, ioe); Log.w(TAG, ioe);
return FAILURE; return new Pair<>(FAILURE, null);
} }
} }
private boolean saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) throws IOException { private @Nullable File saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment)
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType); throws IOException
File mediaFile = constructOutputFile(contentType, attachment.date); {
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
File mediaFile = constructOutputFile(attachment.fileName, contentType, attachment.date);
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri); InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
if (inputStream == null) { if (inputStream == null) {
return false; return null;
} }
OutputStream outputStream = new FileOutputStream(mediaFile); OutputStream outputStream = new FileOutputStream(mediaFile);
@ -95,16 +107,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()}, MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
new String[]{contentType}, null); new String[]{contentType}, null);
return true; return mediaFile.getParentFile();
} }
@Override @Override
protected void onPostExecute(Integer result) { protected void onPostExecute(final Pair<Integer, File> result) {
super.onPostExecute(result); super.onPostExecute(result);
Context context = contextReference.get(); final Context context = contextReference.get();
if (context == null) return; if (context == null) return;
switch (result) { switch (result.first()) {
case FAILURE: case FAILURE:
Toast.makeText(context, Toast.makeText(context,
context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
@ -112,10 +124,26 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
break; break;
case SUCCESS: case SUCCESS:
Toast.makeText(context, Snackbar snackbar = Snackbar.make(view.get(),
context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully, context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully, attachmentCount),
attachmentCount), Snackbar.LENGTH_SHORT);
Toast.LENGTH_LONG).show();
if (result.second() != null) {
snackbar.setDuration(Snackbar.LENGTH_LONG);
snackbar.setAction(R.string.SaveAttachmentTask_open_directory, new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(result.second()), "resource/folder");
if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null)
{
context.startActivity(intent);
}
}
});
}
snackbar.show();
break; break;
case WRITE_ACCESS_FAILURE: case WRITE_ACCESS_FAILURE:
Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation, Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation,
@ -124,7 +152,9 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
} }
} }
private File constructOutputFile(String contentType, long timestamp) throws IOException { private File constructOutputFile(@Nullable String fileName, String contentType, long timestamp)
throws IOException
{
File sdCard = Environment.getExternalStorageDirectory(); File sdCard = Environment.getExternalStorageDirectory();
File outputDirectory; File outputDirectory;
@ -140,32 +170,54 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue"); if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); if (fileName == null) {
String extension = mimeTypeMap.getExtensionFromMimeType(contentType); MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
String base = "signal-" + dateFormatter.format(timestamp); SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
String base = "signal-" + dateFormatter.format(timestamp);
if (extension == null) extension = "attach"; if (extension == null) extension = "attach";
fileName = base + "." + extension;
}
int i = 0;
File file = new File(outputDirectory, fileName);
int i = 0;
File file = new File(outputDirectory, base + "." + extension);
while (file.exists()) { while (file.exists()) {
file = new File(outputDirectory, base + "-" + (++i) + "." + extension); String[] fileParts = getFileNameParts(fileName);
file = new File(outputDirectory, fileParts[0] + "-" + (++i) + "." + fileParts[1]);
} }
return file; return file;
} }
private String[] getFileNameParts(String fileName) {
String[] result = new String[2];
String[] tokens = fileName.split("\\.(?=[^\\.]+$)");
result[0] = tokens[0];
if (tokens.length > 1) result[1] = tokens[1];
else result[1] = "";
return result;
}
public static class Attachment { public static class Attachment {
public Uri uri; public Uri uri;
public String fileName;
public String contentType; public String contentType;
public long date; public long date;
public Attachment(@NonNull Uri uri, @NonNull String contentType, long date) { public Attachment(@NonNull Uri uri, @NonNull String contentType,
long date, @Nullable String fileName)
{
if (uri == null || contentType == null || date < 0) { if (uri == null || contentType == null || date < 0) {
throw new AssertionError("uri, content type, and date must all be specified"); throw new AssertionError("uri, content type, and date must all be specified");
} }
this.uri = uri; this.uri = uri;
this.fileName = fileName;
this.contentType = contentType; this.contentType = contentType;
this.date = date; this.date = date;
} }

View File

@ -54,6 +54,7 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -164,6 +165,14 @@ public class Util {
} }
} }
public static void close(InputStream in) {
try {
in.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
public static void close(OutputStream out) { public static void close(OutputStream out) {
try { try {
out.close(); out.close();
@ -172,6 +181,19 @@ public class Util {
} }
} }
public static long getStreamLength(InputStream in) throws IOException {
byte[] buffer = new byte[4096];
int totalSize = 0;
int read;
while ((read = in.read(buffer)) != -1) {
totalSize += read;
}
return totalSize;
}
public static String canonicalizeNumber(Context context, String number) public static String canonicalizeNumber(Context context, String number)
throws InvalidNumberException throws InvalidNumberException
{ {
@ -463,4 +485,13 @@ public class Util {
public static boolean isEquals(@Nullable Long first, long second) { public static boolean isEquals(@Nullable Long first, long second) {
return first != null && first == second; return first != null && first == second;
} }
public static String getPrettyFileSize(long sizeBytes) {
if (sizeBytes <= 0) return "0";
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
int digitGroups = (int) (Math.log10(sizeBytes) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
} }

View File

@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
@TargetApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M)
public class EncryptedMediaDataSource extends MediaDataSource { public class EncryptedMediaDataSource extends MediaDataSource {
@ -25,9 +26,9 @@ public class EncryptedMediaDataSource extends MediaDataSource {
@Override @Override
public int readAt(long position, byte[] bytes, int offset, int length) throws IOException { public int readAt(long position, byte[] bytes, int offset, int length) throws IOException {
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret); InputStream inputStream = DecryptingPartInputStream.createFor(masterSecret, mediaFile);
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
long headerRemaining = position; long headerRemaining = position;
while (headerRemaining > 0) { while (headerRemaining > 0) {
int read = inputStream.read(buffer, 0, Util.toIntExact(Math.min((long)buffer.length, headerRemaining))); int read = inputStream.read(buffer, 0, Util.toIntExact(Math.min((long)buffer.length, headerRemaining)));
@ -44,9 +45,9 @@ public class EncryptedMediaDataSource extends MediaDataSource {
@Override @Override
public long getSize() throws IOException { public long getSize() throws IOException {
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret); InputStream inputStream = DecryptingPartInputStream.createFor(masterSecret, mediaFile);
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
long size = 0; long size = 0;
int read; int read;

View File

@ -25,6 +25,8 @@ import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource; import org.webrtc.VideoSource;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;

View File

@ -38,7 +38,7 @@ public class AttachmentDatabaseTest extends TextSecureTestCase {
final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID);
DatabaseAttachment mockAttachment = getMockAttachment("x/x"); DatabaseAttachment mockAttachment = getMockAttachment("x/x");
when(database.getAttachment(attachmentId)).thenReturn(mockAttachment); when(database.getAttachment(null, attachmentId)).thenReturn(mockAttachment);
InputStream mockInputStream = mock(InputStream.class); InputStream mockInputStream = mock(InputStream.class);
doReturn(mockInputStream).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); doReturn(mockInputStream).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail"));
@ -52,7 +52,7 @@ public class AttachmentDatabaseTest extends TextSecureTestCase {
final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID);
DatabaseAttachment mockAttachment = getMockAttachment("image/png"); DatabaseAttachment mockAttachment = getMockAttachment("image/png");
when(database.getAttachment(attachmentId)).thenReturn(mockAttachment); when(database.getAttachment(null, attachmentId)).thenReturn(mockAttachment);
doReturn(null).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); 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()); doNothing().when(database).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat());