Support for receiving arbitrary attachment types

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

View File

@ -59,7 +59,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems: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',

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

View File

@ -41,6 +41,18 @@
app:foregroundTintColor="@color/grey_500"
app: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>

View File

@ -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"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.DocumentView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/document_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:documentForegroundTintColor="@color/white"
app:documentBackgroundTintColor="@color/blue_500"
tools:visibility="visible"/>

View File

@ -50,6 +50,11 @@
android:layout_width="210dp"
android:layout_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"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.DocumentView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/document_view"
android:layout_width="210dp"
android:layout_height="wrap_content"
app:documentForegroundTintColor="@color/grey_500"
app:documentBackgroundTintColor="@color/white"
android:visibility="gone"
tools:visibility="visible"/>

View File

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

View File

@ -206,22 +206,26 @@
<item>image</item>
<item>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" />

View File

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

View File

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

View File

@ -83,6 +83,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4;
private static final int MESSAGE_TYPE_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;
}

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,213 +16,100 @@
*/
package org.thoughtcrime.securesms.crypto;
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;

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,6 +65,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
byte[] key = record.getAvatarKey();
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

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

View File

@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.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;

View File

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

View File

@ -2,12 +2,16 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.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;
}

View File

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

View File

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

View File

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

View File

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