streaming media

// FREEBIE
This commit is contained in:
Jake McGinty 2014-12-12 01:03:24 -08:00
parent a09e0afbd6
commit 07bb07c342
22 changed files with 423 additions and 395 deletions

View File

@ -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);

View File

@ -378,7 +378,13 @@ public class PushServiceSocket {
URL uploadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
connection.setDoOutput(true);
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();

View File

@ -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) {

View File

@ -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;

View File

@ -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);

View File

@ -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();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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)) {
@ -708,7 +708,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
throws MmsException
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
PartDatabase partsDatabase = getPartDatabase(masterSecret);
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,7 +1065,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (masterSecret == null)
return null;
PduBody body = getPartsAsBody(getPartDatabase(masterSecret).getParts(id, false));
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
PduBody body = getPartsAsBody(partDatabase.getParts(id));
SlideDeck slideDeck = new SlideDeck(context, masterSecret, body);
if (!body.containsPushInProgress()) {

View File

@ -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,6 +65,7 @@ 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, " +
@ -70,7 +73,7 @@ public class PartDatabase extends Database {
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);";
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<Pair<Long, PduPart>> getParts(long mmsId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, PduPart>> 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<body.getPartsNum();i++) {
long partId = insertPart(masterSecret, body.getPart(i), mmsId);
Log.w(TAG, "Inserted part at ID: " + partId);
}
}
private void getPartValues(PduPart part, Cursor cursor) {
int charsetColumn = cursor.getColumnIndexOrThrow(CHARSET);
@ -126,8 +231,12 @@ public class PartDatabase extends Database {
if (!cursor.isNull(pendingPushColumn))
part.setPendingPush(cursor.getInt(pendingPushColumn) == 1);
}
int sizeColumn = cursor.getColumnIndexOrThrow(SIZE);
if (!cursor.isNull(sizeColumn))
part.setDataSize(cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)));
}
private ContentValues getContentValuesForPart(PduPart part) throws MmsException {
ContentValues contentValues = new ContentValues();
@ -172,123 +281,37 @@ public class PartDatabase extends Database {
return contentValues;
}
protected FileInputStream getPartInputStream(File file, PduPart part) throws FileNotFoundException {
Log.w("PartDatabase", "Reading non-encrypted part from: " + file.getAbsolutePath());
return new FileInputStream(file);
private InputStream getPartInputStream(MasterSecret masterSecret, File path)
throws FileNotFoundException
{
Log.w(TAG, "Getting part at: " + path.getAbsolutePath());
return new DecryptingPartInputStream(path, masterSecret);
}
protected FileOutputStream getPartOutputStream(File file, PduPart part) throws FileNotFoundException {
Log.w("PartDatabase", "Writing non-encrypted part to: " + file.getAbsolutePath());
return new FileOutputStream(file);
protected OutputStream getPartOutputStream(MasterSecret masterSecret, File path, PduPart part)
throws FileNotFoundException
{
Log.w(TAG, "Writing part to: " + path.getAbsolutePath());
part.setEncrypted(true);
return new EncryptingPartOutputStream(path, masterSecret);
}
private void readPartData(PduPart part, String filename) {
try {
File dataFile = new File(filename);
FileInputStream fin = getPartInputStream(dataFile, part);
ByteArrayOutputStream baos = new ByteArrayOutputStream((int)dataFile.length());
byte[] buffer = new byte[512];
int read;
while ((read = fin.read(buffer)) != -1)
baos.write(buffer, 0, read);
part.setData(baos.toByteArray());
fin.close();
} catch (IOException ioe) {
Log.w("PartDatabase", ioe);
part.setData(null);
}
}
private File writePartData(PduPart part, InputStream in) throws MmsException {
try {
File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
FileOutputStream fout = getPartOutputStream(dataFile, part);
byte[] buf = new byte[512];
int read;
while ((read = in.read(buf)) != -1) {
fout.write(buf, 0, read);
}
fout.close();
in.close();
return dataFile;
} catch (IOException e) {
throw new AssertionError(e);
}
}
private File writePartData(PduPart part) throws MmsException {
try {
if (part.getData() != null) {
Log.w("PartDatabase", "Writing part data from buffer");
return writePartData(part, new ByteArrayInputStream(part.getData()));
} else if (part.getDataUri() != null) {
Log.w("PartDatabase", "Writing part dat from URI");
InputStream in = context.getContentResolver().openInputStream(part.getDataUri());
return writePartData(part, in);
} else {
throw new MmsException("Part is empty!");
}
} catch (FileNotFoundException e) {
throw new AssertionError(e);
}
}
private PduPart getPart(Cursor cursor, boolean includeData) {
PduPart part = new PduPart();
String dataLocation = cursor.getString(cursor.getColumnIndexOrThrow(DATA));
long partId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
getPartValues(part, cursor);
if (includeData && !part.isPendingPush())
readPartData(part, dataLocation);
part.setDataUri(ContentUris.withAppendedId(PartProvider.CONTENT_URI, partId));
return part;
}
private long insertPart(PduPart part, long mmsId) throws MmsException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
File dataFile = null;
if (!part.isPendingPush()) {
dataFile = writePartData(part);
Log.w("PartDatabase", "Wrote part to file: " + dataFile.getAbsolutePath());
}
ContentValues contentValues = getContentValuesForPart(part);
contentValues.put(MMS_ID, mmsId);
if (dataFile != null) {
contentValues.put(DATA, dataFile.getAbsolutePath());
}
return database.insert(TABLE_NAME, null, contentValues);
}
public InputStream getPartStream(long partId) throws FileNotFoundException {
private InputStream getDataStream(MasterSecret masterSecret, long partId, String dataType)
throws FileNotFoundException
{
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
Log.w("PartDatabase", "Getting part at ID: " + partId);
try {
cursor = database.query(TABLE_NAME, new String[]{DATA, ENCRYPTED}, ID_WHERE, new String[] {partId+""}, null, null, null);
cursor = database.query(TABLE_NAME, new String[]{dataType}, ID_WHERE,
new String[] {partId+""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
PduPart part = new PduPart();
part.setEncrypted(cursor.getInt(1) == 1);
if (cursor.isNull(0)) {
throw new FileNotFoundException("No part data for id: " + partId);
}
return getPartInputStream(new File(cursor.getString(0)), part);
return getPartInputStream(masterSecret, new File(cursor.getString(0)));
} else {
throw new FileNotFoundException("No part for id: " + partId);
}
@ -298,18 +321,79 @@ public class PartDatabase extends Database {
}
}
void insertParts(long mmsId, PduBody body) throws MmsException {
for (int i=0;i<body.getPartsNum();i++) {
long partId = insertPart(body.getPart(i), mmsId);
Log.w("PartDatabase", "Inserted part at ID: " + partId);
private Pair<File, Long> 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<File, Long> writePartData(MasterSecret masterSecret, PduPart part)
throws MmsException
{
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<File, Long> 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();
File partData = writePartData(part, data);
Pair<File, Long> 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<Pair<Long, PduPart>> getParts(long mmsId, boolean includeData) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, PduPart>> results = new LinkedList<Pair<Long, PduPart>>();
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<Long, PduPart>(cursor.getLong(cursor.getColumnIndexOrThrow(ID)),
part));
}
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
public List<Pair<Long, Pair<Long, PduPart>>> getPushPendingParts() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<Pair<Long, Pair<Long, PduPart>>> results = new LinkedList<Pair<Long, Pair<Long, PduPart>>>();
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<Long, Pair<Long, PduPart>>(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
new Pair<Long, PduPart>(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.length;i++) {
parts[i].delete();
}
}
}

View File

@ -7,7 +7,6 @@ import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingPartDatabase;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
@ -54,11 +53,11 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable
@Override
public void onRun(MasterSecret masterSecret) throws IOException {
PartDatabase database = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret);
PartDatabase database = DatabaseFactory.getPartDatabase(context);
Log.w(TAG, "Downloading push parts for: " + messageId);
List<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
List<Pair<Long, PduPart>> parts = database.getParts(messageId);
for (Pair<Long, PduPart> 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<Pair<Long, PduPart>> parts = database.getParts(messageId, false);
List<Pair<Long, PduPart>> parts = database.getParts(messageId);
for (Pair<Long, PduPart> partPair : parts) {
markFailed(messageId, partPair.second, partPair.first);
@ -78,15 +77,13 @@ 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);
PartDatabase database = DatabaseFactory.getPartDatabase(context);
File attachmentFile = null;
try {
@ -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);

View File

@ -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
{

View File

@ -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<PushAddress> addresses = getPushAddresses(recipients);
List<TextSecureAttachment> attachments = getAttachments(message);
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))

View File

@ -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<TextSecureAttachment> attachments = getAttachments(message);
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);
String body = PartParser.getMessageText(message.getBody());
TextSecureMessage mediaMessage = new TextSecureMessage(message.getSentTimestamp(), attachments, body);

View File

@ -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<TextSecureAttachment> getAttachments(SendReq message) {
protected List<TextSecureAttachment> getAttachments(final MasterSecret masterSecret, final SendReq message) {
List<TextSecureAttachment> attachments = new LinkedList<>();
for (int i=0;i<message.getBody().getPartsNum();i++) {
String contentType = Util.toIsoString(message.getBody().getPart(i).getContentType());
PduPart part = message.getBody().getPart(i);
String contentType = Util.toIsoString(part.getContentType());
if (ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
byte[] data = message.getBody().getPart(i).getData();
Log.w(TAG, "Adding attachment...");
attachments.add(new TextSecureAttachmentStream(new ByteArrayInputStream(data), contentType, data.length));
try {
InputStream is = PartAuthority.getPartStream(context, masterSecret, part.getDataUri());
attachments.add(new TextSecureAttachmentStream(is, contentType, part.getDataSize()));
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
}
}

View File

@ -70,8 +70,7 @@ public class AudioSlide extends Slide {
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {
PduPart part = new PduPart();
if (getMediaSize(context, uri) > MAX_MESSAGE_SIZE)
throw new MediaTooLargeException("Audio track larger than size maximum.");
assertMediaSize(context, uri);
Cursor cursor = null;

View File

@ -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));
}
}

View File

@ -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() {
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.");
}
}
}

View File

@ -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());

View File

@ -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);

View File

@ -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<SaveAttachmentTa
}
File mediaFile = constructOutputFile(attachment.contentType, attachment.date);
InputStream inputStream = DatabaseFactory.getEncryptingPartDatabase(context, masterSecret).getPartStream(ContentUris.parseId(attachment.uri));
InputStream inputStream = PartAuthority.getPartStream(context, masterSecret, attachment.uri);
OutputStream outputStream = new FileOutputStream(mediaFile);
Util.copy(inputStream, outputStream);
@ -139,9 +139,6 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
if (uri == null || contentType == null || date < 0) {
throw new AssertionError("uri, content type, and date must all be specified");
}
if (!PartProvider.isAuthority(uri)) {
throw new AssertionError("attachment must be a TextSecure attachment");
}
this.uri = uri;
this.contentType = contentType;
this.date = date;

View File

@ -146,7 +146,7 @@ public class Util {
else return canonicalizeNumber(context, number);
}
public static String readFully(InputStream in) throws IOException {
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
@ -157,19 +157,27 @@ public class Util {
in.close();
return new String(bout.toByteArray());
return bout.toByteArray();
}
public static void copy(InputStream in, OutputStream out) throws IOException {
public static String readFullyAsString(InputStream in) throws IOException {
return new String(readFully(in));
}
public static long copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[4096];
int read;
long total = 0;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
total += read;
}
in.close();
out.close();
return total;
}
public static String getDeviceE164Number(Context context) {

View File

@ -123,6 +123,7 @@ public class PduPart {
private boolean isEncrypted;
private boolean isPendingPush;
private long dataSize;
/**
* Empty Constructor.
@ -139,6 +140,15 @@ public class PduPart {
return isEncrypted;
}
public void setDataSize(long dataSize) {
this.dataSize = dataSize;
}
public long getDataSize() {
return this.dataSize;
}
public void setPendingPush(boolean isPendingPush) {
this.isPendingPush = isPendingPush;
}