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