diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/crypto/AttachmentCipherInputStream.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/crypto/AttachmentCipherInputStream.java
index d0eff59ee2..d0f23ad23b 100644
--- a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/crypto/AttachmentCipherInputStream.java
+++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/crypto/AttachmentCipherInputStream.java
@@ -101,6 +101,24 @@ public class AttachmentCipherInputStream extends FileInputStream {
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);
diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java
index 3cfa3ff4e0..b4febc940a 100644
--- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java
+++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java
@@ -378,7 +378,13 @@ public class PushServiceSocket {
URL uploadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
connection.setDoOutput(true);
- connection.setFixedLengthStreamingMode((int) AttachmentCipherOutputStream.getCiphertextLength(dataSize));
+
+ if (dataSize > 0) {
+ connection.setFixedLengthStreamingMode((int) AttachmentCipherOutputStream.getCiphertextLength(dataSize));
+ } else {
+ connection.setChunkedStreamingMode(0);
+ }
+
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.connect();
diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java
index 8af0bf5198..ed3827dc20 100644
--- a/src/org/thoughtcrime/securesms/ConversationItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationItem.java
@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -463,7 +464,7 @@ public class ConversationItem extends LinearLayout {
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(slide.getUri(), slide.getContentType());
+ intent.setDataAndType(PartAuthority.getPublicPartUri(slide.getUri()), slide.getContentType());
try {
context.startActivity(intent);
} catch (ActivityNotFoundException anfe) {
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index d38dbaf35a..da9d693d78 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -17,7 +17,6 @@
package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
-import android.content.ContentUris;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.net.Uri;
@@ -37,8 +36,7 @@ import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.providers.PartProvider;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
@@ -133,12 +131,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity {
}
}
- private InputStream getInputStream(Uri uri, MasterSecret masterSecret) throws IOException {
- if (PartProvider.isAuthority(uri)) {
- return DatabaseFactory.getEncryptingPartDatabase(this, masterSecret).getPartStream(ContentUris.parseId(uri));
- } else {
- throw new AssertionError("Given a URI that is not handled by our app.");
- }
+ private InputStream getMediaInputStream() throws IOException {
+ return PartAuthority.getPartStream(this, masterSecret, mediaUri);
}
@Override
@@ -162,8 +156,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity {
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeParams, 0);
int maxTextureSize = Math.max(maxTextureSizeParams[0], 2048);
Log.w(TAG, "reported GL_MAX_TEXTURE_SIZE: " + maxTextureSize);
- return BitmapUtil.createScaledBitmap(getInputStream(mediaUri, masterSecret),
- getInputStream(mediaUri, masterSecret),
+ return BitmapUtil.createScaledBitmap(getMediaInputStream(),
+ getMediaInputStream(),
maxTextureSize, maxTextureSize);
} catch (IOException | BitmapDecodingException e) {
return null;
diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java b/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
index 44878aac48..8b496baf87 100644
--- a/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
+++ b/src/org/thoughtcrime/securesms/crypto/DecryptingPartInputStream.java
@@ -97,6 +97,24 @@ public class DecryptingPartInputStream extends FileInputStream {
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);
diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
index b7a320f10d..6f6074fac3 100644
--- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
+++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
@@ -39,6 +39,7 @@ import org.whispersystems.libaxolotl.InvalidMessageException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
import ws.com.google.android.mms.ContentType;
@@ -56,14 +57,14 @@ public class DatabaseFactory {
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
- private static final int DATABASE_VERSION = 13;
+ private static final int INTRODUCED_PART_DATA_SIZE_VERSION = 14;
+ private static final int DATABASE_VERSION = 14;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
private static DatabaseFactory instance;
- private static EncryptingPartDatabase encryptingPartInstance;
private DatabaseHelper databaseHelper;
@@ -117,17 +118,6 @@ public class DatabaseFactory {
return getInstance(context).part;
}
- public static EncryptingPartDatabase getEncryptingPartDatabase(Context context, MasterSecret masterSecret) {
- synchronized (lock) {
- if (encryptingPartInstance == null) {
- DatabaseFactory factory = getInstance(context);
- encryptingPartInstance = new EncryptingPartDatabase(context, factory.databaseHelper, masterSecret);
- }
-
- return encryptingPartInstance;
- }
- }
-
public static MmsAddressDatabase getMmsAddressDatabase(Context context) {
return getInstance(context).mmsAddress;
}
@@ -360,12 +350,12 @@ public class DatabaseFactory {
boolean encrypted = partCursor.getInt(partCursor.getColumnIndexOrThrow("encrypted")) == 1;
File dataFile = new File(dataLocation);
- FileInputStream fin;
+ InputStream is;
- if (encrypted) fin = new DecryptingPartInputStream(dataFile, masterSecret);
- else fin = new FileInputStream(dataFile);
+ if (encrypted) is = new DecryptingPartInputStream(dataFile, masterSecret);
+ else is = new FileInputStream(dataFile);
- body = (body == null) ? Util.readFully(fin) : body + " " + Util.readFully(fin);
+ body = (body == null) ? Util.readFullyAsString(is) : body + " " + Util.readFullyAsString(is);
dataFile.delete();
db.delete("part", "_id = ?", new String[] {partId+""});
@@ -711,6 +701,10 @@ public class DatabaseFactory {
db.execSQL("CREATE INDEX IF NOT EXISTS mms_date_sent_index ON mms (date);");
}
+ if (oldVersion < INTRODUCED_PART_DATA_SIZE_VERSION) {
+ db.execSQL("ALTER TABLE part ADD COLUMN data_size INTEGER DEFAULT 0;");
+ }
+
db.setTransactionSuccessful();
db.endTransaction();
}
diff --git a/src/org/thoughtcrime/securesms/database/EncryptingPartDatabase.java b/src/org/thoughtcrime/securesms/database/EncryptingPartDatabase.java
deleted file mode 100644
index 948d66dde6..0000000000
--- a/src/org/thoughtcrime/securesms/database/EncryptingPartDatabase.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (C) 2011 Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.thoughtcrime.securesms.database;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
-import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
-import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-
-import ws.com.google.android.mms.pdu.PduPart;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-
-public class EncryptingPartDatabase extends PartDatabase {
-
- private final MasterSecret masterSecret;
-
- public EncryptingPartDatabase(Context context, SQLiteOpenHelper databaseHelper, MasterSecret masterSecret) {
- super(context, databaseHelper);
- this.masterSecret = masterSecret;
- }
-
- @Override
- protected FileInputStream getPartInputStream(File path, PduPart part) throws FileNotFoundException {
- Log.w("EncryptingPartDatabase", "Getting part at: " + path.getAbsolutePath());
- if (!part.getEncrypted())
- return super.getPartInputStream(path, part);
-
- return new DecryptingPartInputStream(path, masterSecret);
- }
-
- @Override
- protected FileOutputStream getPartOutputStream(File path, PduPart part) throws FileNotFoundException {
- Log.w("EncryptingPartDatabase", "Writing part to: " + path.getAbsolutePath());
- part.setEncrypted(true);
- return new EncryptingPartOutputStream(path, masterSecret);
- }
-}
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index 96af50ca82..c63711fbc4 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -467,7 +467,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
throws MmsException, NoSuchMessageException
{
MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context);
- PartDatabase partDatabase = getPartDatabase(masterSecret);
+ PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
MasterCipher masterCipher = new MasterCipher(masterSecret);
Cursor cursor = null;
@@ -485,7 +485,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
PduHeaders headers = getHeadersFromCursor(cursor);
addr.getAddressesForId(messageId, headers);
- PduBody body = getPartsAsBody(partDatabase.getParts(messageId, true));
+ PduBody body = getPartsAsBody(partDatabase.getParts(messageId));
try {
if (!TextUtils.isEmpty(messageText) && Types.isSymmetricEncryption(outboxType)) {
@@ -707,8 +707,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
ContentValues contentValues)
throws MmsException
{
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- PartDatabase partsDatabase = getPartDatabase(masterSecret);
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ PartDatabase partsDatabase = DatabaseFactory.getPartDatabase(context);
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) {
@@ -725,7 +725,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long messageId = db.insert(TABLE_NAME, null, contentValues);
addressDatabase.insertAddressesForId(messageId, headers);
- partsDatabase.insertParts(messageId, body);
+ partsDatabase.insertParts(masterSecret, messageId, body);
notifyConversationListeners(contentValues.getAsLong(THREAD_ID));
DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID));
@@ -894,14 +894,6 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return cvb.getContentValues();
}
-
- protected PartDatabase getPartDatabase(MasterSecret masterSecret) {
- if (masterSecret == null)
- return DatabaseFactory.getPartDatabase(context);
- else
- return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
- }
-
public Reader readerFor(MasterSecret masterSecret, Cursor cursor) {
return new Reader(masterSecret, cursor);
}
@@ -1073,8 +1065,9 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (masterSecret == null)
return null;
- PduBody body = getPartsAsBody(getPartDatabase(masterSecret).getParts(id, false));
- SlideDeck slideDeck = new SlideDeck(context, masterSecret, body);
+ PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
+ PduBody body = getPartsAsBody(partDatabase.getParts(id));
+ SlideDeck slideDeck = new SlideDeck(context, masterSecret, body);
if (!body.containsPushInProgress()) {
slideCache.put(id, new SoftReference(slideDeck));
diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java
index 17a86915ec..19cadbe6e2 100644
--- a/src/org/thoughtcrime/securesms/database/PartDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java
@@ -26,17 +26,18 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import org.thoughtcrime.securesms.providers.PartProvider;
+import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
+import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
@@ -46,6 +47,7 @@ import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
public class PartDatabase extends Database {
+ private static final String TAG = PartDatabase.class.getSimpleName();
private static final String TABLE_NAME = "part";
private static final String ID = "_id";
@@ -63,14 +65,15 @@ public class PartDatabase extends Database {
private static final String ENCRYPTED = "encrypted";
private static final String DATA = "_data";
private static final String PENDING_PUSH_ATTACHMENT = "pending_push";
+ private static final String SIZE = "data_size";
- public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
- MMS_ID + " INTEGER, " + SEQUENCE + " INTEGER DEFAULT 0, " +
- CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + CHARSET + " INTEGER, " +
- CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " +
- CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " +
- CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " +
- PENDING_PUSH_ATTACHMENT + " INTEGER, "+ DATA + " TEXT);";
+ public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
+ MMS_ID + " INTEGER, " + SEQUENCE + " INTEGER DEFAULT 0, " +
+ CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + CHARSET + " INTEGER, " +
+ CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " +
+ CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " +
+ CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " +
+ PENDING_PUSH_ATTACHMENT + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
@@ -81,6 +84,108 @@ public class PartDatabase extends Database {
super(context, databaseHelper);
}
+ public InputStream getPartStream(MasterSecret masterSecret, long partId)
+ throws FileNotFoundException
+ {
+ return getDataStream(masterSecret, partId, DATA);
+ }
+
+ public void updateFailedDownloadedPart(long messageId, long partId, PduPart part)
+ throws MmsException
+ {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+
+ part.setContentDisposition(new byte[0]);
+ part.setPendingPush(false);
+
+ ContentValues values = getContentValuesForPart(part);
+
+ values.put(DATA, (String)null);
+
+ database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
+ notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
+ }
+
+ public PduPart getPart(long partId) {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ Cursor cursor = null;
+
+ try {
+ cursor = database.query(TABLE_NAME, null, ID_WHERE, new String[] {partId+""}, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) return getPart(cursor);
+ else return null;
+
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+
+ public List> getParts(long mmsId) {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ List> results = new LinkedList<>();
+ Cursor cursor = null;
+
+ try {
+ cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""},
+ null, null, null);
+
+ while (cursor != null && cursor.moveToNext()) {
+ PduPart part = getPart(cursor);
+ results.add(new Pair<>(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
+ part));
+ }
+
+ return results;
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ }
+
+ public void deleteParts(long mmsId) {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ Cursor cursor = null;
+
+ try {
+ cursor = database.query(TABLE_NAME, new String[] {DATA}, MMS_ID + " = ?",
+ new String[] {mmsId+""}, null, null, null);
+
+ while (cursor != null && cursor.moveToNext()) {
+ String data = cursor.getString(0);
+
+ if (!TextUtils.isEmpty(data)) {
+ new File(data).delete();
+ }
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+
+ database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId+""});
+ }
+
+ public void deleteAllParts() {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ database.delete(TABLE_NAME, null, null);
+
+ File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
+ File[] parts = partsDirectory.listFiles();
+
+ for (File part : parts) {
+ part.delete();
+ }
+ }
+
+ void insertParts(MasterSecret masterSecret, long mmsId, PduBody body) throws MmsException {
+ for (int i=0;i writePartData(MasterSecret masterSecret, PduPart part, InputStream in)
+ throws MmsException
+ {
+ try {
+ File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
+ File dataFile = File.createTempFile("part", ".mms", partsDirectory);
+ OutputStream out = getPartOutputStream(masterSecret, dataFile, part);
+ long plaintextLength = Util.copy(in, out);
+
+ return new Pair<>(dataFile, plaintextLength);
+ } catch (IOException e) {
+ throw new MmsException(e);
}
}
- public void updateDownloadedPart(long messageId, long partId, PduPart part, InputStream data)
+ private Pair writePartData(MasterSecret masterSecret, PduPart part)
throws MmsException
{
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- File partData = writePartData(part, data);
+ try {
+ if (part.getData() != null) {
+ Log.w(TAG, "Writing part data from buffer");
+ return writePartData(masterSecret, part, new ByteArrayInputStream(part.getData()));
+ } else if (part.getDataUri() != null) {
+ Log.w(TAG, "Writing part data from URI");
+ InputStream in = context.getContentResolver().openInputStream(part.getDataUri());
+ return writePartData(masterSecret, part, in);
+ } else {
+ throw new MmsException("Part is empty!");
+ }
+ } catch (FileNotFoundException e) {
+ throw new MmsException(e);
+ }
+ }
+
+ private PduPart getPart(Cursor cursor) {
+ PduPart part = new PduPart();
+ String dataLocation = cursor.getString(cursor.getColumnIndexOrThrow(DATA));
+ long partId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
+
+ getPartValues(part, cursor);
+
+ part.setDataUri(ContentUris.withAppendedId(PartAuthority.PART_CONTENT_URI, partId));
+
+ return part;
+ }
+
+ private long insertPart(MasterSecret masterSecret, PduPart part, long mmsId) throws MmsException {
+ Log.w(TAG, "inserting part to mms " + mmsId);
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ Pair partData = null;
+
+ if (!part.isPendingPush()) {
+ partData = writePartData(masterSecret, part);
+ Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath());
+ }
+
+ ContentValues contentValues = getContentValuesForPart(part);
+ contentValues.put(MMS_ID, mmsId);
+
+ if (partData != null) {
+ contentValues.put(DATA, partData.first.getAbsolutePath());
+ contentValues.put(SIZE, partData.second);
+ }
+
+ return database.insert(TABLE_NAME, null, contentValues);
+ }
+
+ public void updateDownloadedPart(MasterSecret masterSecret, long messageId,
+ long partId, PduPart part, InputStream data)
+ throws MmsException
+ {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ Pair partData = writePartData(masterSecret, part, data);
part.setContentDisposition(new byte[0]);
part.setPendingPush(false);
@@ -317,120 +401,12 @@ public class PartDatabase extends Database {
ContentValues values = getContentValuesForPart(part);
if (partData != null) {
- values.put(DATA, partData.getAbsolutePath());
+ values.put(DATA, partData.first.getAbsolutePath());
+ values.put(SIZE, partData.second);
}
database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
+
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
-
- public void updateFailedDownloadedPart(long messageId, long partId, PduPart part)
- throws MmsException
- {
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
-
- part.setContentDisposition(new byte[0]);
- part.setPendingPush(false);
-
- ContentValues values = getContentValuesForPart(part);
-
- values.put(DATA, (String)null);
-
- database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
- notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
- }
-
- public PduPart getPart(long partId, boolean includeData) {
- SQLiteDatabase database = databaseHelper.getReadableDatabase();
- Cursor cursor = null;
-
- try {
- cursor = database.query(TABLE_NAME, null, ID_WHERE, new String[] {partId+""}, null, null, null);
-
- if (cursor != null && cursor.moveToFirst())
- return getPart(cursor, includeData);
- else
- return null;
- } finally {
- if (cursor != null)
- cursor.close();
- }
- }
-
- public List> getParts(long mmsId, boolean includeData) {
- SQLiteDatabase database = databaseHelper.getReadableDatabase();
- List> results = new LinkedList>();
- Cursor cursor = null;
-
- try {
- cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""}, null, null, null);
-
- while (cursor != null && cursor.moveToNext()) {
- PduPart part = getPart(cursor, includeData);
- results.add(new Pair(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
- part));
- }
-
- return results;
- } finally {
- if (cursor != null)
- cursor.close();
- }
- }
-
- public List>> getPushPendingParts() {
- SQLiteDatabase database = databaseHelper.getReadableDatabase();
- List>> results = new LinkedList>>();
- Cursor cursor = null;
-
- try {
- cursor = database.query(TABLE_NAME, null, PENDING_PUSH_ATTACHMENT + " = ?", new String[] {"1"}, null, null, null);
-
- while (cursor != null && cursor.moveToNext()) {
- PduPart part = getPart(cursor, false);
- results.add(new Pair>(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
- new Pair(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
- part)));
- }
-
- return results;
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- }
-
- public void deleteParts(long mmsId) {
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- Cursor cursor = null;
-
- try {
- cursor = database.query(TABLE_NAME, new String[] {DATA}, MMS_ID + " = ?", new String[] {mmsId+""}, null, null, null);
-
- while (cursor != null && cursor.moveToNext()) {
- String data = cursor.getString(0);
- if (!TextUtils.isEmpty(data)) {
- new File(cursor.getString(0)).delete();
- }
- }
- } finally {
- if (cursor != null)
- cursor.close();
- }
-
- database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId+""});
- }
-
- public void deleteAllParts() {
- SQLiteDatabase database = databaseHelper.getWritableDatabase();
- database.delete(TABLE_NAME, null, null);
-
- File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
- File[] parts = partsDirectory.listFiles();
-
- for (int i=0;i> parts = database.getParts(messageId, false);
+ List> parts = database.getParts(messageId);
for (Pair partPair : parts) {
retrievePart(masterSecret, partPair.second, messageId, partPair.first);
@@ -69,7 +68,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
@Override
public void onCanceled() {
PartDatabase database = DatabaseFactory.getPartDatabase(context);
- List> parts = database.getParts(messageId, false);
+ List> parts = database.getParts(messageId);
for (Pair partPair : parts) {
markFailed(messageId, partPair.second, partPair.first);
@@ -78,16 +77,14 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
@Override
public boolean onShouldRetryThrowable(Exception exception) {
- if (exception instanceof PushNetworkException) return true;
-
- return false;
+ return (exception instanceof PushNetworkException);
}
private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId, long partId)
throws IOException
{
- EncryptingPartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
- File attachmentFile = null;
+ PartDatabase database = DatabaseFactory.getPartDatabase(context);
+ File attachmentFile = null;
try {
attachmentFile = createTempFile();
@@ -95,7 +92,7 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, part);
InputStream attachment = messageReceiver.retrieveAttachment(pointer, attachmentFile);
- database.updateDownloadedPart(messageId, partId, part, attachment);
+ database.updateDownloadedPart(masterSecret, messageId, partId, part, attachment);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
Log.w(TAG, e);
markFailed(messageId, part, partId);
diff --git a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
index 558e0c56ec..937b4a72e1 100644
--- a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.mms.MmsRadio;
import org.thoughtcrime.securesms.mms.MmsRadioException;
import org.thoughtcrime.securesms.mms.MmsSendResult;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -23,17 +24,22 @@ import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.NumberUtil;
+import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.NoSessionException;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
import java.util.Arrays;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.EncodedStringValue;
+import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduComposer;
import ws.com.google.android.mms.pdu.PduHeaders;
+import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendConf;
import ws.com.google.android.mms.pdu.SendReq;
@@ -60,10 +66,12 @@ public class MmsSendJob extends MasterSecretJob {
}
@Override
- public void onRun(MasterSecret masterSecret) throws MmsException, NoSuchMessageException {
+ public void onRun(MasterSecret masterSecret) throws MmsException, NoSuchMessageException, IOException {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
+ populatePartData(message.getBody(), masterSecret);
+
try {
MmsSendResult result = deliver(masterSecret, message);
@@ -94,6 +102,20 @@ public class MmsSendJob extends MasterSecretJob {
notifyMediaMessageDeliveryFailed(context, messageId);
}
+ private void populatePartData(PduPart part, MasterSecret masterSecret) throws IOException {
+ ByteArrayOutputStream os = part.getDataSize() > 0 && part.getDataSize() < Integer.MAX_VALUE
+ ? new ByteArrayOutputStream((int)part.getDataSize())
+ : new ByteArrayOutputStream();
+ Util.copy(PartAuthority.getPartStream(context, masterSecret, part.getDataUri()), os);
+ part.setData(os.toByteArray());
+ }
+
+ private void populatePartData(PduBody body, MasterSecret masterSecret) throws IOException {
+ for (int i=body.getPartsNum()-1; i>=0; i--) {
+ populatePartData(body.getPart(i), masterSecret);
+ }
+ }
+
public MmsSendResult deliver(MasterSecret masterSecret, SendReq message)
throws UndeliverableMessageException, InsecureFallbackApprovalException
{
diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
index 43ce9d6df6..6b33e27223 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -114,7 +114,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
List addresses = getPushAddresses(recipients);
- List attachments = getAttachments(message);
+ List attachments = getAttachments(masterSecret, message);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index a9dfd4b7fe..a2e40138c5 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
@@ -109,7 +109,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
PushAddress address = getPushAddress(recipients.getPrimaryRecipient());
- List attachments = getAttachments(message);
+ List attachments = getAttachments(masterSecret, message);
String body = PartParser.getMessageText(message.getBody());
TextSecureMessage mediaMessage = new TextSecureMessage(message.getSentTimestamp(), attachments, body);
diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index 86d041d887..95815eaf93 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -3,8 +3,10 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
+import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -20,10 +22,13 @@ import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import ws.com.google.android.mms.ContentType;
+import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
public abstract class PushSendJob extends MasterSecretJob {
@@ -82,18 +87,23 @@ public abstract class PushSendJob extends MasterSecretJob {
return (isSmsFallbackSupported(context, destination, media) && TextSecurePreferences.isFallbackSmsAskRequired(context));
}
- protected List getAttachments(SendReq message) {
+ protected List getAttachments(final MasterSecret masterSecret, final SendReq message) {
List attachments = new LinkedList<>();
for (int i=0;i MAX_MESSAGE_SIZE)
- throw new MediaTooLargeException("Audio track larger than size maximum.");
+ assertMediaSize(context, uri);
Cursor cursor = null;
diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java
new file mode 100644
index 0000000000..fa514a730e
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java
@@ -0,0 +1,45 @@
+package org.thoughtcrime.securesms.mms;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.net.Uri;
+
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.PartDatabase;
+import org.thoughtcrime.securesms.providers.PartProvider;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class PartAuthority {
+
+ private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part";
+ public static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
+
+ private static final int PART_ROW = 1;
+
+ private static final UriMatcher uriMatcher;
+
+ static {
+ uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ uriMatcher.addURI("org.thoughtcrime.securesms", "part/#", PART_ROW);
+ }
+
+ public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
+ throws FileNotFoundException
+ {
+ PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
+ int match = uriMatcher.match(uri);
+
+ switch (match) {
+ case PART_ROW: return partDatabase.getPartStream(masterSecret, ContentUris.parseId(uri));
+ default: return context.getContentResolver().openInputStream(uri);
+ }
+ }
+
+ public static Uri getPublicPartUri(Uri uri) {
+ return ContentUris.withAppendedId(PartProvider.CONTENT_URI, ContentUris.parseId(uri));
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java
index d2d8d98016..4e077c621e 100644
--- a/src/org/thoughtcrime/securesms/mms/Slide.java
+++ b/src/org/thoughtcrime/securesms/mms/Slide.java
@@ -20,6 +20,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import org.thoughtcrime.securesms.util.Util;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
@@ -56,34 +57,19 @@ public abstract class Slide {
}
public InputStream getPartDataInputStream() throws FileNotFoundException {
- Uri partUri = part.getDataUri();
-
- Log.w("Slide", "Loading Part URI: " + partUri);
-
- if (PartProvider.isAuthority(partUri))
- return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret).getPartStream(ContentUris.parseId(partUri));
- else
- return context.getContentResolver().openInputStream(partUri);
- }
-
- protected static long getMediaSize(Context context, Uri uri) throws IOException {
- InputStream in = context.getContentResolver().openInputStream(uri);
- long size = 0;
- byte[] buffer = new byte[512];
- int read;
-
- while ((read = in.read(buffer)) != -1)
- size += read;
-
- return size;
+ return PartAuthority.getPartStream(context, masterSecret, part.getDataUri());
}
protected byte[] getPartData() {
- if (part.getData() != null)
- return part.getData();
+ try {
+ if (part.getData() != null)
+ return part.getData();
- long partId = ContentUris.parseId(part.getDataUri());
- return DatabaseFactory.getEncryptingPartDatabase(context, masterSecret).getPart(partId, true).getData();
+ return Util.readFully(PartAuthority.getPartStream(context, masterSecret, part.getDataUri()));
+ } catch (IOException e) {
+ Log.w("Slide", e);
+ return new byte[0];
+ }
}
public String getContentType() {
@@ -133,4 +119,18 @@ public abstract class Slide {
public abstract SMILRegionElement getSmilRegion(SMILDocument document);
public abstract SMILMediaElement getMediaElement(SMILDocument document);
+
+ protected static void assertMediaSize(Context context, Uri uri)
+ throws MediaTooLargeException, IOException
+ {
+ InputStream in = context.getContentResolver().openInputStream(uri);
+ long size = 0;
+ byte[] buffer = new byte[512];
+ int read;
+
+ while ((read = in.read(buffer)) != -1) {
+ size += read;
+ if (size > MAX_MESSAGE_SIZE) throw new MediaTooLargeException("Media exceeds maximum message size.");
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java
index f3b4e4f1e5..1f1f045f98 100644
--- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java
@@ -91,9 +91,7 @@ public class VideoSlide extends Slide {
cursor.close();
}
- if (getMediaSize(context, uri) > MAX_MESSAGE_SIZE)
- throw new MediaTooLargeException("Video exceeds maximum message size.");
-
+ assertMediaSize(context, uri);
part.setDataUri(uri);
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Video" + System.currentTimeMillis()).getBytes());
diff --git a/src/org/thoughtcrime/securesms/providers/PartProvider.java b/src/org/thoughtcrime/securesms/providers/PartProvider.java
index 3c3be5a263..cca200031d 100644
--- a/src/org/thoughtcrime/securesms/providers/PartProvider.java
+++ b/src/org/thoughtcrime/securesms/providers/PartProvider.java
@@ -68,7 +68,7 @@ public class PartProvider extends ContentProvider {
}
private File copyPartToTemporaryFile(MasterSecret masterSecret, long partId) throws IOException {
- InputStream in = DatabaseFactory.getEncryptingPartDatabase(getContext(), masterSecret).getPartStream(partId);
+ InputStream in = DatabaseFactory.getPartDatabase(getContext()).getPartStream(masterSecret, partId);
File tmpDir = getContext().getDir("tmp", 0);
File tmpFile = File.createTempFile("test", ".jpg", tmpDir);
FileOutputStream fout = new FileOutputStream(tmpFile);
diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
index 2bab6a04ea..77d7840939 100644
--- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
+++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java
@@ -14,7 +14,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.providers.PartProvider;
+import org.thoughtcrime.securesms.mms.PartAuthority;
import java.io.File;
import java.io.FileOutputStream;
@@ -60,7 +60,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask