Significant MMS changes

1) Remove all our PDU code and switch to the PDU code from the
   klinker library

2) Switch to using the system Lollipop MMS library by default,
   and falling back to our own custom library if that fails.

3) Format SMIL differently, using code from klinker instead of
   what we've pieced together.

4) Pull per-carrier MMS media constraints from the XML config
   files in the klinker library, instead of hardcoding it at 280kb.

Hopefully this is an improvement, but given that MMS is involved,
it will probably make things worse instead.
This commit is contained in:
Moxie Marlinspike
2017-05-08 15:32:59 -07:00
parent 165fae5734
commit 51d6144591
99 changed files with 530 additions and 11696 deletions

View File

@@ -72,14 +72,14 @@ public class BitmapUtil {
Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb");
if (quality == MIN_COMPRESSION_QUALITY) break;
int nextQuality = (int)Math.floor(quality * Math.sqrt((double)constraints.getImageMaxSize() / bytes.length));
int nextQuality = (int)Math.floor(quality * Math.sqrt((double)constraints.getImageMaxSize(context) / bytes.length));
if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) {
nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE;
}
quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY);
}
while (bytes.length > constraints.getImageMaxSize() && attempts++ < MAX_COMPRESSION_ATTEMPTS);
if (bytes.length > constraints.getImageMaxSize()) {
while (bytes.length > constraints.getImageMaxSize(context) && attempts++ < MAX_COMPRESSION_ATTEMPTS);
if (bytes.length > constraints.getImageMaxSize(context)) {
throw new BitmapDecodingException("Unable to scale image below: " + bytes.length);
}
Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)");

View File

@@ -31,19 +31,25 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutionException;
import ws.com.google.android.mms.ContentType;
public class MediaUtil {
private static final String TAG = MediaUtil.class.getSimpleName();
public static final String IMAGE_PNG = "image/png";
public static final String IMAGE_JPEG = "image/jpeg";
public static final String IMAGE_GIF = "image/gif";
public static final String AUDIO_AAC = "audio/aac";
public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String VIDEO_UNSPECIFIED = "video/*";
public static @Nullable ThumbnailData generateThumbnail(Context context, MasterSecret masterSecret, String contentType, Uri uri)
throws BitmapDecodingException
{
long startMillis = System.currentTimeMillis();
ThumbnailData data = null;
if (ContentType.isImageType(contentType)) {
if (isImageType(contentType)) {
data = new ThumbnailData(generateImageThumbnail(context, masterSecret, uri));
}
@@ -77,11 +83,11 @@ public class MediaUtil {
Slide slide = null;
if (isGif(attachment.getContentType())) {
slide = new GifSlide(context, attachment);
} else if (ContentType.isImageType(attachment.getContentType())) {
} else if (isImageType(attachment.getContentType())) {
slide = new ImageSlide(context, attachment);
} else if (ContentType.isVideoType(attachment.getContentType())) {
} else if (isVideoType(attachment.getContentType())) {
slide = new VideoSlide(context, attachment);
} else if (ContentType.isAudioType(attachment.getContentType())) {
} else if (isAudioType(attachment.getContentType())) {
slide = new AudioSlide(context, attachment);
} else if (isMms(attachment.getContentType())) {
slide = new MmsSlide(context, attachment);
@@ -112,8 +118,8 @@ public class MediaUtil {
switch(mimeType) {
case "image/jpg":
return MimeTypeMap.getSingleton().hasMimeType(ContentType.IMAGE_JPEG)
? ContentType.IMAGE_JPEG
return MimeTypeMap.getSingleton().hasMimeType(IMAGE_JPEG)
? IMAGE_JPEG
: mimeType;
default:
return mimeType;
@@ -140,34 +146,50 @@ public class MediaUtil {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals("application/mms");
}
public static boolean isGif(String contentType) {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif");
}
public static boolean isGif(Attachment attachment) {
return isGif(attachment.getContentType());
}
public static boolean isImage(Attachment attachment) {
return ContentType.isImageType(attachment.getContentType());
return isImageType(attachment.getContentType());
}
public static boolean isAudio(Attachment attachment) {
return ContentType.isAudioType(attachment.getContentType());
return isAudioType(attachment.getContentType());
}
public static boolean isVideo(Attachment attachment) {
return ContentType.isVideoType(attachment.getContentType());
return isVideoType(attachment.getContentType());
}
public static boolean isVideo(String contentType) {
return !TextUtils.isEmpty(contentType) && contentType.trim().startsWith("video/");
}
public static boolean isGif(String contentType) {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif");
}
public static boolean isFile(Attachment attachment) {
return !isGif(attachment) && !isImage(attachment) && !isAudio(attachment) && !isVideo(attachment);
}
public static boolean isTextType(String contentType) {
return (null != contentType) && contentType.startsWith("text/");
}
public static boolean isImageType(String contentType) {
return (null != contentType) && contentType.startsWith("image/");
}
public static boolean isAudioType(String contentType) {
return (null != contentType) && contentType.startsWith("audio/");
}
public static boolean isVideoType(String contentType) {
return (null != contentType) && contentType.startsWith("video/");
}
public static boolean hasVideoThumbnail(Uri uri) {
Log.w(TAG, "Checking: " + uri);

View File

@@ -1,132 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.util.Log;
import org.thoughtcrime.securesms.dom.smil.SmilDocumentImpl;
import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer;
import org.thoughtcrime.securesms.mms.PartParser;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILElement;
import org.w3c.dom.smil.SMILLayoutElement;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILParElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.w3c.dom.smil.SMILRegionMediaElement;
import org.w3c.dom.smil.SMILRootLayoutElement;
import java.io.ByteArrayOutputStream;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduBody;
import ws.com.google.android.mms.pdu.PduPart;
public class SmilUtil {
private static final String TAG = SmilUtil.class.getSimpleName();
public static final int ROOT_HEIGHT = 1024;
public static final int ROOT_WIDTH = 1024;
public static PduBody getSmilBody(PduBody body) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
SmilXmlSerializer.serialize(SmilUtil.createSmilDocument(body), out);
PduPart smilPart = new PduPart();
smilPart.setContentId("smil".getBytes());
smilPart.setContentLocation("smil.xml".getBytes());
smilPart.setContentType(ContentType.APP_SMIL.getBytes());
smilPart.setData(out.toByteArray());
body.addPart(0, smilPart);
return body;
}
private static SMILDocument createSmilDocument(PduBody body) {
Log.w(TAG, "Creating SMIL document from PduBody.");
SMILDocument document = new SmilDocumentImpl();
SMILElement smilElement = (SMILElement) document.createElement("smil");
document.appendChild(smilElement);
SMILElement headElement = (SMILElement) document.createElement("head");
smilElement.appendChild(headElement);
SMILLayoutElement layoutElement = (SMILLayoutElement) document.createElement("layout");
headElement.appendChild(layoutElement);
SMILRootLayoutElement rootLayoutElement = (SMILRootLayoutElement) document.createElement("root-layout");
rootLayoutElement.setWidth(ROOT_WIDTH);
rootLayoutElement.setHeight(ROOT_HEIGHT);
layoutElement.appendChild(rootLayoutElement);
SMILElement bodyElement = (SMILElement) document.createElement("body");
smilElement.appendChild(bodyElement);
SMILParElement par = (SMILParElement) document.createElement("par");
bodyElement.appendChild(par);
for (int i=0; i<body.getPartsNum(); i++) {
PduPart part = body.getPart(i);
SMILRegionElement regionElement = getRegion(document, part);
SMILMediaElement mediaElement = getMediaElement(document, part);
if (regionElement != null) {
((SMILRegionMediaElement)mediaElement).setRegion(regionElement);
layoutElement.appendChild(regionElement);
}
par.appendChild(mediaElement);
}
return document;
}
private static SMILRegionElement getRegion(SMILDocument document, PduPart part) {
if (PartParser.isAudio(part)) return null;
SMILRegionElement region = (SMILRegionElement) document.createElement("region");
if (PartParser.isText(part)) {
region.setId("Text");
region.setTop(SmilUtil.ROOT_HEIGHT);
region.setHeight(50);
} else {
region.setId("Image");
region.setTop(0);
region.setHeight(SmilUtil.ROOT_HEIGHT);
}
region.setLeft(0);
region.setWidth(SmilUtil.ROOT_WIDTH);
region.setFit("meet");
return region;
}
private static SMILMediaElement getMediaElement(SMILDocument document, PduPart part) {
final String tag;
if (PartParser.isImage(part)) {
tag = "img";
} else if (PartParser.isAudio(part)) {
tag = "audio";
} else if (PartParser.isVideo(part)) {
tag = "video";
} else if (PartParser.isText(part)) {
tag = "text";
} else {
tag = "ref";
}
return createMediaElement(tag, document, new String(part.getName() == null
? new byte[]{}
: part.getName()));
}
private static SMILMediaElement createMediaElement(String tag, SMILDocument document, String src) {
SMILMediaElement mediaElement = (SMILMediaElement) document.createElement(tag);
mediaElement.setSrc(escapeXML(src));
return mediaElement;
}
private static String escapeXML(String str) {
return str.replaceAll("&","&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&apos;");
}
}

View File

@@ -40,6 +40,8 @@ import android.text.style.StyleSpan;
import android.util.Log;
import android.widget.EditText;
import com.google.android.mms.pdu_alt.CharacterSets;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.BuildConfig;
@@ -65,9 +67,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
public class Util {
private static final String TAG = Util.class.getSimpleName();

View File

@@ -6,11 +6,15 @@ import android.support.annotation.Nullable;
public class SubscriptionInfoCompat {
private final int subscriptionId;
private final int mcc;
private final int mnc;
private final @Nullable CharSequence displayName;
public SubscriptionInfoCompat(int subscriptionId, @Nullable CharSequence displayName) {
public SubscriptionInfoCompat(int subscriptionId, @Nullable CharSequence displayName, int mcc, int mnc) {
this.subscriptionId = subscriptionId;
this.displayName = displayName;
this.mcc = mcc;
this.mnc = mnc;
}
public @NonNull CharSequence getDisplayName() {
@@ -20,4 +24,12 @@ public class SubscriptionInfoCompat {
public int getSubscriptionId() {
return subscriptionId;
}
public int getMnc() {
return mnc;
}
public int getMcc() {
return mcc;
}
}

View File

@@ -35,7 +35,8 @@ public class SubscriptionManagerCompat {
SubscriptionInfo subscriptionInfo = SubscriptionManager.from(context).getActiveSubscriptionInfo(subscriptionId);
if (subscriptionInfo != null) {
return Optional.of(new SubscriptionInfoCompat(subscriptionId, subscriptionInfo.getDisplayName()));
return Optional.of(new SubscriptionInfoCompat(subscriptionId, subscriptionInfo.getDisplayName(),
subscriptionInfo.getMcc(), subscriptionInfo.getMnc()));
} else {
return Optional.absent();
}
@@ -56,7 +57,9 @@ public class SubscriptionManagerCompat {
for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
compatList.add(new SubscriptionInfoCompat(subscriptionInfo.getSubscriptionId(),
subscriptionInfo.getDisplayName()));
subscriptionInfo.getDisplayName(),
subscriptionInfo.getMcc(),
subscriptionInfo.getMnc()));
}
return compatList;