From f67eb5f9f3efc688cec1d1690bd379d18e5001b0 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Tue, 28 Mar 2017 12:05:30 -0700 Subject: [PATCH] Support for receiving arbitrary attachment types // FREEBIE --- build.gradle | 6 +- .../ic_insert_drive_file_white_24dp.png | Bin 0 -> 153 bytes .../ic_insert_drive_file_white_24dp.png | Bin 0 -> 133 bytes .../ic_insert_drive_file_white_24dp.png | Bin 0 -> 206 bytes .../ic_insert_drive_file_white_24dp.png | Bin 0 -> 283 bytes .../ic_insert_drive_file_white_24dp.png | Bin 0 -> 372 bytes ...sation_activity_attachment_editor_stub.xml | 12 + res/layout/conversation_item_received.xml | 5 + .../conversation_item_received_document.xml | 12 + res/layout/conversation_item_sent.xml | 5 + .../conversation_item_sent_document.xml | 12 + res/layout/document_view.xml | 105 ++++++++ res/values/arrays.xml | 4 + res/values/attrs.xml | 6 + res/values/strings.xml | 3 + .../securesms/ConversationAdapter.java | 11 + .../securesms/ConversationFragment.java | 6 +- .../securesms/ConversationItem.java | 85 ++++-- .../securesms/DatabaseUpgradeActivity.java | 5 +- .../thoughtcrime/securesms/MediaAdapter.java | 2 +- .../securesms/MediaOverviewActivity.java | 7 +- .../securesms/MediaPreviewActivity.java | 4 +- .../securesms/attachments/Attachment.java | 11 +- .../attachments/DatabaseAttachment.java | 5 +- .../MmsNotificationAttachment.java | 2 +- .../attachments/PointerAttachment.java | 6 +- .../securesms/attachments/UriAttachment.java | 11 +- .../securesms/components/DocumentView.java | 196 ++++++++++++++ .../crypto/DecryptingPartInputStream.java | 243 +++++------------- .../database/AttachmentDatabase.java | 68 ++++- .../securesms/database/DatabaseFactory.java | 9 +- .../securesms/database/MediaDatabase.java | 62 ++--- .../securesms/database/MmsDatabase.java | 6 +- .../securesms/database/MmsSmsDatabase.java | 5 + .../securesms/groups/GroupManager.java | 2 +- .../securesms/jobs/AttachmentDownloadJob.java | 4 +- .../securesms/jobs/AttachmentFileNameJob.java | 86 +++++++ .../securesms/jobs/AvatarDownloadJob.java | 3 +- .../securesms/jobs/MmsDownloadJob.java | 8 +- .../securesms/jobs/PushDecryptJob.java | 12 +- .../securesms/jobs/PushSendJob.java | 38 ++- .../requirements/MediaNetworkRequirement.java | 21 +- .../securesms/mms/AttachmentManager.java | 71 +++-- .../securesms/mms/AudioSlide.java | 4 +- .../securesms/mms/DocumentSlide.java | 29 +++ .../thoughtcrime/securesms/mms/GifSlide.java | 2 +- .../securesms/mms/ImageSlide.java | 2 +- src/org/thoughtcrime/securesms/mms/Slide.java | 26 +- .../thoughtcrime/securesms/mms/SlideDeck.java | 12 +- .../securesms/mms/VideoSlide.java | 2 +- .../securesms/providers/PartProvider.java | 100 ++++--- .../providers/PersistentBlobProvider.java | 2 +- .../securesms/util/LimitedInputStream.java | 120 +++++++++ .../securesms/util/MediaUtil.java | 3 + .../securesms/util/MemoryFileUtil.java | 41 +++ .../securesms/util/SaveAttachmentTask.java | 122 ++++++--- src/org/thoughtcrime/securesms/util/Util.java | 31 +++ .../video/EncryptedMediaDataSource.java | 13 +- .../webrtc/PeerConnectionWrapper.java | 2 + .../database/AttachmentDatabaseTest.java | 4 +- 60 files changed, 1251 insertions(+), 423 deletions(-) create mode 100644 res/drawable-hdpi/ic_insert_drive_file_white_24dp.png create mode 100644 res/drawable-mdpi/ic_insert_drive_file_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png create mode 100644 res/layout/conversation_item_received_document.xml create mode 100644 res/layout/conversation_item_sent_document.xml create mode 100644 res/layout/document_view.xml create mode 100644 src/org/thoughtcrime/securesms/components/DocumentView.java create mode 100644 src/org/thoughtcrime/securesms/jobs/AttachmentFileNameJob.java create mode 100644 src/org/thoughtcrime/securesms/mms/DocumentSlide.java create mode 100644 src/org/thoughtcrime/securesms/util/LimitedInputStream.java create mode 100644 src/org/thoughtcrime/securesms/util/MemoryFileUtil.java 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 0000000000000000000000000000000000000000..84755e48817d86d61d09e70489b6c94b5c1f4d38 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8f~SjPh{y4_XAbf<81OJ3(CJ7} zndX``b76v-N7DYNg?AXzx0$@j_%O$-MDb9#M4)m=#h>FoD;{~Q*wnG^Q^%Sa9jkV9 zthk}E_JdQ<#r;ZwuA-qP*1MK2|9P?TocWEMO!)@USli?GV)cP`F?hQAxvX~5;q+UmdnXs1e(j>>FVdQ&MBb@07N}3VE_OC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..798ebd4e25f68b658c82e773eea97bd2ad412f17 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0Dsh%#5ArXh)UUTGX4&Z5dST1#X z_fmE(jjwTCSC_smS-D$9cp}$l`2@QYT1!{h^YDMEIJ_$LxwOxRqkmdKI;Vst E0NwCNssI20 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f3e153b45eb7886c314afd8642bc9018c1f2b5bd GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw9(lSrhGg7(d+8wOAqSC`hqkR( z1O(pr7kRR-^fM4B-pJrB!18rLU;5WcwbqN9K8JrW^zk~kQ@@Q%*UUm`d)(nn1I4KI z5$WBNZm6sYkec37%CT|!!~%}SX+}cp1pU2?gz^k0^oX!`dZ;RJuH;csnA-GWNs++J zBMgC_G6EBiFkSSp5$HU^66g_g!CF(k6j7})P zi3R_eVwcPdw#%5()K(Lq&bsyI0x4_Bbf>AG4m-Kby09c-#l^%UGs{zXGMryqJkPLY Y&+MxXFXli2*NgVS zce*$hNuLo@ada)wwkl-hV=5FsC)E4*jDt)D|B?8wr!IWgTTscqYIn4V0dUAW1Y}9z7KOhqi^sPPIw7!dLw*NrLf;=eP`^JY9k?(#X=nCd zcgzlKvHty0=g_lyrE}%$KcD@NJa^2A$V!HX=v7zCg9d&s2ko((e7)78&qol`;+ E02^zJCIA2c literal 0 HcmV?d00001 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());