media constraints model

// FREEBIE
master
Jake McGinty 2015-01-02 15:43:28 -08:00 committed by Moxie Marlinspike
parent a0ed0842a0
commit b25b95f933
15 changed files with 294 additions and 48 deletions

View File

@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMmsConnection;
@ -848,8 +849,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, getString(R.string.ConversationActivity_sorry_the_selected_video_exceeds_message_size_restrictions,
(Slide.MAX_MESSAGE_SIZE/1024)),
(MmsMediaConstraints.MAX_MESSAGE_SIZE/1024)),
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
@ -866,7 +868,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, getString(R.string.ConversationActivity_sorry_the_selected_audio_exceeds_message_size_restrictions,
(Slide.MAX_MESSAGE_SIZE/1024)),
(MmsMediaConstraints.MAX_MESSAGE_SIZE/1024)),
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}

View File

@ -461,6 +461,38 @@ public class PartDatabase extends Database {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public void updatePartData(MasterSecret masterSecret, PduPart part, InputStream data)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = writePartData(masterSecret, part, data);
if (partData == null) throw new MmsException("couldn't update part data");
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, new String[]{DATA}, ID_WHERE,
new String[]{part.getId()+""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int dataColumn = cursor.getColumnIndexOrThrow(DATA);
if (!cursor.isNull(dataColumn) && !new File(cursor.getString(dataColumn)).delete()) {
Log.w(TAG, "Couldn't delete old part file");
}
}
} finally {
if (cursor != null) cursor.close();
}
ContentValues values = new ContentValues(2);
values.put(DATA, partData.first.getAbsolutePath());
values.put(SIZE, partData.second);
part.setDataSize(partData.second);
database.update(TABLE_NAME, values, ID_WHERE, new String[] {part.getId()+""});
Log.w(TAG, "updated data for part #" + part.getId());
}
public void updatePartThumbnail(MasterSecret masterSecret, long partId, PduPart part, InputStream in, float aspectRatio)
throws MmsException
{

View File

@ -12,11 +12,11 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
import org.thoughtcrime.securesms.mms.MediaConstraints;
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;
@ -24,21 +24,17 @@ 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.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;
@ -69,8 +65,6 @@ public class MmsSendJob extends SendJob {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
populatePartData(message.getBody(), masterSecret);
try {
MmsSendResult result = deliver(masterSecret, message);
@ -101,24 +95,6 @@ public class MmsSendJob extends SendJob {
notifyMediaMessageDeliveryFailed(context, messageId);
}
private void populatePartData(PduPart part, MasterSecret masterSecret) throws IOException {
if (part.getDataUri() == null) {
return;
}
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
{
@ -182,13 +158,11 @@ public class MmsSendJob extends SendJob {
message.setFrom(new EncodedStringValue(number));
}
prepareMessageMedia(masterSecret, message, MediaConstraints.MMS_CONSTRAINTS, true);
try {
OutgoingMmsConnection connection = new OutgoingMmsConnection(context, radio.getApnInformation(), new PduComposer(context, message).make());
SendConf conf = connection.send(usingMmsRadio, useProxy);
for (int i=0;i<message.getBody().getPartsNum();i++) {
Log.w(TAG, "Sent MMS part of content-type: " + new String(message.getBody().getPart(i).getContentType()));
}
SendConf conf = connection.send(usingMmsRadio, useProxy);
if (conf == null) {
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");

View File

@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException;
@ -58,7 +60,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override
public void onSend(MasterSecret masterSecret)
throws RetryLaterException, MmsException, NoSuchMessageException
throws RetryLaterException, MmsException, NoSuchMessageException, UndeliverableMessageException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
@ -99,7 +101,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
private boolean deliver(MasterSecret masterSecret, SendReq message)
throws RetryLaterException, SecureFallbackApprovalException,
InsecureFallbackApprovalException, UntrustedIdentityException
InsecureFallbackApprovalException, UntrustedIdentityException,
UndeliverableMessageException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
TextSecureMessageSender messageSender = messageSenderFactory.create(masterSecret);
@ -107,6 +110,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
boolean isSmsFallbackSupported = isSmsFallbackSupported(context, destination, true);
try {
prepareMessageMedia(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
PushAddress address = getPushAddress(recipients.getPrimaryRecipient());
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);

View File

@ -6,7 +6,10 @@ 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.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@ -21,7 +24,6 @@ import org.thoughtcrime.securesms.database.TextSecureDirectory;
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;

View File

@ -1,14 +1,27 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.TextSecureExpiredException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
public abstract class SendJob extends MasterSecretJob {
private final static String TAG = SendJob.class.getSimpleName();
public SendJob(Context context, JobParameters parameters) {
super(context, parameters);
@ -26,4 +39,54 @@ public abstract class SendJob extends MasterSecretJob {
}
protected abstract void onSend(MasterSecret masterSecret) throws Exception;
protected void prepareMessageMedia(MasterSecret masterSecret, SendReq message,
MediaConstraints constraints, boolean toMemory)
throws IOException, UndeliverableMessageException
{
try {
for (int i = 0; i < message.getBody().getPartsNum(); i++) {
preparePart(masterSecret, constraints, message.getBody().getPart(i), toMemory);
}
} catch (MmsException me) {
throw new UndeliverableMessageException(me);
}
}
private void preparePart(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part, boolean toMemory)
throws IOException, MmsException, UndeliverableMessageException
{
byte[] resizedData = null;
if (!constraints.isSatisfied(context, masterSecret, part)) {
if (!constraints.canResize(part)) {
throw new UndeliverableMessageException("Size constraints could not be satisfied.");
}
resizedData = resizePart(masterSecret, constraints, part);
}
if (toMemory) {
part.setData(resizedData != null ? resizedData : MediaUtil.getPartData(context, masterSecret, part));
}
if (resizedData != null) {
part.setDataSize(resizedData.length);
}
}
private byte[] resizePart(MasterSecret masterSecret, MediaConstraints constraints,
PduPart part)
throws IOException, MmsException
{
Log.w(TAG, "resizing part " + part.getId());
final long oldSize = part.getDataSize();
final byte[] data = constraints.getResizedMedia(context, masterSecret, part);
DatabaseFactory.getPartDatabase(context).updatePartData(masterSecret, part, new ByteArrayInputStream(data));
Log.w(TAG, String.format("Resized part %.1fkb => %.1fkb", oldSize / 1024.0, part.getDataSize() / 1024.0));
return data;
}
}

View File

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.io.InputStream;
import ws.com.google.android.mms.pdu.PduPart;
public abstract class MediaConstraints {
private static final String TAG = MediaConstraints.class.getSimpleName();
public static MediaConstraints MMS_CONSTRAINTS = new MmsMediaConstraints();
public static MediaConstraints PUSH_CONSTRAINTS = new PushMediaConstraints();
public abstract int getImageMaxWidth();
public abstract int getImageMaxHeight();
public abstract int getImageMaxSize();
public abstract int getVideoMaxSize();
public abstract int getAudioMaxSize();
public boolean isSatisfied(Context context, MasterSecret masterSecret, PduPart part) {
try {
return (MediaUtil.isImage(part) && part.getDataSize() <= getImageMaxSize() && isWithinBounds(context, masterSecret, part.getDataUri())) ||
(MediaUtil.isAudio(part) && part.getDataSize() <= getAudioMaxSize()) ||
(MediaUtil.isVideo(part) && part.getDataSize() <= getVideoMaxSize()) ||
(!MediaUtil.isImage(part) && !MediaUtil.isAudio(part) && !MediaUtil.isVideo(part));
} catch (IOException ioe) {
Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe);
return false;
}
}
public boolean isWithinBounds(Context context, MasterSecret masterSecret, Uri uri) throws IOException {
InputStream is = PartAuthority.getPartStream(context, masterSecret, uri);
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(is);
return dimensions.first > 0 && dimensions.first <= getImageMaxWidth() &&
dimensions.second > 0 && dimensions.second <= getImageMaxHeight();
}
public boolean canResize(PduPart part) {
return part != null && MediaUtil.isImage(part);
}
public byte[] getResizedMedia(Context context, MasterSecret masterSecret, PduPart part)
throws IOException
{
if (!canResize(part) || part.getDataUri() == null) {
throw new UnsupportedOperationException("Cannot resize this content type");
}
try {
return BitmapUtil.createScaledBytes(context, masterSecret, part.getDataUri(),
getImageMaxWidth(),
getImageMaxHeight(),
getImageMaxSize());
} catch (BitmapDecodingException bde) {
throw new IOException(bde);
}
}
}

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.mms;
public class MmsMediaConstraints extends MediaConstraints {
private static final int MAX_IMAGE_DIMEN = 1280;
public static final int MAX_MESSAGE_SIZE = 280 * 1024;
@Override
public int getImageMaxWidth() {
return MAX_IMAGE_DIMEN;
}
@Override
public int getImageMaxHeight() {
return MAX_IMAGE_DIMEN;
}
@Override
public int getImageMaxSize() {
return MAX_MESSAGE_SIZE;
}
@Override
public int getVideoMaxSize() {
return MAX_MESSAGE_SIZE;
}
@Override
public int getAudioMaxSize() {
return MAX_MESSAGE_SIZE;
}
}

View File

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.mms;
public class PushMediaConstraints extends MediaConstraints {
private static final int MAX_IMAGE_DIMEN = 1280;
private static final int KB = 1024;
private static final int MB = 1024 * KB;
@Override
public int getImageMaxWidth() {
return MAX_IMAGE_DIMEN;
}
@Override
public int getImageMaxHeight() {
return MAX_IMAGE_DIMEN;
}
@Override
public int getImageMaxSize() {
return 300 * KB;
}
@Override
public int getVideoMaxSize() {
return MmsMediaConstraints.MAX_MESSAGE_SIZE;
}
@Override
public int getAudioMaxSize() {
return MmsMediaConstraints.MAX_MESSAGE_SIZE;
}
}

View File

@ -36,8 +36,6 @@ import ws.com.google.android.mms.pdu.PduPart;
public abstract class Slide {
public static final int MAX_MESSAGE_SIZE = 280 * 1024;
protected final PduPart part;
protected final Context context;
protected MasterSecret masterSecret;
@ -124,7 +122,7 @@ public abstract class Slide {
while ((read = in.read(buffer)) != -1) {
size += read;
if (size > MAX_MESSAGE_SIZE) throw new MediaTooLargeException("Media exceeds maximum message size.");
if (size > MmsMediaConstraints.MAX_MESSAGE_SIZE) throw new MediaTooLargeException("Media exceeds maximum message size.");
}
}
}

View File

@ -75,10 +75,12 @@ public class VideoSlide extends Slide {
return SmilUtil.createMediaElement("video", document, new String(getPart().getName()));
}
private static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {
PduPart part = new PduPart();
private static PduPart constructPartFromUri(Context context, Uri uri)
throws IOException, MediaTooLargeException
{
PduPart part = new PduPart();
ContentResolver resolver = context.getContentResolver();
Cursor cursor = null;
Cursor cursor = null;
try {
cursor = resolver.query(uri, new String[] {MediaStore.Video.Media.MIME_TYPE}, null, null, null);

View File

@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.transport;
public class UndeliverableMessageException extends Throwable {
public class UndeliverableMessageException extends Exception {
public UndeliverableMessageException() {
}

View File

@ -6,13 +6,10 @@ import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
@ -29,9 +26,6 @@ import java.io.InputStream;
import com.android.gallery3d.data.Exif;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.PartAuthority;
public class BitmapUtil {
private static final String TAG = BitmapUtil.class.getSimpleName();
@ -61,6 +55,8 @@ public class BitmapUtil {
quality = Math.max((quality * maxSize) / baos.size(), MIN_COMPRESSION_QUALITY);
} while (baos.size() > maxSize && attempts++ < MAX_COMPRESSION_ATTEMPTS);
Log.w(TAG, "createScaledBytes(" + uri + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)");
bitmap.recycle();
if (baos.size() <= maxSize) return baos.toByteArray();
@ -216,6 +212,11 @@ public class BitmapUtil {
return options;
}
public static Pair<Integer, Integer> getDimensions(InputStream inputStream) {
BitmapFactory.Options options = getImageDimensions(inputStream);
return new Pair<>(options.outWidth, options.outHeight);
}
public static Bitmap getCircleCroppedBitmap(Bitmap bitmap) {
if (bitmap == null) return null;
final int srcSize = Math.min(bitmap.getWidth(), bitmap.getHeight());

View File

@ -9,7 +9,12 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@ -18,6 +23,7 @@ import java.util.concurrent.Callable;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
public class MediaUtil {
private static final String TAG = MediaUtil.class.getSimpleName();
@ -39,6 +45,16 @@ public class MediaUtil {
return data;
}
public static byte[] getPartData(Context context, MasterSecret masterSecret, PduPart part)
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);
return os.toByteArray();
}
private static Bitmap generateImageThumbnail(Context context, MasterSecret masterSecret, Uri uri)
throws IOException, BitmapDecodingException, OutOfMemoryError
{
@ -46,6 +62,18 @@ public class MediaUtil {
return BitmapUtil.createScaledBitmap(context, masterSecret, uri, maxSize, maxSize);
}
public static boolean isImage(PduPart part) {
return ContentType.isImageType(Util.toIsoString(part.getContentType()));
}
public static boolean isAudio(PduPart part) {
return ContentType.isAudioType(Util.toIsoString(part.getContentType()));
}
public static boolean isVideo(PduPart part) {
return ContentType.isVideoType(Util.toIsoString(part.getContentType()));
}
public static class ThumbnailData {
Bitmap bitmap;
float aspectRatio;

View File

@ -20,8 +20,13 @@ package ws.com.google.android.mms.pdu;
import android.graphics.Bitmap;
import android.net.Uri;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;