mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Support for receiving arbitrary attachment types
// FREEBIE
This commit is contained in:
parent
c69efbffd2
commit
f67eb5f9f3
@ -59,7 +59,7 @@ dependencies {
|
||||
|
||||
compile 'org.whispersystems:jobmanager:1.0.2'
|
||||
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 "me.leolin:ShortcutBadger:1.10-WS1"
|
||||
@ -129,7 +129,7 @@ dependencyVerification {
|
||||
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
|
||||
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
|
||||
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
|
||||
'org.whispersystems:signal-service-android:28a5368cb1336106ba7732aeaf0c5a33ef8fb22500c41f38ad8147375f59073b',
|
||||
'org.whispersystems:signal-service-android:3d7859b194e518fbaf5a082daf22ca345411705e825791f751eb388f149583c3',
|
||||
'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
|
||||
'me.leolin:ShortcutBadger:e8e39df8a59d8211a30f40b1eeab21b3fa57b3f3e0f03abb995f82d66588778c',
|
||||
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
|
||||
@ -165,7 +165,7 @@ dependencyVerification {
|
||||
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
|
||||
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
|
||||
'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
|
||||
'org.whispersystems:signal-service-java:969b4e1fb0b87e553d8b231a090002a03748e0444fa23afa1bc6f7065e8039ff',
|
||||
'org.whispersystems:signal-service-java:4d51d423510bcc3f3a0db1a2c5c7164e379af7ad7f9c20cf0faa753eef9f3f27',
|
||||
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
|
||||
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
|
||||
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
|
||||
|
BIN
res/drawable-hdpi/ic_insert_drive_file_white_24dp.png
Normal file
BIN
res/drawable-hdpi/ic_insert_drive_file_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 153 B |
BIN
res/drawable-mdpi/ic_insert_drive_file_white_24dp.png
Normal file
BIN
res/drawable-mdpi/ic_insert_drive_file_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 B |
BIN
res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png
Normal file
BIN
res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 B |
BIN
res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png
Normal file
BIN
res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 283 B |
BIN
res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png
Normal file
BIN
res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 372 B |
@ -41,6 +41,18 @@
|
||||
app:foregroundTintColor="@color/grey_500"
|
||||
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>
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -61,6 +61,11 @@
|
||||
android:layout_width="210dp"
|
||||
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
|
||||
android:id="@+id/conversation_item_body"
|
||||
android:layout_width="wrap_content"
|
||||
|
12
res/layout/conversation_item_received_document.xml
Normal file
12
res/layout/conversation_item_received_document.xml
Normal 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"/>
|
@ -50,6 +50,11 @@
|
||||
android:layout_width="210dp"
|
||||
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
|
||||
android:id="@+id/conversation_item_body"
|
||||
android:autoLink="all"
|
||||
|
12
res/layout/conversation_item_sent_document.xml
Normal file
12
res/layout/conversation_item_sent_document.xml
Normal 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"/>
|
105
res/layout/document_view.xml
Normal file
105
res/layout/document_view.xml
Normal 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>
|
@ -206,22 +206,26 @@
|
||||
<item>image</item>
|
||||
<item>audio</item>
|
||||
<item>video</item>
|
||||
<item>documents</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_media_download_values">
|
||||
<item>@string/arrays__images</item>
|
||||
<item>@string/arrays__audio</item>
|
||||
<item>@string/arrays__video</item>
|
||||
<item>@string/arrays__documents</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_media_download_mobile_data_default">
|
||||
<item>image</item>
|
||||
<item>audio</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_media_download_wifi_default">
|
||||
<item>image</item>
|
||||
<item>audio</item>
|
||||
<item>video</item>
|
||||
<item>documents</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_media_download_roaming_default" />
|
||||
|
@ -197,4 +197,10 @@
|
||||
<attr name="pickerColors" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="DocumentView">
|
||||
<attr name="documentWidgetBackground" format="color"/>
|
||||
<attr name="documentForegroundTintColor" format="color" />
|
||||
<attr name="documentBackgroundTintColor" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
@ -1063,6 +1063,7 @@
|
||||
<string name="arrays__images">Images</string>
|
||||
<string name="arrays__audio">Audio</string>
|
||||
<string name="arrays__video">Video</string>
|
||||
<string name="arrays__documents">Documents</string>
|
||||
|
||||
<!-- plurals.xml -->
|
||||
<plurals name="hours_ago">
|
||||
@ -1343,6 +1344,8 @@
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<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 -->
|
||||
|
||||
|
@ -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_THUMBNAIL_OUTGOING = 5;
|
||||
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>());
|
||||
|
||||
@ -223,9 +225,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
switch (viewType) {
|
||||
case MESSAGE_TYPE_AUDIO_OUTGOING:
|
||||
case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
|
||||
case MESSAGE_TYPE_DOCUMENT_OUTGOING:
|
||||
case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent;
|
||||
case MESSAGE_TYPE_AUDIO_INCOMING:
|
||||
case MESSAGE_TYPE_THUMBNAIL_INCOMING:
|
||||
case MESSAGE_TYPE_DOCUMENT_INCOMING:
|
||||
case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received;
|
||||
case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update;
|
||||
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)) {
|
||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
|
||||
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)) {
|
||||
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING;
|
||||
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;
|
||||
}
|
||||
|
||||
private boolean hasDocument(MessageRecord messageRecord) {
|
||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
|
||||
}
|
||||
|
||||
private boolean hasThumbnail(MessageRecord messageRecord) {
|
||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
|
||||
}
|
||||
|
@ -389,9 +389,9 @@ public class ConversationFragment extends Fragment
|
||||
SaveAttachmentTask.showWarningDialog(getActivity(), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
for (Slide slide : message.getSlideDeck().getSlides()) {
|
||||
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio()) && slide.getUri() != null) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret);
|
||||
saveTask.execute(new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived()));
|
||||
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) && slide.getUri() != null) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity(), masterSecret, list);
|
||||
saveTask.execute(new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,11 @@ import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
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.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.ExpirationTimerView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
@ -112,6 +115,7 @@ public class ConversationItem extends LinearLayout
|
||||
private @Nullable Recipients conversationRecipients;
|
||||
private @NonNull Stub<ThumbnailView> mediaThumbnailStub;
|
||||
private @NonNull Stub<AudioView> audioViewStub;
|
||||
private @NonNull Stub<DocumentView> documentViewStub;
|
||||
private @NonNull ExpirationTimerView expirationTimer;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
@ -153,6 +157,7 @@ public class ConversationItem extends LinearLayout
|
||||
this.bodyBubble = findViewById(R.id.body_bubble);
|
||||
this.mediaThumbnailStub = new Stub<>((ViewStub) findViewById(R.id.image_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);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
@ -229,6 +234,10 @@ public class ConversationItem extends LinearLayout
|
||||
if (audioViewStub.resolved()) {
|
||||
setAudioViewTint(messageRecord, conversationRecipients);
|
||||
}
|
||||
|
||||
if (documentViewStub.resolved()) {
|
||||
setDocumentViewTint(messageRecord, conversationRecipients);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
setSelected(batchSelected.contains(messageRecord));
|
||||
bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0);
|
||||
@ -258,6 +279,11 @@ public class ConversationItem extends LinearLayout
|
||||
audioViewStub.get().setClickable(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) {
|
||||
@ -272,6 +298,10 @@ public class ConversationItem extends LinearLayout
|
||||
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) {
|
||||
bodyText.setClickable(false);
|
||||
bodyText.setFocusable(false);
|
||||
@ -290,6 +320,7 @@ public class ConversationItem extends LinearLayout
|
||||
if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
audioViewStub.get().setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
|
||||
@ -297,9 +328,22 @@ public class ConversationItem extends LinearLayout
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
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)) {
|
||||
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
|
||||
mediaThumbnailStub.get().setImageResource(masterSecret,
|
||||
@ -314,6 +358,7 @@ public class ConversationItem extends LinearLayout
|
||||
} else {
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.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));
|
||||
}
|
||||
}
|
||||
@ -498,19 +543,6 @@ public class ConversationItem extends LinearLayout
|
||||
}
|
||||
|
||||
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) {
|
||||
if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
|
||||
performClick();
|
||||
@ -525,18 +557,18 @@ public class ConversationItem extends LinearLayout
|
||||
|
||||
context.startActivity(intent);
|
||||
} else if (slide.getUri() != null) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.ConversationItem_view_secure_media_question);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setCancelable(true);
|
||||
builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning);
|
||||
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
fireIntent(slide);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.no, null);
|
||||
builder.show();
|
||||
Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
|
||||
Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());
|
||||
Log.w(TAG, "Public URI: " + publicUri);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -554,6 +586,7 @@ public class ConversationItem extends LinearLayout
|
||||
performClick();
|
||||
}
|
||||
}
|
||||
|
||||
private class ClickListener implements View.OnClickListener {
|
||||
private OnClickListener parent;
|
||||
|
||||
|
@ -32,10 +32,10 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase.Reader;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
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.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
|
||||
@ -244,7 +243,7 @@ public class DatabaseUpgradeActivity extends BaseActivity {
|
||||
private void schedulePendingIncomingParts(Context context) {
|
||||
final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(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.");
|
||||
for (DatabaseAttachment attachment : pendingAttachments) {
|
||||
|
@ -67,7 +67,7 @@ public class MediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
|
||||
@Override
|
||||
public void onBindItemViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) {
|
||||
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());
|
||||
|
||||
|
@ -166,10 +166,11 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
|
||||
List<SaveAttachmentTask.Attachment> attachments = new ArrayList<>(cursor.getCount());
|
||||
|
||||
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(),
|
||||
record.getContentType(),
|
||||
record.getDate()));
|
||||
record.getDate(),
|
||||
null));
|
||||
}
|
||||
|
||||
return attachments;
|
||||
@ -179,7 +180,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> 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()]));
|
||||
}
|
||||
}.execute();
|
||||
|
@ -207,9 +207,9 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
SaveAttachmentTask.showWarningDialog(this, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
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();
|
||||
saveTask.execute(new Attachment(mediaUri, mediaType, saveDate));
|
||||
saveTask.execute(new Attachment(mediaUri, mediaType, saveDate, null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -13,6 +13,9 @@ public abstract class Attachment {
|
||||
private final int transferState;
|
||||
private final long size;
|
||||
|
||||
@Nullable
|
||||
private final String fileName;
|
||||
|
||||
@Nullable
|
||||
private final String location;
|
||||
|
||||
@ -25,13 +28,14 @@ public abstract class Attachment {
|
||||
@Nullable
|
||||
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 byte[] digest)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
this.transferState = transferState;
|
||||
this.size = size;
|
||||
this.fileName = fileName;
|
||||
this.location = location;
|
||||
this.key = key;
|
||||
this.relay = relay;
|
||||
@ -57,6 +61,11 @@ public abstract class Attachment {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
|
@ -15,9 +15,10 @@ public class DatabaseAttachment extends Attachment {
|
||||
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
||||
boolean hasData, boolean hasThumbnail,
|
||||
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.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
|
@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -16,10 +16,11 @@ import java.util.List;
|
||||
public class PointerAttachment extends Attachment {
|
||||
|
||||
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)
|
||||
{
|
||||
super(contentType, transferState, size, location, key, relay, digest);
|
||||
super(contentType, transferState, size, fileName, location, key, relay, digest);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -45,6 +46,7 @@ public class PointerAttachment extends Attachment {
|
||||
results.add(new PointerAttachment(pointer.getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING,
|
||||
pointer.asPointer().getSize().or(0),
|
||||
pointer.asPointer().getFileName().orNull(),
|
||||
String.valueOf(pointer.asPointer().getId()),
|
||||
encryptedKey, pointer.asPointer().getRelay().orNull(),
|
||||
pointer.asPointer().getDigest().orNull()));
|
||||
|
@ -9,14 +9,17 @@ public class UriAttachment extends Attachment {
|
||||
private final @NonNull Uri dataUri;
|
||||
private final @Nullable Uri thumbnailUri;
|
||||
|
||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size) {
|
||||
this(uri, uri, contentType, transferState, size);
|
||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
this(uri, uri, contentType, transferState, size, fileName);
|
||||
}
|
||||
|
||||
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.thumbnailUri = thumbnailUri;
|
||||
}
|
||||
|
196
src/org/thoughtcrime/securesms/components/DocumentView.java
Normal file
196
src/org/thoughtcrime/securesms/components/DocumentView.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,213 +16,100 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.thoughtcrime.securesms.util.LimitedInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.lang.System;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Class for streaming an encrypted MMS "part" off the disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class DecryptingPartInputStream extends FileInputStream {
|
||||
public class DecryptingPartInputStream {
|
||||
|
||||
private static final String TAG = DecryptingPartInputStream.class.getSimpleName();
|
||||
|
||||
private static final int IV_LENGTH = 16;
|
||||
private static final int MAC_LENGTH = 20;
|
||||
|
||||
private Cipher cipher;
|
||||
private Mac mac;
|
||||
|
||||
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
|
||||
public static InputStream createFor(MasterSecret masterSecret, File file)
|
||||
throws IOException
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = readIv(cipher.getBlockSize());
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
try {
|
||||
if (file.length() <= IV_LENGTH + MAC_LENGTH) {
|
||||
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 {
|
||||
byte[] iv = new byte[size];
|
||||
readFully(iv);
|
||||
private static void verifyMac(MasterSecret masterSecret, File file) throws IOException {
|
||||
Mac mac = initializeMac(masterSecret.getMacKey());
|
||||
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);
|
||||
return new IvParameterSpec(iv);
|
||||
if (macStream.skip(file.length() - MAC_LENGTH) != file.length() - MAC_LENGTH) {
|
||||
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;
|
||||
|
||||
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;
|
||||
else return;
|
||||
|
@ -36,8 +36,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||
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.DecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
|
||||
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.Util;
|
||||
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@ -76,6 +79,7 @@ public class AttachmentDatabase extends Database {
|
||||
static final String DATA = "_data";
|
||||
static final String TRANSFER_STATE = "pending_push";
|
||||
static final String SIZE = "data_size";
|
||||
static final String FILE_NAME = "file_name";
|
||||
static final String THUMBNAIL = "thumbnail";
|
||||
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
|
||||
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,
|
||||
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
|
||||
CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE,
|
||||
SIZE, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
|
||||
SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO,
|
||||
UNIQUE_ID, DIGEST};
|
||||
|
||||
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, " +
|
||||
"ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " +
|
||||
TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " +
|
||||
THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL, " +
|
||||
DIGEST + " BLOB);";
|
||||
FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " +
|
||||
UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"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));
|
||||
}
|
||||
|
||||
public @Nullable DatabaseAttachment getAttachment(AttachmentId attachmentId) {
|
||||
public @Nullable DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, AttachmentId attachmentId)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
} 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();
|
||||
List<DatabaseAttachment> results = new LinkedList<>();
|
||||
Cursor cursor = null;
|
||||
@ -184,7 +189,7 @@ public class AttachmentDatabase extends Database {
|
||||
null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(getAttachment(cursor));
|
||||
results.add(getAttachment(masterSecret, cursor));
|
||||
}
|
||||
|
||||
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 List<DatabaseAttachment> attachments = new LinkedList<>();
|
||||
|
||||
@ -202,7 +207,7 @@ public class AttachmentDatabase extends Database {
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
attachments.add(getAttachment(cursor));
|
||||
attachments.add(getAttachment(masterSecret, cursor));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
@ -282,7 +287,6 @@ public class AttachmentDatabase extends Database {
|
||||
return partData.second;
|
||||
}
|
||||
|
||||
|
||||
void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret,
|
||||
long mmsId,
|
||||
@NonNull List<Attachment> attachments)
|
||||
@ -324,6 +328,7 @@ public class AttachmentDatabase extends Database {
|
||||
mediaStream.getMimeType(),
|
||||
databaseAttachment.getTransferState(),
|
||||
dataSize,
|
||||
databaseAttachment.getFileName(),
|
||||
databaseAttachment.getLocation(),
|
||||
databaseAttachment.getKey(),
|
||||
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) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
@ -365,9 +386,9 @@ public class AttachmentDatabase extends Database {
|
||||
File dataFile = getAttachmentDataFile(attachmentId, dataType);
|
||||
|
||||
try {
|
||||
if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret);
|
||||
if (dataFile != null) return DecryptingPartInputStream.createFor(masterSecret, dataFile);
|
||||
else return null;
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
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)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
|
||||
@ -447,6 +479,7 @@ public class AttachmentDatabase extends Database {
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
fileName,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
|
||||
@ -462,12 +495,17 @@ public class AttachmentDatabase extends Database {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Pair<File, Long> partData = null;
|
||||
long uniqueId = System.currentTimeMillis();
|
||||
String fileName = null;
|
||||
|
||||
if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) {
|
||||
partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri());
|
||||
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.put(MMS_ID, mmsId);
|
||||
contentValues.put(CONTENT_TYPE, attachment.getContentType());
|
||||
@ -477,6 +515,8 @@ public class AttachmentDatabase extends Database {
|
||||
contentValues.put(DIGEST, attachment.getDigest());
|
||||
contentValues.put(CONTENT_DISPOSITION, attachment.getKey());
|
||||
contentValues.put(NAME, attachment.getRelay());
|
||||
contentValues.put(FILE_NAME, fileName);
|
||||
contentValues.put(SIZE, attachment.getSize());
|
||||
|
||||
if (partData != null) {
|
||||
contentValues.put(DATA, partData.first.getAbsolutePath());
|
||||
@ -543,7 +583,7 @@ public class AttachmentDatabase extends Database {
|
||||
return stream;
|
||||
}
|
||||
|
||||
DatabaseAttachment attachment = getAttachment(attachmentId);
|
||||
DatabaseAttachment attachment = getAttachment(masterSecret, attachmentId);
|
||||
|
||||
if (attachment == null || !attachment.hasData()) {
|
||||
return null;
|
||||
|
@ -76,7 +76,8 @@ public class DatabaseFactory {
|
||||
private static final int INTRODUCED_LAST_SEEN = 29;
|
||||
private static final int INTRODUCED_DIGEST = 30;
|
||||
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 Object lock = new Object();
|
||||
@ -388,7 +389,7 @@ public class DatabaseFactory {
|
||||
|
||||
InputStream is;
|
||||
|
||||
if (encrypted) is = new DecryptingPartInputStream(dataFile, masterSecret);
|
||||
if (encrypted) is = DecryptingPartInputStream.createFor(masterSecret, dataFile);
|
||||
else is = new FileInputStream(dataFile);
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
if (oldVersion < INTRODUCED_DOCUMENTS) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN file_name TEXT");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
@ -4,22 +4,30 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
|
||||
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.THUMBNAIL_ASPECT_RATIO + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
|
||||
+ 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.DATE_SENT + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", "
|
||||
@ -46,35 +54,21 @@ public class MediaDatabase extends Database {
|
||||
}
|
||||
|
||||
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,
|
||||
boolean hasData, boolean hasThumbnail,
|
||||
String contentType, String address, long date,
|
||||
int transferState, long size)
|
||||
{
|
||||
this.attachmentId = attachmentId;
|
||||
this.mmsId = mmsId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
this.contentType = contentType;
|
||||
this.address = address;
|
||||
this.date = date;
|
||||
this.transferState = transferState;
|
||||
this.size = size;
|
||||
private final DatabaseAttachment attachment;
|
||||
private final String address;
|
||||
private final long date;
|
||||
|
||||
private MediaRecord(DatabaseAttachment attachment, String address, long date) {
|
||||
this.attachment = attachment;
|
||||
this.address = address;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public static MediaRecord from(Cursor cursor) {
|
||||
AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)));
|
||||
public static MediaRecord from(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Cursor cursor) {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment attachment = attachmentDatabase.getAttachment(masterSecret, cursor);
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
|
||||
|
||||
long date;
|
||||
|
||||
@ -84,23 +78,15 @@ public class MediaDatabase extends Database {
|
||||
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED));
|
||||
}
|
||||
|
||||
return new MediaRecord(attachmentId,
|
||||
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)));
|
||||
return new MediaRecord(attachment, address, date);
|
||||
}
|
||||
|
||||
public Attachment getAttachment() {
|
||||
return new DatabaseAttachment(attachmentId, mmsId, hasData, hasThumbnail, contentType, transferState, size, null, null, null, null);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
return attachment.getContentType();
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
|
@ -138,6 +138,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
AttachmentDatabase.UNIQUE_ID,
|
||||
AttachmentDatabase.MMS_ID,
|
||||
AttachmentDatabase.SIZE,
|
||||
AttachmentDatabase.FILE_NAME,
|
||||
AttachmentDatabase.DATA,
|
||||
AttachmentDatabase.THUMBNAIL,
|
||||
AttachmentDatabase.CONTENT_TYPE,
|
||||
@ -630,7 +631,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT));
|
||||
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID));
|
||||
long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
|
||||
List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(messageId));
|
||||
List<Attachment> attachments = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(masterSecret, messageId));
|
||||
MmsAddresses addresses = addr.getAddressesForId(messageId);
|
||||
List<String> destinations = new LinkedList<>();
|
||||
String body = getDecryptedBody(masterSecret, messageText, outboxType);
|
||||
@ -689,6 +690,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
databaseAttachment.getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
databaseAttachment.getSize(),
|
||||
databaseAttachment.getFileName(),
|
||||
databaseAttachment.getLocation(),
|
||||
databaseAttachment.getKey(),
|
||||
databaseAttachment.getRelay(),
|
||||
@ -1267,7 +1269,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@ -63,6 +64,7 @@ public class MmsSmsDatabase extends Database {
|
||||
AttachmentDatabase.UNIQUE_ID,
|
||||
AttachmentDatabase.MMS_ID,
|
||||
AttachmentDatabase.SIZE,
|
||||
AttachmentDatabase.FILE_NAME,
|
||||
AttachmentDatabase.DATA,
|
||||
AttachmentDatabase.THUMBNAIL,
|
||||
AttachmentDatabase.CONTENT_TYPE,
|
||||
@ -157,6 +159,7 @@ public class MmsSmsDatabase extends Database {
|
||||
AttachmentDatabase.UNIQUE_ID,
|
||||
AttachmentDatabase.MMS_ID,
|
||||
AttachmentDatabase.SIZE,
|
||||
AttachmentDatabase.FILE_NAME,
|
||||
AttachmentDatabase.DATA,
|
||||
AttachmentDatabase.THUMBNAIL,
|
||||
AttachmentDatabase.CONTENT_TYPE,
|
||||
@ -185,6 +188,7 @@ public class MmsSmsDatabase extends Database {
|
||||
AttachmentDatabase.UNIQUE_ID,
|
||||
AttachmentDatabase.MMS_ID,
|
||||
AttachmentDatabase.SIZE,
|
||||
AttachmentDatabase.FILE_NAME,
|
||||
AttachmentDatabase.DATA,
|
||||
AttachmentDatabase.THUMBNAIL,
|
||||
AttachmentDatabase.CONTENT_TYPE,
|
||||
@ -239,6 +243,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.MMS_ID);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.SIZE);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.FILE_NAME);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.DATA);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.THUMBNAIL);
|
||||
mmsColumnsPresent.add(AttachmentDatabase.CONTENT_TYPE);
|
||||
|
@ -104,7 +104,7 @@ public class GroupManager {
|
||||
|
||||
if (avatar != null) {
|
||||
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);
|
||||
|
@ -70,7 +70,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
|
||||
@Override
|
||||
public void onRun(MasterSecret masterSecret) throws IOException {
|
||||
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) {
|
||||
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...");
|
||||
}
|
||||
|
||||
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) {
|
||||
Log.w(TAG, e);
|
||||
throw new InvalidPartException(e);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -65,6 +65,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
|
||||
byte[] key = record.getAvatarKey();
|
||||
String relay = record.getRelay();
|
||||
Optional<byte[]> digest = Optional.fromNullable(record.getAvatarDigest());
|
||||
Optional<String> fileName = Optional.absent();
|
||||
|
||||
if (avatarId == -1 || key == null) {
|
||||
return;
|
||||
@ -77,7 +78,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
|
||||
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
|
||||
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);
|
||||
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500);
|
||||
|
||||
|
@ -185,10 +185,14 @@ public class MmsDownloadJob extends MasterSecretJob {
|
||||
PduPart part = media.getPart(i);
|
||||
|
||||
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()),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
part.getData().length));
|
||||
part.getData().length, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
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.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -483,13 +485,19 @@ public class PushDecryptJob extends ContextJob {
|
||||
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
|
||||
|
||||
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) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(),
|
||||
attachment.getAttachmentId()));
|
||||
|
||||
if (!masterSecret.getMasterSecret().isPresent()) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new AttachmentFileNameJob(context, masterSecret.getAsymmetricMasterSecret().get(), attachment, mediaMessage));
|
||||
}
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) {
|
||||
@ -550,7 +558,7 @@ public class PushDecryptJob extends ContextJob {
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
|
||||
for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) {
|
||||
for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, messageId)) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId()));
|
||||
|
@ -74,27 +74,23 @@ public abstract class PushSendJob extends SendJob {
|
||||
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
|
||||
for (final Attachment attachment : parts) {
|
||||
if (ContentType.isImageType(attachment.getContentType()) ||
|
||||
ContentType.isAudioType(attachment.getContentType()) ||
|
||||
ContentType.isVideoType(attachment.getContentType()))
|
||||
{
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
|
||||
attachments.add(SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withListener(new ProgressListener() {
|
||||
@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);
|
||||
}
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
|
||||
attachments.add(SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withListener(new ProgressListener() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ import org.whispersystems.jobqueue.requirements.Requirement;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import ws.com.google.android.mms.ContentType;
|
||||
|
||||
public class MediaNetworkRequirement implements Requirement, ContextDependent {
|
||||
private static final long serialVersionUID = 0L;
|
||||
private static final String TAG = MediaNetworkRequirement.class.getSimpleName();
|
||||
@ -76,7 +78,7 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
|
||||
public boolean isPresent() {
|
||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
||||
final AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context);
|
||||
final Attachment attachment = db.getAttachment(attachmentId);
|
||||
final Attachment attachment = db.getAttachment(null, attachmentId);
|
||||
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment was null, returning vacuous true");
|
||||
@ -89,7 +91,15 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
|
||||
return true;
|
||||
case AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING:
|
||||
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
|
||||
// *modifying the database* just by calling isPresent().
|
||||
@ -99,4 +109,11 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNonDocumentType(String contentType) {
|
||||
return
|
||||
ContentType.isImageType(contentType) ||
|
||||
ContentType.isVideoType(contentType) ||
|
||||
ContentType.isAudioType(contentType);
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,14 @@ import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
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.R;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalMapView;
|
||||
@ -76,6 +79,7 @@ public class AttachmentManager {
|
||||
private RemovableEditableMediaView removableMediaView;
|
||||
private ThumbnailView thumbnail;
|
||||
private AudioView audioView;
|
||||
private DocumentView documentView;
|
||||
private SignalMapView mapView;
|
||||
|
||||
private @NonNull List<Uri> garbage = new LinkedList<>();
|
||||
@ -94,6 +98,7 @@ public class AttachmentManager {
|
||||
|
||||
this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail);
|
||||
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.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
|
||||
@ -195,7 +200,7 @@ public class AttachmentManager {
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
new AsyncTask<Void, Void, Slide>() {
|
||||
new AsyncTask<Void, Void, Slide>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
thumbnail.clear();
|
||||
@ -205,16 +210,33 @@ public class AttachmentManager {
|
||||
|
||||
@Override
|
||||
protected @Nullable Slide doInBackground(Void... params) {
|
||||
long start = System.currentTimeMillis();
|
||||
long start = System.currentTimeMillis();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
final long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
|
||||
final Slide slide = mediaType.createSlide(context, uri, mediaSize);
|
||||
Log.w(TAG, "slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return slide;
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
return null;
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
|
||||
Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||
return mediaType.createSlide(context, uri, null, null, mediaSize);
|
||||
} else {
|
||||
cursor = context.getContentResolver().query(uri, null, null, null, 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
|
||||
@ -234,8 +256,11 @@ public class AttachmentManager {
|
||||
attachmentViewStub.get().setVisibility(View.VISIBLE);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
audioView.setAudio(masterSecret, (AudioSlide)slide, false);
|
||||
audioView.setAudio(masterSecret, (AudioSlide) slide, false);
|
||||
removableMediaView.display(audioView, false);
|
||||
} else if (slide.hasDocument()) {
|
||||
documentView.setDocument((DocumentSlide)slide, false);
|
||||
removableMediaView.display(documentView, false);
|
||||
} else {
|
||||
thumbnail.setImageResource(masterSecret, slide, false);
|
||||
removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
|
||||
@ -386,18 +411,25 @@ public class AttachmentManager {
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
IMAGE, GIF, AUDIO, VIDEO;
|
||||
IMAGE, GIF, AUDIO, VIDEO, DOCUMENT;
|
||||
|
||||
public @NonNull Slide createSlide(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
long dataSize)
|
||||
public @NonNull Slide createSlide(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@Nullable String fileName,
|
||||
@Nullable String mimeType,
|
||||
long dataSize)
|
||||
{
|
||||
if (mimeType == null) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
switch (this) {
|
||||
case IMAGE: return new ImageSlide(context, uri, dataSize);
|
||||
case GIF: return new GifSlide(context, uri, dataSize);
|
||||
case AUDIO: return new AudioSlide(context, uri, dataSize);
|
||||
case VIDEO: return new VideoSlide(context, uri, dataSize);
|
||||
default: throw new AssertionError("unrecognized enum");
|
||||
case IMAGE: return new ImageSlide(context, uri, dataSize);
|
||||
case GIF: return new GifSlide(context, uri, dataSize);
|
||||
case AUDIO: return new AudioSlide(context, uri, dataSize);
|
||||
case VIDEO: return new VideoSlide(context, uri, dataSize);
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -37,11 +37,11 @@ import ws.com.google.android.mms.pdu.PduPart;
|
||||
public class AudioSlide extends Slide {
|
||||
|
||||
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) {
|
||||
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) {
|
||||
|
29
src/org/thoughtcrime/securesms/mms/DocumentSlide.java
Normal file
29
src/org/thoughtcrime/securesms/mms/DocumentSlide.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -20,7 +20,7 @@ public class GifSlide extends ImageSlide {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -36,7 +36,7 @@ public class ImageSlide extends Slide {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -60,6 +60,15 @@ public abstract class Slide {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getFileName() {
|
||||
return Optional.fromNullable(attachment.getFileName());
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return attachment.getSize();
|
||||
}
|
||||
|
||||
public boolean hasImage() {
|
||||
return false;
|
||||
}
|
||||
@ -72,6 +81,10 @@ public abstract class Slide {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasDocument() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return false;
|
||||
}
|
||||
@ -107,14 +120,15 @@ public abstract class Slide {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
boolean hasThumbnail)
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
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
|
||||
|
@ -86,7 +86,7 @@ public class SlideDeck {
|
||||
|
||||
public boolean containsMediaSlide() {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio()) {
|
||||
if (slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -112,4 +112,14 @@ public class SlideDeck {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable DocumentSlide getDocumentSlide() {
|
||||
for (Slide slide: slides) {
|
||||
if (slide.hasDocument()) {
|
||||
return (DocumentSlide)slide;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ import ws.com.google.android.mms.ContentType;
|
||||
public class VideoSlide extends Slide {
|
||||
|
||||
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) {
|
||||
|
@ -21,24 +21,30 @@ import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.MemoryFile;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mms.PartUriParser;
|
||||
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.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class PartProvider extends ContentProvider {
|
||||
|
||||
private static final String TAG = PartProvider.class.getSimpleName();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@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
|
||||
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!");
|
||||
|
||||
if (masterSecret == null) {
|
||||
@ -95,15 +83,8 @@ public class PartProvider extends ContentProvider {
|
||||
case SINGLE_ROW:
|
||||
Log.w(TAG, "Parting out a single row...");
|
||||
try {
|
||||
PartUriParser partUri = new PartUriParser(uri);
|
||||
File tmpFile = copyPartToTemporaryFile(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;
|
||||
final PartUriParser partUri = new PartUriParser(uri);
|
||||
return getParcelStreamForAttachment(masterSecret, partUri.getPartId());
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new FileNotFoundException("Error opening file");
|
||||
@ -115,26 +96,81 @@ public class PartProvider extends ContentProvider {
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri arg0, String arg1, String[] arg2) {
|
||||
Log.w(TAG, "delete() called");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri arg0, ContentValues arg1) {
|
||||
Log.w(TAG, "insert() called");
|
||||
return null;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
|
||||
Log.w(TAG, "update() called");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ public class PersistentBlobProvider {
|
||||
public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
|
||||
final byte[] cached = cache.get(id);
|
||||
return cached != null ? new ByteArrayInputStream(cached)
|
||||
: new DecryptingPartInputStream(getFile(id), masterSecret);
|
||||
: DecryptingPartInputStream.createFor(masterSecret, getFile(id));
|
||||
}
|
||||
|
||||
private File getFile(long id) {
|
||||
|
120
src/org/thoughtcrime/securesms/util/LimitedInputStream.java
Normal file
120
src/org/thoughtcrime/securesms/util/LimitedInputStream.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.MmsSlide;
|
||||
@ -82,6 +83,8 @@ public class MediaUtil {
|
||||
slide = new AudioSlide(context, attachment);
|
||||
} else if (isMms(attachment.getContentType())) {
|
||||
slide = new MmsSlide(context, attachment);
|
||||
} else {
|
||||
slide = new DocumentSlide(context, attachment);
|
||||
}
|
||||
|
||||
return slide;
|
||||
|
41
src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
Normal file
41
src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,16 @@ package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -15,6 +19,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@ -24,69 +29,76 @@ import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
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 int SUCCESS = 0;
|
||||
private static final int FAILURE = 1;
|
||||
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<View> view;
|
||||
|
||||
private final int attachmentCount;
|
||||
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
|
||||
this(context, masterSecret, 1);
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) {
|
||||
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,
|
||||
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));
|
||||
this.contextReference = new WeakReference<>(context);
|
||||
this.masterSecretReference = new WeakReference<>(masterSecret);
|
||||
this.view = new WeakReference<>(view);
|
||||
this.attachmentCount = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||
protected Pair<Integer, File> doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||
if (attachments == null || attachments.length == 0) {
|
||||
throw new AssertionError("must pass in at least one attachment");
|
||||
}
|
||||
|
||||
try {
|
||||
Context context = contextReference.get();
|
||||
Context context = contextReference.get();
|
||||
MasterSecret masterSecret = masterSecretReference.get();
|
||||
File directory = null;
|
||||
|
||||
if (!Environment.getExternalStorageDirectory().canWrite()) {
|
||||
return WRITE_ACCESS_FAILURE;
|
||||
return new Pair<>(WRITE_ACCESS_FAILURE, null);
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
return FAILURE;
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
if (attachment != null && !saveAttachment(context, masterSecret, attachment)) {
|
||||
return FAILURE;
|
||||
if (attachment != null) {
|
||||
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) {
|
||||
Log.w(TAG, ioe);
|
||||
return FAILURE;
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) throws IOException {
|
||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||
File mediaFile = constructOutputFile(contentType, attachment.date);
|
||||
private @Nullable File saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment)
|
||||
throws IOException
|
||||
{
|
||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||
File mediaFile = constructOutputFile(attachment.fileName, contentType, attachment.date);
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
|
||||
|
||||
if (inputStream == null) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
OutputStream outputStream = new FileOutputStream(mediaFile);
|
||||
@ -95,16 +107,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
|
||||
new String[]{contentType}, null);
|
||||
|
||||
return true;
|
||||
return mediaFile.getParentFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result) {
|
||||
protected void onPostExecute(final Pair<Integer, File> result) {
|
||||
super.onPostExecute(result);
|
||||
Context context = contextReference.get();
|
||||
final Context context = contextReference.get();
|
||||
if (context == null) return;
|
||||
|
||||
switch (result) {
|
||||
switch (result.first()) {
|
||||
case FAILURE:
|
||||
Toast.makeText(context,
|
||||
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();
|
||||
break;
|
||||
case SUCCESS:
|
||||
Toast.makeText(context,
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully,
|
||||
attachmentCount),
|
||||
Toast.LENGTH_LONG).show();
|
||||
Snackbar snackbar = Snackbar.make(view.get(),
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully, attachmentCount),
|
||||
Snackbar.LENGTH_SHORT);
|
||||
|
||||
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;
|
||||
case WRITE_ACCESS_FAILURE:
|
||||
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 outputDirectory;
|
||||
|
||||
@ -140,32 +170,54 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
|
||||
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
|
||||
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
|
||||
String base = "signal-" + dateFormatter.format(timestamp);
|
||||
if (fileName == null) {
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
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()) {
|
||||
file = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
||||
String[] fileParts = getFileNameParts(fileName);
|
||||
file = new File(outputDirectory, fileParts[0] + "-" + (++i) + "." + fileParts[1]);
|
||||
}
|
||||
|
||||
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 Uri uri;
|
||||
public String fileName;
|
||||
public String contentType;
|
||||
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) {
|
||||
throw new AssertionError("uri, content type, and date must all be specified");
|
||||
}
|
||||
this.uri = uri;
|
||||
this.fileName = fileName;
|
||||
this.contentType = contentType;
|
||||
this.date = date;
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
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) {
|
||||
try {
|
||||
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)
|
||||
throws InvalidNumberException
|
||||
{
|
||||
@ -463,4 +485,13 @@ public class Util {
|
||||
public static boolean isEquals(@Nullable Long first, long 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];
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public class EncryptedMediaDataSource extends MediaDataSource {
|
||||
@ -25,9 +26,9 @@ public class EncryptedMediaDataSource extends MediaDataSource {
|
||||
|
||||
@Override
|
||||
public int readAt(long position, byte[] bytes, int offset, int length) throws IOException {
|
||||
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret);
|
||||
byte[] buffer = new byte[4096];
|
||||
long headerRemaining = position;
|
||||
InputStream inputStream = DecryptingPartInputStream.createFor(masterSecret, mediaFile);
|
||||
byte[] buffer = new byte[4096];
|
||||
long headerRemaining = position;
|
||||
|
||||
while (headerRemaining > 0) {
|
||||
int read = inputStream.read(buffer, 0, Util.toIntExact(Math.min((long)buffer.length, headerRemaining)));
|
||||
@ -44,9 +45,9 @@ public class EncryptedMediaDataSource extends MediaDataSource {
|
||||
|
||||
@Override
|
||||
public long getSize() throws IOException {
|
||||
DecryptingPartInputStream inputStream = new DecryptingPartInputStream(mediaFile, masterSecret);
|
||||
byte[] buffer = new byte[4096];
|
||||
long size = 0;
|
||||
InputStream inputStream = DecryptingPartInputStream.createFor(masterSecret, mediaFile);
|
||||
byte[] buffer = new byte[4096];
|
||||
long size = 0;
|
||||
|
||||
int read;
|
||||
|
||||
|
@ -25,6 +25,8 @@ import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoRenderer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -38,7 +38,7 @@ public class AttachmentDatabaseTest extends TextSecureTestCase {
|
||||
final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID);
|
||||
|
||||
DatabaseAttachment mockAttachment = getMockAttachment("x/x");
|
||||
when(database.getAttachment(attachmentId)).thenReturn(mockAttachment);
|
||||
when(database.getAttachment(null, attachmentId)).thenReturn(mockAttachment);
|
||||
|
||||
InputStream mockInputStream = mock(InputStream.class);
|
||||
doReturn(mockInputStream).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail"));
|
||||
@ -52,7 +52,7 @@ public class AttachmentDatabaseTest extends TextSecureTestCase {
|
||||
final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID);
|
||||
|
||||
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"));
|
||||
doNothing().when(database).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat());
|
||||
|
Loading…
Reference in New Issue
Block a user