diff --git a/build.gradle b/build.gradle
index d6744a7631..3b74f553eb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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',
diff --git a/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..84755e4881
Binary files /dev/null and b/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..b51ce3ed95
Binary files /dev/null and b/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..798ebd4e25
Binary files /dev/null and b/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..f3e153b45e
Binary files /dev/null and b/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..5bd56903d0
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/layout/conversation_activity_attachment_editor_stub.xml b/res/layout/conversation_activity_attachment_editor_stub.xml
index 07cb9a1353..6e88fadacd 100644
--- a/res/layout/conversation_activity_attachment_editor_stub.xml
+++ b/res/layout/conversation_activity_attachment_editor_stub.xml
@@ -41,6 +41,18 @@
app:foregroundTintColor="@color/grey_500"
app:backgroundTintColor="?conversation_item_bubble_background"/>
+
+
+
diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml
index 7573f4755a..3752f55781 100644
--- a/res/layout/conversation_item_received.xml
+++ b/res/layout/conversation_item_received.xml
@@ -61,6 +61,11 @@
android:layout_width="210dp"
android:layout_height="wrap_content"/>
+
+
+
diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml
index 3025880ae7..cdd345a9f8 100644
--- a/res/layout/conversation_item_sent.xml
+++ b/res/layout/conversation_item_sent.xml
@@ -50,6 +50,11 @@
android:layout_width="210dp"
android:layout_height="wrap_content"/>
+
+
+
diff --git a/res/layout/document_view.xml b/res/layout/document_view.xml
new file mode 100644
index 0000000000..1eff4c0cb7
--- /dev/null
+++ b/res/layout/document_view.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 665975a668..38c8f71048 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -206,22 +206,26 @@
- image
- audio
- video
+ - documents
- @string/arrays__images
- @string/arrays__audio
- @string/arrays__video
+ - @string/arrays__documents
- image
+ - audio
- image
- audio
- video
+ - documents
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 4991cff627..a94f4942c2 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -197,4 +197,10 @@
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c75345feb1..af19638885 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1063,6 +1063,7 @@
Images
Audio
Video
+ Documents
@@ -1343,6 +1344,8 @@
Transport icon
+ Open Directory
+ unknown file
diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java
index 71f6a70a35..86ad92eacb 100644
--- a/src/org/thoughtcrime/securesms/ConversationAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java
@@ -83,6 +83,8 @@ public class ConversationAdapter
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 batchSelected = Collections.synchronizedSet(new HashSet());
@@ -223,9 +225,11 @@ public class ConversationAdapter
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
} 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
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;
}
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index 707932702e..f1797c3507 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -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;
}
}
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index 181ed5e9ac..474e919277 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -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 mediaThumbnailStub;
private @NonNull Stub audioViewStub;
+ private @NonNull Stub 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;
diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java
index f424f2f10c..8a506bdbd7 100644
--- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java
+++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java
@@ -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 pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments();
+ final List pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(masterSecret);
Log.w(TAG, pendingAttachments.size() + " pending parts.");
for (DatabaseAttachment attachment : pendingAttachments) {
diff --git a/src/org/thoughtcrime/securesms/MediaAdapter.java b/src/org/thoughtcrime/securesms/MediaAdapter.java
index e5011ee1c8..140aacabb5 100644
--- a/src/org/thoughtcrime/securesms/MediaAdapter.java
+++ b/src/org/thoughtcrime/securesms/MediaAdapter.java
@@ -67,7 +67,7 @@ public class MediaAdapter extends CursorRecyclerViewAdapter {
@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());
diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
index 0d68038271..2552e08c16 100644
--- a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java
@@ -166,10 +166,11 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
List 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 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();
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index db2090a0ed..f56673fb92 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -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));
}
});
}
diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java
index bb9c8245cb..26e91a0190 100644
--- a/src/org/thoughtcrime/securesms/attachments/Attachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
index cfdaabc08a..18b08dbdfa 100644
--- a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
index 5bc46303a3..f885e93856 100644
--- a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
index f90e32a565..611c5dd12b 100644
--- a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java
@@ -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()));
diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
index c7eebf4d50..8ccf036297 100644
--- a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
+++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java
@@ -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;
}
diff --git a/src/org/thoughtcrime/securesms/components/DocumentView.java b/src/org/thoughtcrime/securesms/components/DocumentView.java
new file mode 100644
index 0000000000..933823c733
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/DocumentView.java
@@ -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 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);
+ }
+ }
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java b/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
index 3b50ecd62e..582a4e7826 100644
--- a/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
+++ b/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index fae47e9c62..2b18822cc8 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -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 getAttachmentsForMessage(long mmsId) {
+ public @NonNull List getAttachmentsForMessage(@Nullable MasterSecret masterSecret, long mmsId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List 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 getPendingAttachments() {
+ public @NonNull List getPendingAttachments(@NonNull MasterSecret masterSecret) {
final SQLiteDatabase database = databaseHelper.getReadableDatabase();
final List 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 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 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;
diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
index dc8b90fd66..f8aef10c05 100644
--- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
+++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
@@ -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();
}
diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
index 5c71a5d63f..44cf8b8836 100644
--- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java
@@ -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() {
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index 64ebab704d..8afab52905 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -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 attachments = new LinkedList(attachmentDatabase.getAttachmentsForMessage(messageId));
+ List attachments = new LinkedList(attachmentDatabase.getAttachmentsForMessage(masterSecret, messageId));
MmsAddresses addresses = addr.getAddressesForId(messageId);
List 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);
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index ee1526cc47..57b84241d0 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -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);
diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java
index 9e0842bb8f..896bdde819 100644
--- a/src/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -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);
diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
index f3339bd53f..1360a3ab68 100644
--- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java
@@ -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);
diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentFileNameJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentFileNameJob.java
new file mode 100644
index 0000000000..83325f893d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobs/AttachmentFileNameJob.java
@@ -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;
+ }
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
index 24cbc444ab..586514e50a 100644
--- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java
@@ -65,6 +65,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
byte[] key = record.getAvatarKey();
String relay = record.getRelay();
Optional digest = Optional.fromNullable(record.getAvatarDigest());
+ Optional 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);
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
index 277f054f27..408b1451df 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java
@@ -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));
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 3b181d0518..ac26ab3e56 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -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 = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
if (insertResult.isPresent()) {
- List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
+ List 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()));
diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 1eb8d6bba3..6462bd3574 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -74,27 +74,23 @@ public abstract class PushSendJob extends SendJob {
List 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);
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java
index 7e361599b2..24aaa19c7d 100644
--- a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java
+++ b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java
@@ -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 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);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index a055f4d69d..81294e9a96 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -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 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() {
+ new AsyncTask() {
@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;
}
+
}
}
diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java
index 805a61ec3c..435e5d1fa0 100644
--- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java
new file mode 100644
index 0000000000..495fe0284b
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java
@@ -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;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/mms/GifSlide.java b/src/org/thoughtcrime/securesms/mms/GifSlide.java
index 5a2c614b8d..f7d5acb2ab 100644
--- a/src/org/thoughtcrime/securesms/mms/GifSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/GifSlide.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java
index 2324309306..af0a0690d0 100644
--- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java
index 71550bee5f..233e52d57e 100644
--- a/src/org/thoughtcrime/securesms/mms/Slide.java
+++ b/src/org/thoughtcrime/securesms/mms/Slide.java
@@ -60,6 +60,15 @@ public abstract class Slide {
return Optional.absent();
}
+ @NonNull
+ public Optional 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 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
diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
index 6d83e777e0..b3328d48e1 100644
--- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java
+++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java
@@ -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;
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java
index 465e91ff34..d5f85f0d98 100644
--- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java
@@ -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) {
diff --git a/src/org/thoughtcrime/securesms/providers/PartProvider.java b/src/org/thoughtcrime/securesms/providers/PartProvider.java
index 6e134ada43..c1f0e17a15 100644
--- a/src/org/thoughtcrime/securesms/providers/PartProvider.java
+++ b/src/org/thoughtcrime/securesms/providers/PartProvider.java
@@ -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;iint in the range
+ * 0 to 255. If no byte is available
+ * because the end of the stream has been reached, the value
+ * -1 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 in.read() and returns the result.
+ *
+ * @return the next byte of data, or -1 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 len bytes of data from this input stream
+ * into an array of bytes. If len is not zero, the method
+ * blocks until some input is available; otherwise, no
+ * bytes are read and 0 is returned.
+ *
+ * This method simply performs in.read(b, off, len)
+ * and returns the result.
+ *
+ * @param b the buffer into which the data is read.
+ * @param off The start offset in the destination array
+ * b.
+ * @param len the maximum number of bytes read.
+ * @return the total number of bytes read into the buffer, or
+ * -1 if there is no more data because the end of
+ * the stream has been reached.
+ * @exception NullPointerException If b is null.
+ * @exception IndexOutOfBoundsException If off is negative,
+ * len is negative, or len is greater than
+ * b.length - off
+ * @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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java
index a8c4024ad4..793df1ce3e 100644
--- a/src/org/thoughtcrime/securesms/util/MediaUtil.java
+++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/util/MemoryFileUtil.java b/src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
new file mode 100644
index 0000000000..06a4cecd1e
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
@@ -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);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
index fa601cadd5..21e9ea0f0d 100644
--- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
+++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
@@ -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 {
+public class SaveAttachmentTask extends ProgressDialogAsyncTask> {
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 contextReference;
+ private final WeakReference contextReference;
private final WeakReference masterSecretReference;
+ private final WeakReference 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 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 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 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;
}
diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java
index 2b0d699103..7b8f959f95 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -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];
+ }
}
diff --git a/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java b/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java
index 4924adfc59..1132c1dd27 100644
--- a/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java
+++ b/src/org/thoughtcrime/securesms/video/EncryptedMediaDataSource.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java
index 42a297965d..7204296667 100644
--- a/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java
+++ b/src/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.java
@@ -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;
diff --git a/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java b/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java
index c791b31c94..165110cdac 100644
--- a/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java
+++ b/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java
@@ -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());