diff --git a/build.gradle b/build.gradle
index d6744a7631..3b74f553eb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -59,7 +59,7 @@ dependencies {
 
     compile 'org.whispersystems:jobmanager:1.0.2'
     compile 'org.whispersystems:libpastelog:1.0.7'
-    compile 'org.whispersystems:signal-service-android:2.5.3'
+    compile 'org.whispersystems:signal-service-android:2.5.5'
     compile 'org.whispersystems:webrtc-android:M57-S2'
 
     compile "me.leolin:ShortcutBadger:1.10-WS1"
@@ -129,7 +129,7 @@ dependencyVerification {
         'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
         'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
         'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
-        'org.whispersystems:signal-service-android:28a5368cb1336106ba7732aeaf0c5a33ef8fb22500c41f38ad8147375f59073b',
+        'org.whispersystems:signal-service-android:3d7859b194e518fbaf5a082daf22ca345411705e825791f751eb388f149583c3',
         'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
         'me.leolin:ShortcutBadger:e8e39df8a59d8211a30f40b1eeab21b3fa57b3f3e0f03abb995f82d66588778c',
         'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@@ -165,7 +165,7 @@ dependencyVerification {
         'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
         'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
         'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19',
-        'org.whispersystems:signal-service-java:969b4e1fb0b87e553d8b231a090002a03748e0444fa23afa1bc6f7065e8039ff',
+        'org.whispersystems:signal-service-java:4d51d423510bcc3f3a0db1a2c5c7164e379af7ad7f9c20cf0faa753eef9f3f27',
         'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
         'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
         'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
diff --git a/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..84755e4881
Binary files /dev/null and b/res/drawable-hdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..b51ce3ed95
Binary files /dev/null and b/res/drawable-mdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..798ebd4e25
Binary files /dev/null and b/res/drawable-xhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..f3e153b45e
Binary files /dev/null and b/res/drawable-xxhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png b/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png
new file mode 100644
index 0000000000..5bd56903d0
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_insert_drive_file_white_24dp.png differ
diff --git a/res/layout/conversation_activity_attachment_editor_stub.xml b/res/layout/conversation_activity_attachment_editor_stub.xml
index 07cb9a1353..6e88fadacd 100644
--- a/res/layout/conversation_activity_attachment_editor_stub.xml
+++ b/res/layout/conversation_activity_attachment_editor_stub.xml
@@ -41,6 +41,18 @@
                 app:foregroundTintColor="@color/grey_500"
                 app:backgroundTintColor="?conversation_item_bubble_background"/>
 
+        <org.thoughtcrime.securesms.components.DocumentView
+                android:id="@+id/attachment_document"
+                android:layout_width="210dp"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                android:paddingTop="15dp"
+                android:paddingBottom="15dp"
+                app:documentWidgetBackground="?conversation_item_bubble_background"
+                app:documentForegroundTintColor="@color/grey_500"
+                app:documentBackgroundTintColor="?conversation_item_bubble_background"/>
+
+
     </org.thoughtcrime.securesms.components.RemovableEditableMediaView>
 
 </FrameLayout>
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"/>
 
+            <ViewStub android:id="@+id/document_view_stub"
+                      android:layout="@layout/conversation_item_received_document"
+                      android:layout_width="210dp"
+                      android:layout_height="wrap_content"/>
+
             <org.thoughtcrime.securesms.components.emoji.EmojiTextView
                     android:id="@+id/conversation_item_body"
                     android:layout_width="wrap_content"
diff --git a/res/layout/conversation_item_received_document.xml b/res/layout/conversation_item_received_document.xml
new file mode 100644
index 0000000000..e449ea4748
--- /dev/null
+++ b/res/layout/conversation_item_received_document.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.thoughtcrime.securesms.components.DocumentView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:id="@+id/document_view"
+        android:layout_width="210dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:documentForegroundTintColor="@color/white"
+        app:documentBackgroundTintColor="@color/blue_500"
+        tools:visibility="visible"/>
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"/>
 
+            <ViewStub android:id="@+id/document_view_stub"
+                      android:layout="@layout/conversation_item_sent_document"
+                      android:layout_width="210dp"
+                      android:layout_height="wrap_content"/>
+
             <org.thoughtcrime.securesms.components.emoji.EmojiTextView
                     android:id="@+id/conversation_item_body"
                     android:autoLink="all"
diff --git a/res/layout/conversation_item_sent_document.xml b/res/layout/conversation_item_sent_document.xml
new file mode 100644
index 0000000000..27e5dc5238
--- /dev/null
+++ b/res/layout/conversation_item_sent_document.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.thoughtcrime.securesms.components.DocumentView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:id="@+id/document_view"
+        android:layout_width="210dp"
+        android:layout_height="wrap_content"
+        app:documentForegroundTintColor="@color/grey_500"
+        app:documentBackgroundTintColor="@color/white"
+        android:visibility="gone"
+        tools:visibility="visible"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:tools="http://schemas.android.com/tools"
+       xmlns:app="http://schemas.android.com/apk/res-auto"
+       tools:context="org.thoughtcrime.securesms.components.DocumentView">
+
+        <LinearLayout android:id="@+id/document_container"
+                      android:layout_width="fill_parent"
+                      android:layout_height="wrap_content"
+                      android:clickable="false"
+                      android:focusable="false"
+                      android:orientation="horizontal">
+
+            <org.thoughtcrime.securesms.components.AnimatingToggle
+                    android:id="@+id/control_toggle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:clickable="false"
+                    android:focusable="false"
+                    android:gravity="center">
+
+                <com.pnikosis.materialishprogress.ProgressWheel
+                        android:id="@+id/download_progress"
+                        android:layout_width="48dp"
+                        android:layout_height="48dp"
+                        android:visibility="gone"
+                        android:clickable="false"
+                        android:layout_gravity="center"
+                        app:matProg_barColor="@color/white"
+                        app:matProg_linearProgress="true"
+                        app:matProg_spinSpeed="0.333"
+                        tools:visibility="gone"/>
+
+                <FrameLayout android:id="@+id/document_background"
+                             android:layout_width="wrap_content"
+                             android:layout_height="wrap_content"
+                             android:layout_gravity="center_vertical"
+                             android:gravity="center_vertical"
+                             android:background="@drawable/ic_circle_fill_white_48dp"
+                             android:visibility="visible"
+                             android:clickable="false"
+                             android:focusable="false"
+                             tools:backgroundTint="@color/blue_400">
+
+                    <TextView android:id="@+id/document"
+                              android:layout_width="wrap_content"
+                              android:layout_height="wrap_content"
+                              android:layout_gravity="center"
+                              android:gravity="center"
+                              android:clickable="false"
+                              android:visibility="visible"
+                              android:background="@drawable/ic_insert_drive_file_white_24dp"
+                              android:textAlignment="center"
+                              android:scaleType="centerInside"
+                              android:textAllCaps="true"
+                              android:textSize="8sp"
+                              android:paddingTop="8dp"
+                              android:typeface="monospace"
+                              tools:visibility="visible"
+                              tools:text="PDF"
+                              tools:textColor="@color/blue_400"/>
+
+                </FrameLayout>
+
+                <ImageView android:id="@+id/download"
+                           android:layout_width="wrap_content"
+                           android:layout_height="wrap_content"
+                           android:layout_gravity="center_vertical"
+                           android:clickable="true"
+                           android:visibility="gone"
+                           android:background="@drawable/circle_touch_highlight_background"
+                           android:src="@drawable/ic_download_circle_fill_white_48dp"
+                           android:contentDescription="@string/audio_view__download_accessibility_description"/>
+
+            </org.thoughtcrime.securesms.components.AnimatingToggle>
+
+            <LinearLayout android:orientation="vertical"
+                          android:layout_marginLeft="7dp"
+                          android:layout_gravity="center_vertical"
+                          android:layout_width="match_parent"
+                          android:focusable="false"
+                          android:clickable="false"
+                          android:layout_height="wrap_content">
+
+                <TextView android:id="@+id/file_name"
+                          android:layout_width="match_parent"
+                          android:layout_height="wrap_content"
+                          android:textStyle="bold"
+                          android:singleLine="true"
+                          android:maxLines="1"
+                          android:clickable="false"
+                          android:ellipsize="end"
+                          tools:text="The-Anarchist-Tension-by-Alfredo-Bonanno.pdf"/>
+
+                <TextView android:id="@+id/file_size"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:layout_marginTop="5dp"
+                          android:textSize="12sp"
+                          android:clickable="false"
+                          tools:text="24kb"/>
+            </LinearLayout>
+        </LinearLayout>
+</merge>
\ 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 @@
       <item>image</item>
       <item>audio</item>
       <item>video</item>
+      <item>documents</item>
   </string-array>
 
   <string-array name="pref_media_download_values">
       <item>@string/arrays__images</item>
       <item>@string/arrays__audio</item>
       <item>@string/arrays__video</item>
+      <item>@string/arrays__documents</item>
   </string-array>
 
   <string-array name="pref_media_download_mobile_data_default">
       <item>image</item>
+      <item>audio</item>
   </string-array>
 
   <string-array name="pref_media_download_wifi_default">
       <item>image</item>
       <item>audio</item>
       <item>video</item>
+      <item>documents</item>
   </string-array>
 
   <string-array name="pref_media_download_roaming_default" />
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 @@
         <attr name="pickerColors" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="DocumentView">
+        <attr name="documentWidgetBackground" format="color"/>
+        <attr name="documentForegroundTintColor" format="color" />
+        <attr name="documentBackgroundTintColor" format="color" />
+    </declare-styleable>
+
 </resources>
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 @@
     <string name="arrays__images">Images</string>
     <string name="arrays__audio">Audio</string>
     <string name="arrays__video">Video</string>
+    <string name="arrays__documents">Documents</string>
 
     <!-- plurals.xml -->
     <plurals name="hours_ago">
@@ -1343,6 +1344,8 @@
 
     <!-- transport_selection_list_item -->
     <string name="transport_selection_list_item__transport_icon">Transport icon</string>
+    <string name="SaveAttachmentTask_open_directory">Open Directory</string>
+    <string name="DocumentView_unknown_file">unknown file</string>
 
     <!-- EOF -->
 
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 <V extends View & BindableConversationItem>
   private static final int MESSAGE_TYPE_AUDIO_INCOMING     = 4;
   private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5;
   private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6;
+  private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING  = 7;
+  private static final int MESSAGE_TYPE_DOCUMENT_INCOMING  = 8;
 
   private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
 
@@ -223,9 +225,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
     switch (viewType) {
       case MESSAGE_TYPE_AUDIO_OUTGOING:
       case MESSAGE_TYPE_THUMBNAIL_OUTGOING:
+      case MESSAGE_TYPE_DOCUMENT_OUTGOING:
       case MESSAGE_TYPE_OUTGOING:        return R.layout.conversation_item_sent;
       case MESSAGE_TYPE_AUDIO_INCOMING:
       case MESSAGE_TYPE_THUMBNAIL_INCOMING:
+      case MESSAGE_TYPE_DOCUMENT_INCOMING:
       case MESSAGE_TYPE_INCOMING:        return R.layout.conversation_item_received;
       case MESSAGE_TYPE_UPDATE:          return R.layout.conversation_item_update;
       default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter");
@@ -242,6 +246,9 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
     } else if (hasAudio(messageRecord)) {
       if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
       else                            return MESSAGE_TYPE_AUDIO_INCOMING;
+    } else if (hasDocument(messageRecord)) {
+      if (messageRecord.isOutgoing()) return MESSAGE_TYPE_DOCUMENT_OUTGOING;
+      else                            return MESSAGE_TYPE_DOCUMENT_INCOMING;
     } else if (hasThumbnail(messageRecord)) {
       if (messageRecord.isOutgoing()) return MESSAGE_TYPE_THUMBNAIL_OUTGOING;
       else                            return MESSAGE_TYPE_THUMBNAIL_INCOMING;
@@ -315,6 +322,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
     return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getAudioSlide() != null;
   }
 
+  private boolean hasDocument(MessageRecord messageRecord) {
+    return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
+  }
+
   private boolean hasThumbnail(MessageRecord messageRecord) {
     return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
   }
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<ThumbnailView> mediaThumbnailStub;
   private @NonNull  Stub<AudioView>     audioViewStub;
+  private @NonNull  Stub<DocumentView>  documentViewStub;
   private @NonNull  ExpirationTimerView expirationTimer;
 
   private int defaultBubbleColor;
@@ -153,6 +157,7 @@ public class ConversationItem extends LinearLayout
     this.bodyBubble              =                      findViewById(R.id.body_bubble);
     this.mediaThumbnailStub      = new Stub<>((ViewStub) findViewById(R.id.image_view_stub));
     this.audioViewStub           = new Stub<>((ViewStub) findViewById(R.id.audio_view_stub));
+    this.documentViewStub        = new Stub<>((ViewStub) findViewById(R.id.document_view_stub));
     this.expirationTimer         = (ExpirationTimerView) findViewById(R.id.expiration_indicator);
 
     setOnClickListener(new ClickListener(null));
@@ -229,6 +234,10 @@ public class ConversationItem extends LinearLayout
     if (audioViewStub.resolved()) {
       setAudioViewTint(messageRecord, conversationRecipients);
     }
+
+    if (documentViewStub.resolved()) {
+      setDocumentViewTint(messageRecord, conversationRecipients);
+    }
   }
 
   private void setAudioViewTint(MessageRecord messageRecord, Recipients recipients) {
@@ -243,6 +252,18 @@ public class ConversationItem extends LinearLayout
     }
   }
 
+  private void setDocumentViewTint(MessageRecord messageRecord, Recipients recipients) {
+    if (messageRecord.isOutgoing()) {
+      if (DynamicTheme.LIGHT.equals(TextSecurePreferences.getTheme(context))) {
+        documentViewStub.get().setTint(recipients.getColor().toConversationColor(context), defaultBubbleColor);
+      } else {
+        documentViewStub.get().setTint(Color.WHITE, defaultBubbleColor);
+      }
+    } else {
+      documentViewStub.get().setTint(Color.WHITE, recipients.getColor().toConversationColor(context));
+    }
+  }
+
   private void setInteractionState(MessageRecord messageRecord) {
     setSelected(batchSelected.contains(messageRecord));
     bodyText.setAutoLinkMask(batchSelected.isEmpty() ? Linkify.ALL : 0);
@@ -258,6 +279,11 @@ public class ConversationItem extends LinearLayout
       audioViewStub.get().setClickable(batchSelected.isEmpty());
       audioViewStub.get().setEnabled(batchSelected.isEmpty());
     }
+
+    if (documentViewStub.resolved()) {
+      documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty());
+      documentViewStub.get().setClickable(batchSelected.isEmpty());
+    }
   }
 
   private boolean isCaptionlessMms(MessageRecord messageRecord) {
@@ -272,6 +298,10 @@ public class ConversationItem extends LinearLayout
     return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
   }
 
+  private boolean hasDocument(MessageRecord messageRecord) {
+    return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide() != null;
+  }
+
   private void setBodyText(MessageRecord messageRecord) {
     bodyText.setClickable(false);
     bodyText.setFocusable(false);
@@ -290,6 +320,7 @@ public class ConversationItem extends LinearLayout
     if (hasAudio(messageRecord)) {
       audioViewStub.get().setVisibility(View.VISIBLE);
       if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
+      if (documentViewStub.resolved())   documentViewStub.get().setVisibility(View.GONE);
 
       //noinspection ConstantConditions
       audioViewStub.get().setAudio(masterSecret, ((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls);
@@ -297,9 +328,22 @@ public class ConversationItem extends LinearLayout
       audioViewStub.get().setOnLongClickListener(passthroughClickListener);
 
       bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+    } else if (hasDocument(messageRecord)) {
+      documentViewStub.get().setVisibility(View.VISIBLE);
+      if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
+      if (audioViewStub.resolved())      audioViewStub.get().setVisibility(View.GONE);
+
+      //noinspection ConstantConditions
+      documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls);
+      documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
+      documentViewStub.get().setDownloadClickListener(downloadClickListener);
+      documentViewStub.get().setOnLongClickListener(passthroughClickListener);
+
+      bodyText.setLayoutParams(new ActionBar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
     } else if (hasThumbnail(messageRecord)) {
       mediaThumbnailStub.get().setVisibility(View.VISIBLE);
-      if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
+      if (audioViewStub.resolved())    audioViewStub.get().setVisibility(View.GONE);
+      if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
 
       //noinspection ConstantConditions
       mediaThumbnailStub.get().setImageResource(masterSecret,
@@ -314,6 +358,7 @@ public class ConversationItem extends LinearLayout
     } else {
       if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
       if (audioViewStub.resolved())      audioViewStub.get().setVisibility(View.GONE);
+      if (documentViewStub.resolved())   documentViewStub.get().setVisibility(View.GONE);
       bodyText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
     }
   }
@@ -498,19 +543,6 @@ public class ConversationItem extends LinearLayout
   }
 
   private class ThumbnailClickListener implements SlideClickListener {
-    private void fireIntent(Slide slide) {
-      Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
-      Intent intent = new Intent(Intent.ACTION_VIEW);
-      intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-      intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
-      try {
-        context.startActivity(intent);
-      } catch (ActivityNotFoundException anfe) {
-        Log.w(TAG, "No activity existed to view the media.");
-        Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
-      }
-    }
-
     public void onClick(final View v, final Slide slide) {
       if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) {
         performClick();
@@ -525,18 +557,18 @@ public class ConversationItem extends LinearLayout
 
         context.startActivity(intent);
       } else if (slide.getUri() != null) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(context);
-        builder.setTitle(R.string.ConversationItem_view_secure_media_question);
-        builder.setIconAttribute(R.attr.dialog_alert_icon);
-        builder.setCancelable(true);
-        builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning);
-        builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
-          public void onClick(DialogInterface dialog, int which) {
-            fireIntent(slide);
-          }
-        });
-        builder.setNegativeButton(R.string.no, null);
-        builder.show();
+        Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
+        Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());
+        Log.w(TAG, "Public URI: " + publicUri);
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
+        try {
+          context.startActivity(intent);
+        } catch (ActivityNotFoundException anfe) {
+          Log.w(TAG, "No activity existed to view the media.");
+          Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
+        }
       }
     }
   }
@@ -554,6 +586,7 @@ public class ConversationItem extends LinearLayout
       performClick();
     }
   }
+
   private class ClickListener implements View.OnClickListener {
     private OnClickListener parent;
 
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<DatabaseAttachment> pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments();
+      final List<DatabaseAttachment> pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(masterSecret);
 
       Log.w(TAG, pendingAttachments.size() + " pending parts.");
       for (DatabaseAttachment attachment : pendingAttachments) {
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<ViewHolder> {
   @Override
   public void onBindItemViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) {
     final ThumbnailView imageView   = viewHolder.imageView;
-    final MediaRecord   mediaRecord = MediaRecord.from(cursor);
+    final MediaRecord   mediaRecord = MediaRecord.from(getContext(), masterSecret, cursor);
 
     Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());
 
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<SaveAttachmentTask.Attachment> attachments = new ArrayList<>(cursor.getCount());
 
             while (cursor != null && cursor.moveToNext()) {
-              MediaRecord record = MediaRecord.from(cursor);
+              MediaRecord record = MediaRecord.from(c, masterSecret, cursor);
               attachments.add(new SaveAttachmentTask.Attachment(record.getAttachment().getDataUri(),
                                                                 record.getContentType(),
-                                                                record.getDate()));
+                                                                record.getDate(),
+                                                                null));
             }
 
             return attachments;
@@ -179,7 +180,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i
           protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
             super.onPostExecute(attachments);
 
-            SaveAttachmentTask saveTask = new SaveAttachmentTask(c, masterSecret, attachments.size());
+            SaveAttachmentTask saveTask = new SaveAttachmentTask(c, masterSecret, gridView, attachments.size());
             saveTask.execute(attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
           }
         }.execute();
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<String> fileName) {
+    if (!fileName.isPresent()) return "";
+
+    String[] parts = fileName.get().split("\\.");
+
+    if (parts.length < 2) {
+      return "";
+    }
+
+    String suffix = parts[parts.length - 1];
+
+    if (suffix.length() <= 3) {
+      return suffix;
+    }
+
+    return "";
+  }
+
+  @Subscribe(sticky = true, threadMode = ThreadMode.ASYNC)
+  public void onEventAsync(final PartProgressEvent event) {
+    if (documentSlide != null && event.attachment.equals(this.documentSlide.asAttachment())) {
+      Util.runOnMain(new Runnable() {
+        @Override
+        public void run() {
+          downloadProgress.setInstantProgress(((float) event.progress) / event.total);
+        }
+      });
+    }
+  }
+
+  private class DownloadClickedListener implements View.OnClickListener {
+    private final @NonNull DocumentSlide slide;
+
+    private DownloadClickedListener(@NonNull DocumentSlide slide) {
+      this.slide = slide;
+    }
+
+    @Override
+    public void onClick(View v) {
+      if (downloadListener != null) downloadListener.onClick(v, slide);
+    }
+  }
+
+  private class OpenClickedListener implements View.OnClickListener {
+    private final @NonNull DocumentSlide slide;
+
+    private OpenClickedListener(@NonNull DocumentSlide slide) {
+      this.slide = slide;
+    }
+
+    @Override
+    public void onClick(View v) {
+      if (!slide.isPendingDownload() && !slide.isInProgress() && viewListener != null) {
+        viewListener.onClick(v, slide);
+      }
+    }
+  }
+
+}
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<DatabaseAttachment> getAttachmentsForMessage(long mmsId) {
+  public @NonNull List<DatabaseAttachment> getAttachmentsForMessage(@Nullable MasterSecret masterSecret, long mmsId) {
     SQLiteDatabase           database = databaseHelper.getReadableDatabase();
     List<DatabaseAttachment> results  = new LinkedList<>();
     Cursor                   cursor   = null;
@@ -184,7 +189,7 @@ public class AttachmentDatabase extends Database {
                               null, null, null);
 
       while (cursor != null && cursor.moveToNext()) {
-        results.add(getAttachment(cursor));
+        results.add(getAttachment(masterSecret, cursor));
       }
 
       return results;
@@ -194,7 +199,7 @@ public class AttachmentDatabase extends Database {
     }
   }
 
-  public @NonNull List<DatabaseAttachment> getPendingAttachments() {
+  public @NonNull List<DatabaseAttachment> getPendingAttachments(@NonNull MasterSecret masterSecret) {
     final SQLiteDatabase           database    = databaseHelper.getReadableDatabase();
     final List<DatabaseAttachment> attachments = new LinkedList<>();
 
@@ -202,7 +207,7 @@ public class AttachmentDatabase extends Database {
     try {
       cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
       while (cursor != null && cursor.moveToNext()) {
-        attachments.add(getAttachment(cursor));
+        attachments.add(getAttachment(masterSecret, cursor));
       }
     } finally {
       if (cursor != null) cursor.close();
@@ -282,7 +287,6 @@ public class AttachmentDatabase extends Database {
     return partData.second;
   }
 
-
   void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret,
                                    long mmsId,
                                    @NonNull List<Attachment> attachments)
@@ -324,6 +328,7 @@ public class AttachmentDatabase extends Database {
                                   mediaStream.getMimeType(),
                                   databaseAttachment.getTransferState(),
                                   dataSize,
+                                  databaseAttachment.getFileName(),
                                   databaseAttachment.getLocation(),
                                   databaseAttachment.getKey(),
                                   databaseAttachment.getRelay(),
@@ -331,6 +336,22 @@ public class AttachmentDatabase extends Database {
   }
 
 
+  public void updateAttachmentFileName(@NonNull MasterSecret masterSecret,
+                                       @NonNull AttachmentId attachmentId,
+                                       @Nullable String fileName)
+  {
+    SQLiteDatabase database = databaseHelper.getWritableDatabase();
+
+    if (fileName != null) {
+      fileName = new MasterCipher(masterSecret).encryptBody(fileName);
+    }
+
+    ContentValues contentValues = new ContentValues(1);
+    contentValues.put(FILE_NAME, fileName);
+
+    database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings());
+  }
+
   public void markAttachmentUploaded(long messageId, Attachment attachment) {
     ContentValues  values   = new ContentValues(1);
     SQLiteDatabase database = databaseHelper.getWritableDatabase();
@@ -365,9 +386,9 @@ public class AttachmentDatabase extends Database {
     File dataFile = getAttachmentDataFile(attachmentId, dataType);
 
     try {
-      if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret);
+      if (dataFile != null) return DecryptingPartInputStream.createFor(masterSecret, dataFile);
       else                  return null;
-    } catch (FileNotFoundException e) {
+    } catch (IOException e) {
       Log.w(TAG, e);
       return null;
     }
@@ -438,7 +459,18 @@ public class AttachmentDatabase extends Database {
     }
   }
 
-  DatabaseAttachment getAttachment(Cursor cursor) {
+  DatabaseAttachment getAttachment(@Nullable MasterSecret masterSecret, Cursor cursor) {
+    String encryptedFileName = cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME));
+    String fileName          = null;
+
+    if (masterSecret != null && !TextUtils.isEmpty(encryptedFileName)) {
+      try {
+        fileName = new MasterCipher(masterSecret).decryptBody(encryptedFileName);
+      } catch (InvalidMessageException e) {
+        Log.w(TAG, e);
+      }
+    }
+
     return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ATTACHMENT_ID_ALIAS)),
                                                    cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
                                   cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
@@ -447,6 +479,7 @@ public class AttachmentDatabase extends Database {
                                   cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
                                   cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
                                   cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
+                                  fileName,
                                   cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
                                   cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
                                   cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
@@ -462,12 +495,17 @@ public class AttachmentDatabase extends Database {
     SQLiteDatabase   database = databaseHelper.getWritableDatabase();
     Pair<File, Long> partData = null;
     long             uniqueId = System.currentTimeMillis();
+    String           fileName = null;
 
     if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) {
       partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri());
       Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath());
     }
 
+    if (masterSecret.getMasterSecret().isPresent() && !TextUtils.isEmpty(attachment.getFileName())) {
+      fileName = new MasterCipher(masterSecret.getMasterSecret().get()).encryptBody(attachment.getFileName());
+    }
+
     ContentValues contentValues = new ContentValues();
     contentValues.put(MMS_ID, mmsId);
     contentValues.put(CONTENT_TYPE, attachment.getContentType());
@@ -477,6 +515,8 @@ public class AttachmentDatabase extends Database {
     contentValues.put(DIGEST, attachment.getDigest());
     contentValues.put(CONTENT_DISPOSITION, attachment.getKey());
     contentValues.put(NAME, attachment.getRelay());
+    contentValues.put(FILE_NAME, fileName);
+    contentValues.put(SIZE, attachment.getSize());
 
     if (partData != null) {
       contentValues.put(DATA, partData.first.getAbsolutePath());
@@ -543,7 +583,7 @@ public class AttachmentDatabase extends Database {
         return stream;
       }
 
-      DatabaseAttachment attachment = getAttachment(attachmentId);
+      DatabaseAttachment attachment = getAttachment(masterSecret, attachmentId);
 
       if (attachment == null || !attachment.hasData()) {
         return null;
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<Attachment> attachments    = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(messageId));
+        List<Attachment> attachments    = new LinkedList<Attachment>(attachmentDatabase.getAttachmentsForMessage(masterSecret, messageId));
         MmsAddresses     addresses      = addr.getAddressesForId(messageId);
         List<String>     destinations   = new LinkedList<>();
         String           body           = getDecryptedBody(masterSecret, messageText, outboxType);
@@ -689,6 +690,7 @@ public class MmsDatabase extends MessagingDatabase {
                                                databaseAttachment.getContentType(),
                                                AttachmentDatabase.TRANSFER_PROGRESS_DONE,
                                                databaseAttachment.getSize(),
+                                               databaseAttachment.getFileName(),
                                                databaseAttachment.getLocation(),
                                                databaseAttachment.getKey(),
                                                databaseAttachment.getRelay(),
@@ -1267,7 +1269,7 @@ public class MmsDatabase extends MessagingDatabase {
     }
 
     private SlideDeck getSlideDeck(@NonNull Cursor cursor) {
-      Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
+      Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(masterSecret, cursor);
       return new SlideDeck(context, attachment);
     }
 
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<byte[]> digest      = Optional.fromNullable(record.getAvatarDigest());
+        Optional<String> fileName    = Optional.absent();
 
         if (avatarId == -1 || key == null) {
           return;
@@ -77,7 +78,7 @@ public class AvatarDownloadJob extends MasterSecretJob implements InjectableType
         attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
         attachment.deleteOnExit();
 
-        SignalServiceAttachmentPointer pointer     = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest);
+        SignalServiceAttachmentPointer pointer     = new SignalServiceAttachmentPointer(avatarId, contentType, key, relay, digest, fileName);
         InputStream                    inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
         Bitmap                         avatar      = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500);
 
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> insertResult = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
 
     if (insertResult.isPresent()) {
-      List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
+      List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, insertResult.get().getMessageId());
 
       for (DatabaseAttachment attachment : attachments) {
         ApplicationContext.getInstance(context)
                           .getJobManager()
                           .add(new AttachmentDownloadJob(context, insertResult.get().getMessageId(),
                                                          attachment.getAttachmentId()));
+
+        if (!masterSecret.getMasterSecret().isPresent()) {
+          ApplicationContext.getInstance(context)
+                            .getJobManager()
+                            .add(new AttachmentFileNameJob(context, masterSecret.getAsymmetricMasterSecret().get(), attachment, mediaMessage));
+        }
       }
 
       if (smsMessageId.isPresent()) {
@@ -550,7 +558,7 @@ public class PushDecryptJob extends ContextJob {
 
     database.markAsSent(messageId, true);
 
-    for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) {
+    for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(null, messageId)) {
       ApplicationContext.getInstance(context)
                         .getJobManager()
                         .add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId()));
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<SignalServiceAttachment> attachments = new LinkedList<>();
 
     for (final Attachment attachment : parts) {
-      if (ContentType.isImageType(attachment.getContentType()) ||
-          ContentType.isAudioType(attachment.getContentType()) ||
-          ContentType.isVideoType(attachment.getContentType()))
-      {
-        try {
-          if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
-          InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
-          attachments.add(SignalServiceAttachment.newStreamBuilder()
-                                                 .withStream(is)
-                                                 .withContentType(attachment.getContentType())
-                                                 .withLength(attachment.getSize())
-                                                 .withListener(new ProgressListener() {
-                                                   @Override
-                                                   public void onAttachmentProgress(long total, long progress) {
-                                                     EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
-                                                   }
-                                                 })
-                                                 .build());
-        } catch (IOException ioe) {
-          Log.w(TAG, "Couldn't open attachment", ioe);
-        }
+      try {
+        if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
+        InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
+        attachments.add(SignalServiceAttachment.newStreamBuilder()
+                                               .withStream(is)
+                                               .withContentType(attachment.getContentType())
+                                               .withLength(attachment.getSize())
+                                               .withFileName(attachment.getFileName())
+                                               .withListener(new ProgressListener() {
+                                                 @Override
+                                                 public void onAttachmentProgress(long total, long progress) {
+                                                   EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
+                                                 }
+                                               })
+                                               .build());
+      } catch (IOException ioe) {
+        Log.w(TAG, "Couldn't open attachment", ioe);
       }
     }
 
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<String> allowedTypes = getAllowedAutoDownloadTypes();
-      final boolean     isAllowed    = allowedTypes.contains(MediaUtil.getDiscreteMimeType(attachment.getContentType()));
+      final String      contentType  = attachment.getContentType();
+
+      boolean isAllowed;
+
+      if (isNonDocumentType(contentType)) {
+        isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
+      } else {
+        isAllowed = allowedTypes.contains("documents");
+      }
 
       /// XXX WTF -- This is *hella* gross. A requirement shouldn't have the side effect of
       // *modifying the database* just by calling isPresent().
@@ -99,4 +109,11 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent {
       return false;
     }
   }
+
+  private boolean isNonDocumentType(String contentType) {
+    return
+        ContentType.isImageType(contentType) ||
+        ContentType.isVideoType(contentType) ||
+        ContentType.isAudioType(contentType);
+  }
 }
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<Uri>       garbage = new LinkedList<>();
@@ -94,6 +98,7 @@ public class AttachmentManager {
 
       this.thumbnail          = ViewUtil.findById(root, R.id.attachment_thumbnail);
       this.audioView          = ViewUtil.findById(root, R.id.attachment_audio);
+      this.documentView       = ViewUtil.findById(root, R.id.attachment_document);
       this.mapView            = ViewUtil.findById(root, R.id.attachment_location);
       this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
 
@@ -195,7 +200,7 @@ public class AttachmentManager {
   {
     inflateStub();
 
-    new AsyncTask<Void, Void, Slide>() {
+            new AsyncTask<Void, Void, Slide>() {
       @Override
       protected void onPreExecute() {
         thumbnail.clear();
@@ -205,16 +210,33 @@ public class AttachmentManager {
 
       @Override
       protected @Nullable Slide doInBackground(Void... params) {
-        long start = System.currentTimeMillis();
+        long   start  = System.currentTimeMillis();
+        Cursor cursor = null;
+
         try {
-          final long  mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
-          final Slide slide     = mediaType.createSlide(context, uri, mediaSize);
-          Log.w(TAG, "slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
-          return slide;
-        } catch (IOException ioe) {
-          Log.w(TAG, ioe);
-          return null;
+          if (PartAuthority.isLocalUri(uri)) {
+            long  mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
+            Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
+            return mediaType.createSlide(context, uri, null, null, mediaSize);
+          } else {
+            cursor = context.getContentResolver().query(uri, null, null, null, null);
+
+            if (cursor != null && cursor.moveToFirst()) {
+              String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
+              long   fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
+              String mimeType = context.getContentResolver().getType(uri);
+
+              Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
+              return mediaType.createSlide(context, uri, fileName, mimeType, fileSize);
+            }
+          }
+        } catch (IOException e) {
+          Log.w(TAG, e);
+        } finally {
+          if (cursor != null) cursor.close();
         }
+
+        return null;
       }
 
       @Override
@@ -234,8 +256,11 @@ public class AttachmentManager {
           attachmentViewStub.get().setVisibility(View.VISIBLE);
 
           if (slide.hasAudio()) {
-            audioView.setAudio(masterSecret, (AudioSlide)slide, false);
+            audioView.setAudio(masterSecret, (AudioSlide) slide, false);
             removableMediaView.display(audioView, false);
+          } else if (slide.hasDocument()) {
+            documentView.setDocument((DocumentSlide)slide, false);
+            removableMediaView.display(documentView, false);
           } else {
             thumbnail.setImageResource(masterSecret, slide, false);
             removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE);
@@ -386,18 +411,25 @@ public class AttachmentManager {
   }
 
   public enum MediaType {
-    IMAGE, GIF, AUDIO, VIDEO;
+    IMAGE, GIF, AUDIO, VIDEO, DOCUMENT;
 
-    public @NonNull Slide createSlide(@NonNull Context context,
-                                      @NonNull Uri     uri,
-                                               long    dataSize)
+    public @NonNull Slide createSlide(@NonNull  Context context,
+                                      @NonNull  Uri     uri,
+                                      @Nullable String fileName,
+                                      @Nullable String mimeType,
+                                                long    dataSize)
     {
+      if (mimeType == null) {
+        mimeType = "application/octet-stream";
+      }
+
       switch (this) {
-      case IMAGE: return new ImageSlide(context, uri, dataSize);
-      case GIF:   return new GifSlide(context, uri, dataSize);
-      case AUDIO: return new AudioSlide(context, uri, dataSize);
-      case VIDEO: return new VideoSlide(context, uri, dataSize);
-      default:    throw  new AssertionError("unrecognized enum");
+      case IMAGE:    return new ImageSlide(context, uri, dataSize);
+      case GIF:      return new GifSlide(context, uri, dataSize);
+      case AUDIO:    return new AudioSlide(context, uri, dataSize);
+      case VIDEO:    return new VideoSlide(context, uri, dataSize);
+      case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName);
+      default:       throw  new AssertionError("unrecognized enum");
       }
     }
 
@@ -409,5 +441,6 @@ public class AttachmentManager {
       if (ContentType.isVideoType(mimeType)) return VIDEO;
       return null;
     }
+
   }
 }
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<String> getFileName() {
+    return Optional.fromNullable(attachment.getFileName());
+  }
+
+  public long getFileSize() {
+    return attachment.getSize();
+  }
+
   public boolean hasImage() {
     return false;
   }
@@ -72,6 +81,10 @@ public abstract class Slide {
     return false;
   }
 
+  public boolean hasDocument() {
+    return false;
+  }
+
   public boolean hasLocation() {
     return false;
   }
@@ -107,14 +120,15 @@ public abstract class Slide {
     return false;
   }
 
-  protected static Attachment constructAttachmentFromUri(@NonNull Context context,
-                                                         @NonNull Uri     uri,
-                                                         @NonNull String  defaultMime,
-                                                                  long     size,
-                                                                  boolean  hasThumbnail)
+  protected static Attachment constructAttachmentFromUri(@NonNull  Context context,
+                                                         @NonNull  Uri     uri,
+                                                         @NonNull  String  defaultMime,
+                                                                   long     size,
+                                                                   boolean  hasThumbnail,
+                                                         @Nullable String   fileName)
   {
     Optional<String> resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri));
-    return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size);
+    return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size, fileName);
   }
 
   @Override
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;i<projection.length;i++) {
+          if (OpenableColumns.DISPLAY_NAME.equals(projection[i])) {
+            resultRow[i] = attachment.getFileName();
+          }
+        }
+
+        matrixCursor.addRow(resultRow);
+        return matrixCursor;
+    }
+
     return null;
   }
 
   @Override
   public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
+    Log.w(TAG, "update() called");
     return 0;
   }
+
+  private ParcelFileDescriptor getParcelStreamForAttachment(MasterSecret masterSecret, AttachmentId attachmentId) throws IOException {
+    long       plaintextLength = Util.getStreamLength(DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId));
+    MemoryFile memoryFile      = new MemoryFile(attachmentId.toString(), Util.toIntExact(plaintextLength));
+
+    InputStream  in  = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId);
+    OutputStream out = memoryFile.getOutputStream();
+
+    Util.copy(in, out);
+    Util.close(out);
+    Util.close(in);
+
+    return MemoryFileUtil.getParcelFileDescriptor(memoryFile);
+  }
 }
diff --git a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java
index 30dc5acafe..f4755893ae 100644
--- a/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java
+++ b/src/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java
@@ -125,7 +125,7 @@ public class PersistentBlobProvider {
   public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
     final byte[] cached = cache.get(id);
     return cached != null ? new ByteArrayInputStream(cached)
-                          : new DecryptingPartInputStream(getFile(id), masterSecret);
+                          : DecryptingPartInputStream.createFor(masterSecret, getFile(id));
   }
 
   private File getFile(long id) {
diff --git a/src/org/thoughtcrime/securesms/util/LimitedInputStream.java b/src/org/thoughtcrime/securesms/util/LimitedInputStream.java
new file mode 100644
index 0000000000..9092b785fc
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/LimitedInputStream.java
@@ -0,0 +1,120 @@
+package org.thoughtcrime.securesms.util;
+
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * An input stream, which limits its data size. This stream is
+ * used, if the content length is unknown.
+ */
+public class LimitedInputStream extends FilterInputStream {
+
+  /**
+   * The maximum size of an item, in bytes.
+   */
+  private long sizeMax;
+
+  /**
+   * The current number of bytes.
+   */
+  private long count;
+
+  /**
+   * Whether this stream is already closed.
+   */
+  private boolean closed;
+
+  /**
+   * Creates a new instance.
+   * @param pIn The input stream, which shall be limited.
+   * @param pSizeMax The limit; no more than this number of bytes
+   *   shall be returned by the source stream.
+   */
+  public LimitedInputStream(InputStream pIn, long pSizeMax) {
+    super(pIn);
+    sizeMax = pSizeMax;
+  }
+
+  /**
+   * Reads the next byte of data from this input stream. The value
+   * byte is returned as an <code>int</code> in the range
+   * <code>0</code> to <code>255</code>. If no byte is available
+   * because the end of the stream has been reached, the value
+   * <code>-1</code> is returned. This method blocks until input data
+   * is available, the end of the stream is detected, or an exception
+   * is thrown.
+   *
+   * This method
+   * simply performs <code>in.read()</code> and returns the result.
+   *
+   * @return     the next byte of data, or <code>-1</code> if the end of the
+   *             stream is reached.
+   * @exception  IOException  if an I/O error occurs.
+   * @see        java.io.FilterInputStream#in
+   */
+  public int read() throws IOException {
+    if (count >= sizeMax) return -1;
+
+    int res = super.read();
+    if (res != -1) {
+      count++;
+    }
+    return res;
+  }
+
+  /**
+   * Reads up to <code>len</code> bytes of data from this input stream
+   * into an array of bytes. If <code>len</code> is not zero, the method
+   * blocks until some input is available; otherwise, no
+   * bytes are read and <code>0</code> is returned.
+   *
+   * This method simply performs <code>in.read(b, off, len)</code>
+   * and returns the result.
+   *
+   * @param      b     the buffer into which the data is read.
+   * @param      off   The start offset in the destination array
+   *                   <code>b</code>.
+   * @param      len   the maximum number of bytes read.
+   * @return     the total number of bytes read into the buffer, or
+   *             <code>-1</code> if there is no more data because the end of
+   *             the stream has been reached.
+   * @exception  NullPointerException If <code>b</code> is <code>null</code>.
+   * @exception  IndexOutOfBoundsException If <code>off</code> is negative,
+   * <code>len</code> is negative, or <code>len</code> is greater than
+   * <code>b.length - off</code>
+   * @exception  IOException  if an I/O error occurs.
+   * @see        java.io.FilterInputStream#in
+   */
+  public int read(byte[] b, int off, int len) throws IOException {
+    if (count >= sizeMax) return -1;
+
+    long correctLength = Math.min(len, sizeMax - count);
+
+    int res = super.read(b, off, Util.toIntExact(correctLength));
+    if (res > 0) {
+      count += res;
+    }
+    return res;
+  }
+
+}
\ 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<SaveAttachmentTask.Attachment, Void, Integer> {
+public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, File>> {
   private static final String TAG = SaveAttachmentTask.class.getSimpleName();
 
   private static final int SUCCESS              = 0;
   private static final int FAILURE              = 1;
   private static final int WRITE_ACCESS_FAILURE = 2;
 
-  private final WeakReference<Context> contextReference;
+  private final WeakReference<Context>      contextReference;
   private final WeakReference<MasterSecret> masterSecretReference;
+  private final WeakReference<View>         view;
 
   private final int attachmentCount;
 
-  public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
-    this(context, masterSecret, 1);
+  public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) {
+    this(context, masterSecret, view, 1);
   }
 
-  public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) {
+  public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view, int count) {
     super(context,
           context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
           context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
     this.contextReference      = new WeakReference<>(context);
     this.masterSecretReference = new WeakReference<>(masterSecret);
+    this.view                  = new WeakReference<>(view);
     this.attachmentCount       = count;
   }
 
   @Override
-  protected Integer doInBackground(SaveAttachmentTask.Attachment... attachments) {
+  protected Pair<Integer, File> doInBackground(SaveAttachmentTask.Attachment... attachments) {
     if (attachments == null || attachments.length == 0) {
       throw new AssertionError("must pass in at least one attachment");
     }
 
     try {
-      Context context           = contextReference.get();
+      Context      context      = contextReference.get();
       MasterSecret masterSecret = masterSecretReference.get();
+      File         directory    = null;
 
       if (!Environment.getExternalStorageDirectory().canWrite()) {
-        return WRITE_ACCESS_FAILURE;
+        return new Pair<>(WRITE_ACCESS_FAILURE, null);
       }
 
       if (context == null) {
-        return FAILURE;
+        return new Pair<>(FAILURE, null);
       }
 
       for (Attachment attachment : attachments) {
-        if (attachment != null && !saveAttachment(context, masterSecret, attachment)) {
-          return FAILURE;
+        if (attachment != null) {
+          directory = saveAttachment(context, masterSecret, attachment);
+          if (directory == null) return new Pair<>(FAILURE, null);
         }
       }
 
-      return SUCCESS;
+      if (attachments.length > 1) return new Pair<>(SUCCESS, null);
+      else                        return new Pair<>(SUCCESS, directory);
     } catch (IOException ioe) {
       Log.w(TAG, ioe);
-      return FAILURE;
+      return new Pair<>(FAILURE, null);
     }
   }
 
-  private boolean saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) throws IOException {
-    String contentType      = MediaUtil.getCorrectedMimeType(attachment.contentType);
-    File mediaFile          = constructOutputFile(contentType, attachment.date);
+  private @Nullable File saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment)
+      throws IOException
+  {
+    String      contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
+    File        mediaFile   = constructOutputFile(attachment.fileName, contentType, attachment.date);
     InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
 
     if (inputStream == null) {
-      return false;
+      return null;
     }
 
     OutputStream outputStream = new FileOutputStream(mediaFile);
@@ -95,16 +107,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
     MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
                                     new String[]{contentType}, null);
 
-    return true;
+    return mediaFile.getParentFile();
   }
 
   @Override
-  protected void onPostExecute(Integer result) {
+  protected void onPostExecute(final Pair<Integer, File> result) {
     super.onPostExecute(result);
-    Context context = contextReference.get();
+    final Context context = contextReference.get();
     if (context == null) return;
 
-    switch (result) {
+    switch (result.first()) {
       case FAILURE:
         Toast.makeText(context,
                        context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
@@ -112,10 +124,26 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
                        Toast.LENGTH_LONG).show();
         break;
       case SUCCESS:
-        Toast.makeText(context,
-                       context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully,
-                                                              attachmentCount),
-                       Toast.LENGTH_LONG).show();
+        Snackbar snackbar = Snackbar.make(view.get(),
+                                          context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully, attachmentCount),
+                                          Snackbar.LENGTH_SHORT);
+
+        if (result.second() != null) {
+          snackbar.setDuration(Snackbar.LENGTH_LONG);
+          snackbar.setAction(R.string.SaveAttachmentTask_open_directory, new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+              Intent intent = new Intent(Intent.ACTION_VIEW);
+              intent.setDataAndType(Uri.fromFile(result.second()), "resource/folder");
+              if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null)
+              {
+                context.startActivity(intent);
+              }
+            }
+          });
+        }
+
+        snackbar.show();
         break;
       case WRITE_ACCESS_FAILURE:
         Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation,
@@ -124,7 +152,9 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
     }
   }
 
-  private File constructOutputFile(String contentType, long timestamp) throws IOException {
+  private File constructOutputFile(@Nullable String fileName, String contentType, long timestamp)
+      throws IOException
+  {
     File sdCard = Environment.getExternalStorageDirectory();
     File outputDirectory;
 
@@ -140,32 +170,54 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
 
     if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
 
-    MimeTypeMap       mimeTypeMap   = MimeTypeMap.getSingleton();
-    String            extension     = mimeTypeMap.getExtensionFromMimeType(contentType);
-    SimpleDateFormat  dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
-    String            base          = "signal-" + dateFormatter.format(timestamp);
+    if (fileName == null) {
+      MimeTypeMap      mimeTypeMap   = MimeTypeMap.getSingleton();
+      String           extension     = mimeTypeMap.getExtensionFromMimeType(contentType);
+      SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
+      String           base          = "signal-" + dateFormatter.format(timestamp);
 
-    if (extension == null) extension = "attach";
+      if (extension == null) extension = "attach";
+
+      fileName = base + "." + extension;
+    }
+
+    int  i    = 0;
+    File file = new File(outputDirectory, fileName);
 
-    int i = 0;
-    File file = new File(outputDirectory, base + "." + extension);
     while (file.exists()) {
-      file = new File(outputDirectory, base + "-" + (++i) + "." + extension);
+      String[] fileParts = getFileNameParts(fileName);
+      file = new File(outputDirectory, fileParts[0] + "-" + (++i) + "." + fileParts[1]);
     }
 
     return file;
   }
 
+  private String[] getFileNameParts(String fileName) {
+    String[] result = new String[2];
+    String[] tokens = fileName.split("\\.(?=[^\\.]+$)");
+
+    result[0] = tokens[0];
+
+    if (tokens.length > 1) result[1] = tokens[1];
+    else                   result[1] = "";
+
+    return result;
+  }
+
   public static class Attachment {
     public Uri    uri;
+    public String fileName;
     public String contentType;
     public long   date;
 
-    public Attachment(@NonNull Uri uri, @NonNull String contentType, long date) {
+    public Attachment(@NonNull Uri uri, @NonNull String contentType,
+                      long date, @Nullable String fileName)
+    {
       if (uri == null || contentType == null || date < 0) {
         throw new AssertionError("uri, content type, and date must all be specified");
       }
       this.uri         = uri;
+      this.fileName    = fileName;
       this.contentType = contentType;
       this.date        = date;
     }
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());