Large attachment support

Closes #4019
// FREEBIE
This commit is contained in:
Jake McGinty 2015-09-04 17:33:22 -07:00 committed by Moxie Marlinspike
parent 4f7ac59c6f
commit 551274f167
14 changed files with 209 additions and 246 deletions

View File

@ -126,6 +126,7 @@
<string name="ConversationActivity_unblock_question">Unblock?</string> <string name="ConversationActivity_unblock_question">Unblock?</string>
<string name="ConversationActivity_are_you_sure_you_want_to_unblock_this_contact">Are you sure you want to unblock this contact?</string> <string name="ConversationActivity_are_you_sure_you_want_to_unblock_this_contact">Are you sure you want to unblock this contact?</string>
<string name="ConversationActivity_unblock">Unblock</string> <string name="ConversationActivity_unblock">Unblock</string>
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
<!-- ConversationFragment --> <!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string> <string name="ConversationFragment_message_details">Message details</string>

View File

@ -35,6 +35,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.WindowCompat; import android.support.v4.view.WindowCompat;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -85,6 +86,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaTooLargeException; import org.thoughtcrime.securesms.mms.MediaTooLargeException;
@ -113,6 +115,7 @@ import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@ -288,13 +291,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
switch (reqCode) { switch (reqCode) {
case PICK_IMAGE: case PICK_IMAGE:
addAttachmentImage(masterSecret, data.getData()); setMedia(data.getData(),
MediaUtil.isGif(MediaUtil.getMimeType(this, data.getData())) ? MediaType.GIF
: MediaType.IMAGE,
false);
break; break;
case PICK_VIDEO: case PICK_VIDEO:
addAttachmentVideo(data.getData()); setMedia(data.getData(), MediaType.VIDEO, false);
break; break;
case PICK_AUDIO: case PICK_AUDIO:
addAttachmentAudio(data.getData()); setMedia(data.getData(), MediaType.AUDIO, false);
break; break;
case PICK_CONTACT_INFO: case PICK_CONTACT_INFO:
addAttachmentContactInfo(data.getData()); addAttachmentContactInfo(data.getData());
@ -308,7 +314,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
break; break;
case TAKE_PHOTO: case TAKE_PHOTO:
if (attachmentManager.getCaptureUri() != null) { if (attachmentManager.getCaptureUri() != null) {
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri()); setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE, true);
} }
break; break;
} }
@ -671,9 +677,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA); Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);
if (draftText != null) composeText.setText(draftText); if (draftText != null) composeText.setText(draftText);
if (draftImage != null) addAttachmentImage(masterSecret, draftImage);
if (draftAudio != null) addAttachmentAudio(draftAudio); if (draftImage != null) setMedia(draftImage, MediaType.IMAGE, false);
if (draftVideo != null) addAttachmentVideo(draftVideo); else if (draftAudio != null) setMedia(draftAudio, MediaType.AUDIO, false);
else if (draftVideo != null) setMedia(draftVideo, MediaType.VIDEO, false);
if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) { if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) {
initializeDraftFromDatabase(); initializeDraftFromDatabase();
@ -707,11 +714,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draft.getType().equals(Draft.TEXT)) { if (draft.getType().equals(Draft.TEXT)) {
composeText.setText(draft.getValue()); composeText.setText(draft.getValue());
} else if (draft.getType().equals(Draft.IMAGE)) { } else if (draft.getType().equals(Draft.IMAGE)) {
addAttachmentImage(masterSecret, Uri.parse(draft.getValue())); setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE, false);
} else if (draft.getType().equals(Draft.AUDIO)) { } else if (draft.getType().equals(Draft.AUDIO)) {
addAttachmentAudio(Uri.parse(draft.getValue())); setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO, false);
} else if (draft.getType().equals(Draft.VIDEO)) { } else if (draft.getType().equals(Draft.VIDEO)) {
addAttachmentVideo(Uri.parse(draft.getValue())); setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO, false);
} }
} }
@ -917,55 +924,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
} }
private void addAttachmentImage(MasterSecret masterSecret, Uri imageUri) { private void setMedia(Uri uri, MediaType mediaType, boolean isCapture) {
try { attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints(), isCapture);
attachmentManager.setImage(masterSecret, imageUri);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_LONG).show();
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, getString(R.string.ConversationActivity_the_gif_you_selected_was_too_big),
Toast.LENGTH_LONG).show();
Log.w(TAG, e);
}
}
private void addAttachmentVideo(Uri videoUri) {
try {
attachmentManager.setVideo(videoUri);
} catch (IOException e) {
attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, getString(R.string.ConversationActivity_sorry_the_selected_video_exceeds_message_size_restrictions,
(MmsMediaConstraints.MAX_MESSAGE_SIZE/1024)),
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
}
private void addAttachmentAudio(Uri audioUri) {
try {
attachmentManager.setAudio(audioUri);
} catch (IOException e) {
attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
} catch (MediaTooLargeException e) {
attachmentManager.clear();
Toast.makeText(this, getString(R.string.ConversationActivity_sorry_the_selected_audio_exceeds_message_size_restrictions,
(MmsMediaConstraints.MAX_MESSAGE_SIZE/1024)),
Toast.LENGTH_LONG).show();
Log.w("ComposeMessageActivity", e);
}
} }
private void addAttachmentContactInfo(Uri contactUri) { private void addAttachmentContactInfo(Uri contactUri) {
@ -1132,6 +1092,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return rawText; return rawText;
} }
private MediaConstraints getCurrentMediaConstraints() {
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
? MediaConstraints.PUSH_CONSTRAINTS
: MediaConstraints.MMS_CONSTRAINTS;
}
private void markThreadAsRead() { private void markThreadAsRead() {
new AsyncTask<Long, Void, Void>() { new AsyncTask<Long, Void, Void>() {
@Override @Override
@ -1198,8 +1164,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = getApplicationContext(); final Context context = getApplicationContext();
SlideDeck slideDeck; SlideDeck slideDeck;
if (attachmentManager.isAttachmentPresent()) slideDeck = new SlideDeck(attachmentManager.getSlideDeck()); if (attachmentManager.isAttachmentPresent()) {
else slideDeck = new SlideDeck(); Slide mediaSlide = attachmentManager.getSlideDeck().getThumbnailSlide();
MediaConstraints constraints = getCurrentMediaConstraints();
if (mediaSlide != null &&
!constraints.isSatisfied(this, masterSecret, mediaSlide.getPart()) &&
!constraints.canResize(mediaSlide.getPart()))
{
Toast.makeText(context,
R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show();
return;
}
slideDeck = new SlideDeck(attachmentManager.getSlideDeck());
} else {
slideDeck = new SlideDeck();
}
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck, OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck,
getMessage(), distributionType); getMessage(), distributionType);
@ -1272,8 +1254,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void onImageCapture(@NonNull final byte[] imageBytes) { public void onImageCapture(@NonNull final byte[] imageBytes) {
attachmentManager.setCaptureUri(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes)); setMedia(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes), MediaType.IMAGE, true);
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
quickAttachmentDrawer.hide(false); quickAttachmentDrawer.hide(false);
} }
@ -1397,4 +1378,5 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeSecurity(); initializeSecurity();
updateToggleButtonState(); updateToggleButtonState();
} }
} }

View File

@ -74,7 +74,7 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
part.setContentType(imageRecord.getContentType().getBytes()); part.setContentType(imageRecord.getContentType().getBytes());
part.setPartId(imageRecord.getPartId()); part.setPartId(imageRecord.getPartId());
Slide slide = MediaUtil.getSlideForPart(getContext(), masterSecret, part, imageRecord.getContentType()); Slide slide = MediaUtil.getSlideForPart(getContext(), part, imageRecord.getContentType());
if (slide != null) { if (slide != null) {
imageView.setImageResource(slide, masterSecret); imageView.setImageResource(slide, masterSecret);
} }

View File

@ -201,7 +201,12 @@ public class ThumbnailView extends FrameLayout {
} }
public void clear() { public void clear() {
if (isContextValid()) Glide.clear(this); if (isContextValid()) Glide.clear(image);
if (slideDeckFuture != null) slideDeckFuture.removeListener(slideDeckListener);
slide = null;
slideId = null;
slideDeckFuture = null;
slideDeckListener = null;
} }
public void hideControls(boolean hideControls) { public void hideControls(boolean hideControls) {
@ -209,6 +214,11 @@ public class ThumbnailView extends FrameLayout {
if (hideControls) hideProgressWheel(); if (hideControls) hideProgressWheel();
} }
public void showProgressSpinner() {
getProgressWheel().spin();
getProgressWheel().setVisibility(VISIBLE);
}
@TargetApi(VERSION_CODES.JELLY_BEAN_MR1) @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
private boolean isContextValid() { private boolean isContextValid() {
return !(getContext() instanceof Activity) || return !(getContext() instanceof Activity) ||

View File

@ -1094,7 +1094,7 @@ public class MmsDatabase extends MessagingDatabase {
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument); List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
List<NetworkFailure> networkFailures = getFailures(networkDocument); List<NetworkFailure> networkFailures = getFailures(networkDocument);
ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, dateReceived, id); ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(dateReceived, id);
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(), return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, receiptCount, addressDeviceId, dateSent, dateReceived, receiptCount,
@ -1159,8 +1159,7 @@ public class MmsDatabase extends MessagingDatabase {
} }
} }
private ListenableFutureTask<SlideDeck> getSlideDeck(final MasterSecret masterSecret, private ListenableFutureTask<SlideDeck> getSlideDeck(final long timestamp,
final long timestamp,
final long id) final long id)
{ {
ListenableFutureTask<SlideDeck> future = getCachedSlideDeck(timestamp, id); ListenableFutureTask<SlideDeck> future = getCachedSlideDeck(timestamp, id);
@ -1172,12 +1171,9 @@ public class MmsDatabase extends MessagingDatabase {
Callable<SlideDeck> task = new Callable<SlideDeck>() { Callable<SlideDeck> task = new Callable<SlideDeck>() {
@Override @Override
public SlideDeck call() throws Exception { public SlideDeck call() throws Exception {
if (masterSecret == null)
return null;
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context); PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
PduBody body = getPartsAsBody(partDatabase.getParts(id)); PduBody body = getPartsAsBody(partDatabase.getParts(id));
SlideDeck slideDeck = new SlideDeck(context, masterSecret, body); SlideDeck slideDeck = new SlideDeck(context, body);
if (!body.containsPushInProgress()) { if (!body.containsPushInProgress()) {
slideCache.put(timestamp + "::" + id, new SoftReference<>(slideDeck)); slideCache.put(timestamp + "::" + id, new SoftReference<>(slideDeck));

View File

@ -21,9 +21,11 @@ import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@ -36,7 +38,6 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException; import java.io.IOException;
@ -54,7 +55,7 @@ public class AttachmentManager {
public AttachmentManager(Activity view, AttachmentListener listener) { public AttachmentManager(Activity view, AttachmentListener listener) {
this.attachmentView = view.findViewById(R.id.attachment_editor); this.attachmentView = view.findViewById(R.id.attachment_editor);
this.thumbnail = (ThumbnailView)view.findViewById(R.id.attachment_thumbnail); this.thumbnail = (ThumbnailView) view.findViewById(R.id.attachment_thumbnail);
this.slideDeck = new SlideDeck(); this.slideDeck = new SlideDeck();
this.context = view; this.context = view;
this.attachmentListener = listener; this.attachmentListener = listener;
@ -70,6 +71,7 @@ public class AttachmentManager {
@Override public void onAnimationRepeat(Animation animation) {} @Override public void onAnimationRepeat(Animation animation) {}
@Override public void onAnimationEnd(Animation animation) { @Override public void onAnimationEnd(Animation animation) {
slideDeck.clear(); slideDeck.clear();
thumbnail.clear();
attachmentView.setVisibility(View.GONE); attachmentView.setVisibility(View.GONE);
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
} }
@ -83,34 +85,55 @@ public class AttachmentManager {
captureUri = null; captureUri = null;
} }
public void setImage(MasterSecret masterSecret, Uri image) public void setMedia(@NonNull final MasterSecret masterSecret,
throws IOException, BitmapDecodingException, MediaTooLargeException @NonNull final Uri uri,
@NonNull final MediaType mediaType,
@NonNull final MediaConstraints constraints,
final boolean isCapture)
{ {
if (MediaUtil.isGif(MediaUtil.getMimeType(context, image))) { new AsyncTask<Void, Void, Slide>() {
setMedia(new GifSlide(context, masterSecret, image), masterSecret); @Override protected void onPreExecute() {
} else { slideDeck.clear();
setMedia(new ImageSlide(context, masterSecret, image), masterSecret); thumbnail.clear();
} thumbnail.showProgressSpinner();
} attachmentView.setVisibility(View.VISIBLE);
public void setVideo(Uri video) throws IOException, MediaTooLargeException { if (isCapture) captureUri = uri;
setMedia(new VideoSlide(context, video)); if (!uri.equals(captureUri)) cleanup();
} }
public void setAudio(Uri audio) throws IOException, MediaTooLargeException { @Override protected @Nullable Slide doInBackground(Void... params) {
setMedia(new AudioSlide(context, audio)); long start = System.currentTimeMillis();
} try {
final long mediaSize = MediaUtil.getMediaSize(context, masterSecret, uri);
final Slide slide = mediaType.createSlide(context, uri, mediaSize);
Log.w(TAG, "slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
return slide;
} catch (IOException ioe) {
Log.w(TAG, ioe);
return null;
}
}
public void setMedia(final Slide slide) { @Override protected void onPostExecute(@Nullable final Slide slide) {
setMedia(slide, null); if (slide == null) {
} attachmentView.setVisibility(View.GONE);
Toast.makeText(context,
public void setMedia(final Slide slide, @Nullable MasterSecret masterSecret) { R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
slideDeck.clear(); Toast.LENGTH_SHORT).show();
slideDeck.addSlide(slide); } else if (!areConstraintsSatisfied(context, masterSecret, slide, constraints)) {
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.GONE);
thumbnail.setImageResource(slide, masterSecret); Toast.makeText(context,
attachmentListener.onAttachmentChanged(); R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show();
} else {
slideDeck.addSlide(slide);
attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(slide, masterSecret);
attachmentListener.onAttachmentChanged();
}
}
}.execute();
} }
public boolean isAttachmentPresent() { public boolean isAttachmentPresent() {
@ -118,7 +141,7 @@ public class AttachmentManager {
} }
public SlideDeck getSlideDeck() { public @NonNull SlideDeck getSlideDeck() {
return slideDeck; return slideDeck;
} }
@ -143,10 +166,6 @@ public class AttachmentManager {
return captureUri; return captureUri;
} }
public void setCaptureUri(Uri captureUri) {
this.captureUri = captureUri;
}
public void capturePhoto(Activity activity, Recipients recipients, int requestCode) { public void capturePhoto(Activity activity, Recipients recipients, int requestCode) {
try { try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
@ -183,6 +202,16 @@ public class AttachmentManager {
} }
} }
private boolean areConstraintsSatisfied(final @NonNull Context context,
final @NonNull MasterSecret masterSecret,
final @Nullable Slide slide,
final @NonNull MediaConstraints constraints)
{
return slide == null ||
constraints.isSatisfied(context, masterSecret, slide.getPart()) ||
constraints.canResize(slide.getPart());
}
private class RemoveButtonListener implements View.OnClickListener { private class RemoveButtonListener implements View.OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -194,4 +223,22 @@ public class AttachmentManager {
public interface AttachmentListener { public interface AttachmentListener {
void onAttachmentChanged(); void onAttachmentChanged();
} }
public enum MediaType {
IMAGE, GIF, AUDIO, VIDEO;
public @NonNull Slide createSlide(@NonNull Context context,
@NonNull Uri uri,
long dataSize)
throws IOException
{
switch (this) {
case IMAGE: return new ImageSlide(context, uri, dataSize);
case GIF: return new GifSlide(context, uri, dataSize);
case AUDIO: return new AudioSlide(context, uri, dataSize);
case VIDEO: return new VideoSlide(context, uri, dataSize);
default: throw new AssertionError("unrecognized enum");
}
}
}
} }

View File

@ -18,27 +18,25 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore.Audio;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException; import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.PduPart;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri) throws IOException, MediaTooLargeException { public AudioSlide(Context context, Uri uri, long dataSize) throws IOException {
super(context, constructPartFromUri(context, uri)); super(context, constructPartFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize));
} }
public AudioSlide(Context context, MasterSecret masterSecret, PduPart part) { public AudioSlide(Context context, PduPart part) {
super(context, masterSecret, part); super(context, part);
} }
@Override @Override
@ -55,30 +53,4 @@ public class AudioSlide extends Slide {
public @DrawableRes int getPlaceholderRes(Theme theme) { public @DrawableRes int getPlaceholderRes(Theme theme) {
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio); return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio);
} }
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {
PduPart part = new PduPart();
assertMediaSize(context, uri, MmsMediaConstraints.MAX_MESSAGE_SIZE);
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[]{Audio.Media.MIME_TYPE}, null, null, null);
if (cursor != null && cursor.moveToFirst())
part.setContentType(cursor.getString(0).getBytes());
else
throw new IOException("Unable to query content type.");
} finally {
if (cursor != null)
cursor.close();
}
part.setDataUri(uri);
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Audio" + System.currentTimeMillis()).getBytes());
return part;
}
} }

View File

@ -2,33 +2,18 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore.Audio.Media;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.IOException; import java.io.IOException;
import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.PduPart;
public class GifSlide extends ImageSlide { public class GifSlide extends ImageSlide {
public GifSlide(Context context, MasterSecret masterSecret, PduPart part) { public GifSlide(Context context, PduPart part) {
super(context, masterSecret, part); super(context, part);
} }
public GifSlide(Context context, MasterSecret masterSecret, Uri uri) public GifSlide(Context context, Uri uri, long dataSize) throws IOException {
throws IOException, BitmapDecodingException, MediaTooLargeException super(context, uri, dataSize);
{
super(context, masterSecret, uri);
assertMediaSize();
}
private void assertMediaSize() throws MediaTooLargeException, IOException {
// TODO move assertion outside of slides and take available transport options into account
assertMediaSize(context, getPart().getDataUri(), MediaConstraints.PUSH_CONSTRAINTS.getGifMaxSize());
if (!MediaConstraints.PUSH_CONSTRAINTS.isSatisfied(context, masterSecret, part)) {
throw new MediaTooLargeException("Media exceeds maximum message size.");
}
} }
@Override public Uri getThumbnailUri() { @Override public Uri getThumbnailUri() {

View File

@ -22,9 +22,6 @@ import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException; import java.io.IOException;
@ -34,12 +31,12 @@ import ws.com.google.android.mms.pdu.PduPart;
public class ImageSlide extends Slide { public class ImageSlide extends Slide {
private static final String TAG = ImageSlide.class.getSimpleName(); private static final String TAG = ImageSlide.class.getSimpleName();
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) { public ImageSlide(Context context, PduPart part) {
super(context, masterSecret, part); super(context, part);
} }
public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException { public ImageSlide(Context context, Uri uri, long size) throws IOException {
super(context, masterSecret, constructPartFromUri(context, uri)); super(context, constructPartFromUri(context, uri, ContentType.IMAGE_JPEG, size));
} }
@Override @Override
@ -62,20 +59,4 @@ public class ImageSlide extends Slide {
public boolean hasImage() { public boolean hasImage() {
return true; return true;
} }
private static PduPart constructPartFromUri(Context context, Uri uri)
throws IOException, BitmapDecodingException
{
PduPart part = new PduPart();
final String mimeType = MediaUtil.getMimeType(context, uri);
part.setDataUri(uri);
part.setContentType((mimeType != null ? mimeType : ContentType.IMAGE_JPEG).getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Image" + System.currentTimeMillis()).getBytes());
return part;
}
} }

View File

@ -27,16 +27,16 @@ public class PushMediaConstraints extends MediaConstraints {
@Override @Override
public int getGifMaxSize() { public int getGifMaxSize() {
return 1 * MB; return 5 * MB;
} }
@Override @Override
public int getVideoMaxSize() { public int getVideoMaxSize() {
return MmsMediaConstraints.MAX_MESSAGE_SIZE; return 100 * MB;
} }
@Override @Override
public int getAudioMaxSize() { public int getAudioMaxSize() {
return MmsMediaConstraints.MAX_MESSAGE_SIZE; return 100 * MB;
} }
} }

View File

@ -21,8 +21,10 @@ import android.content.res.Resources.Theme;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.IOException; import java.io.IOException;
@ -32,20 +34,14 @@ import ws.com.google.android.mms.pdu.PduPart;
public abstract class Slide { public abstract class Slide {
protected final PduPart part; protected final PduPart part;
protected final Context context; protected final Context context;
protected MasterSecret masterSecret;
public Slide(Context context, @NonNull PduPart part) { public Slide(Context context, @NonNull PduPart part) {
this.part = part; this.part = part;
this.context = context; this.context = context;
} }
public Slide(Context context, @NonNull MasterSecret masterSecret, @NonNull PduPart part) {
this(context, part);
this.masterSecret = masterSecret;
}
public String getContentType() { public String getContentType() {
return new String(part.getContentType()); return new String(part.getContentType());
} }
@ -90,18 +86,24 @@ public abstract class Slide {
return !getPart().getPartId().isValid(); return !getPart().getPartId().isValid();
} }
protected static void assertMediaSize(Context context, Uri uri, long max)
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) { protected static PduPart constructPartFromUri(@NonNull Context context,
size += read; @NonNull Uri uri,
if (size > max) throw new MediaTooLargeException("Media exceeds maximum message size."); @NonNull String defaultMime,
} long dataSize)
throws IOException
{
final PduPart part = new PduPart();
final String mimeType = MediaUtil.getMimeType(context, uri);
final String derivedMimeType = mimeType != null ? mimeType : defaultMime;
part.setDataSize(dataSize);
part.setDataUri(uri);
part.setContentType(derivedMimeType.getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName((MediaUtil.getDiscreteMimeType(derivedMimeType) + System.currentTimeMillis()).getBytes());
return part;
} }
@Override @Override
@ -125,7 +127,4 @@ public abstract class Slide {
return Util.hashCode(getContentType(), hasAudio(), hasImage(), return Util.hashCode(getContentType(), hasAudio(), hasImage(),
hasVideo(), isDraft(), getUri(), getThumbnailUri(), getTransferProgress()); hasVideo(), isDraft(), getUri(), getThumbnailUri(), getTransferProgress());
} }
} }

View File

@ -47,10 +47,10 @@ public class SlideDeck {
this.slides.addAll(copy.getSlides()); this.slides.addAll(copy.getSlides());
} }
public SlideDeck(Context context, MasterSecret masterSecret, PduBody body) { public SlideDeck(Context context, PduBody body) {
for (int i=0;i<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType()); String contentType = Util.toIsoString(body.getPart(i).getContentType());
Slide slide = MediaUtil.getSlideForPart(context, masterSecret, body.getPart(i), contentType); Slide slide = MediaUtil.getSlideForPart(context, body.getPart(i), contentType);
if (slide != null) slides.add(slide); if (slide != null) slides.add(slide);
} }
} }

View File

@ -16,31 +16,27 @@
*/ */
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.util.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException; import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.PduPart;
public class VideoSlide extends Slide { public class VideoSlide extends Slide {
public VideoSlide(Context context, Uri uri) throws IOException, MediaTooLargeException { public VideoSlide(Context context, Uri uri, long dataSize) throws IOException {
super(context, constructPartFromUri(context, uri)); super(context, constructPartFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize));
} }
public VideoSlide(Context context, MasterSecret masterSecret, PduPart part) { public VideoSlide(Context context, PduPart part) {
super(context, masterSecret, part); super(context, part);
} }
@Override @Override
@ -57,30 +53,4 @@ public class VideoSlide extends Slide {
public boolean hasVideo() { public boolean hasVideo() {
return true; return true;
} }
private static PduPart constructPartFromUri(Context context, Uri uri)
throws IOException, MediaTooLargeException
{
PduPart part = new PduPart();
ContentResolver resolver = context.getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(uri, new String[] {MediaStore.Video.Media.MIME_TYPE}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
Log.w("VideoSlide", "Setting mime type: " + cursor.getString(0));
part.setContentType(cursor.getString(0).getBytes());
}
} finally {
if (cursor != null)
cursor.close();
}
assertMediaSize(context, uri, MmsMediaConstraints.MAX_MESSAGE_SIZE);
part.setDataUri(uri);
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Video" + System.currentTimeMillis()).getBytes());
return part;
}
} }

View File

@ -66,16 +66,16 @@ public class MediaUtil {
return BitmapUtil.createScaledBitmap(context, new DecryptableUri(masterSecret, uri), maxSize, maxSize); return BitmapUtil.createScaledBitmap(context, new DecryptableUri(masterSecret, uri), maxSize, maxSize);
} }
public static Slide getSlideForPart(Context context, MasterSecret masterSecret, PduPart part, String contentType) { public static Slide getSlideForPart(Context context, PduPart part, String contentType) {
Slide slide = null; Slide slide = null;
if (isGif(contentType)) { if (isGif(contentType)) {
slide = new GifSlide(context, masterSecret, part); slide = new GifSlide(context, part);
} else if (ContentType.isImageType(contentType)) { } else if (ContentType.isImageType(contentType)) {
slide = new ImageSlide(context, masterSecret, part); slide = new ImageSlide(context, part);
} else if (ContentType.isVideoType(contentType)) { } else if (ContentType.isVideoType(contentType)) {
slide = new VideoSlide(context, masterSecret, part); slide = new VideoSlide(context, part);
} else if (ContentType.isAudioType(contentType)) { } else if (ContentType.isAudioType(contentType)) {
slide = new AudioSlide(context, masterSecret, part); slide = new AudioSlide(context, part);
} }
return slide; return slide;
@ -90,6 +90,22 @@ public class MediaUtil {
return type; return type;
} }
public static long getMediaSize(Context context, MasterSecret masterSecret, Uri uri) throws IOException {
InputStream in = PartAuthority.getPartStream(context, masterSecret, uri);
if (in == null) throw new IOException("Couldn't obtain input stream.");
long size = 0;
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
size += read;
}
in.close();
return size;
}
public static boolean isGif(String contentType) { public static boolean isGif(String contentType) {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif"); return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif");
} }
@ -111,7 +127,11 @@ public class MediaUtil {
} }
public static @Nullable String getDiscreteMimeType(@NonNull PduPart part) { public static @Nullable String getDiscreteMimeType(@NonNull PduPart part) {
final String[] sections = (Util.toIsoString(part.getContentType()).split("/", 2)); return getDiscreteMimeType(Util.toIsoString(part.getContentType()));
}
public static @Nullable String getDiscreteMimeType(@NonNull String mimeType) {
final String[] sections = mimeType.split("/", 2);
return sections.length > 1 ? sections[0] : null; return sections.length > 1 ? sections[0] : null;
} }