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_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_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
<!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string>

View File

@ -35,6 +35,7 @@ import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.WindowCompat;
import android.text.Editable;
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.ThreadDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.MediaConstraints;
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.DynamicTheme;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@ -288,13 +291,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
switch (reqCode) {
case PICK_IMAGE:
addAttachmentImage(masterSecret, data.getData());
setMedia(data.getData(),
MediaUtil.isGif(MediaUtil.getMimeType(this, data.getData())) ? MediaType.GIF
: MediaType.IMAGE,
false);
break;
case PICK_VIDEO:
addAttachmentVideo(data.getData());
setMedia(data.getData(), MediaType.VIDEO, false);
break;
case PICK_AUDIO:
addAttachmentAudio(data.getData());
setMedia(data.getData(), MediaType.AUDIO, false);
break;
case PICK_CONTACT_INFO:
addAttachmentContactInfo(data.getData());
@ -308,7 +314,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
break;
case TAKE_PHOTO:
if (attachmentManager.getCaptureUri() != null) {
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
setMedia(attachmentManager.getCaptureUri(), MediaType.IMAGE, true);
}
break;
}
@ -671,9 +677,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);
if (draftText != null) composeText.setText(draftText);
if (draftImage != null) addAttachmentImage(masterSecret, draftImage);
if (draftAudio != null) addAttachmentAudio(draftAudio);
if (draftVideo != null) addAttachmentVideo(draftVideo);
if (draftImage != null) setMedia(draftImage, MediaType.IMAGE, false);
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) {
initializeDraftFromDatabase();
@ -707,11 +714,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draft.getType().equals(Draft.TEXT)) {
composeText.setText(draft.getValue());
} 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)) {
addAttachmentAudio(Uri.parse(draft.getValue()));
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO, false);
} 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) {
try {
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 setMedia(Uri uri, MediaType mediaType, boolean isCapture) {
attachmentManager.setMedia(masterSecret, uri, mediaType, getCurrentMediaConstraints(), isCapture);
}
private void addAttachmentContactInfo(Uri contactUri) {
@ -1132,6 +1092,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return rawText;
}
private MediaConstraints getCurrentMediaConstraints() {
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
? MediaConstraints.PUSH_CONSTRAINTS
: MediaConstraints.MMS_CONSTRAINTS;
}
private void markThreadAsRead() {
new AsyncTask<Long, Void, Void>() {
@Override
@ -1198,8 +1164,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = getApplicationContext();
SlideDeck slideDeck;
if (attachmentManager.isAttachmentPresent()) slideDeck = new SlideDeck(attachmentManager.getSlideDeck());
else slideDeck = new SlideDeck();
if (attachmentManager.isAttachmentPresent()) {
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,
getMessage(), distributionType);
@ -1272,8 +1254,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onImageCapture(@NonNull final byte[] imageBytes) {
attachmentManager.setCaptureUri(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes));
addAttachmentImage(masterSecret, attachmentManager.getCaptureUri());
setMedia(CaptureProvider.getInstance(this).create(masterSecret, recipients, imageBytes), MediaType.IMAGE, true);
quickAttachmentDrawer.hide(false);
}
@ -1397,4 +1378,5 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeSecurity();
updateToggleButtonState();
}
}

View File

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

View File

@ -201,7 +201,12 @@ public class ThumbnailView extends FrameLayout {
}
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) {
@ -209,6 +214,11 @@ public class ThumbnailView extends FrameLayout {
if (hideControls) hideProgressWheel();
}
public void showProgressSpinner() {
getProgressWheel().spin();
getProgressWheel().setVisibility(VISIBLE);
}
@TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
private boolean isContextValid() {
return !(getContext() instanceof Activity) ||

View File

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

View File

@ -21,9 +21,11 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
@ -36,7 +38,6 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.CaptureProvider;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
@ -54,7 +55,7 @@ public class AttachmentManager {
public AttachmentManager(Activity view, AttachmentListener listener) {
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.context = view;
this.attachmentListener = listener;
@ -70,6 +71,7 @@ public class AttachmentManager {
@Override public void onAnimationRepeat(Animation animation) {}
@Override public void onAnimationEnd(Animation animation) {
slideDeck.clear();
thumbnail.clear();
attachmentView.setVisibility(View.GONE);
attachmentListener.onAttachmentChanged();
}
@ -83,34 +85,55 @@ public class AttachmentManager {
captureUri = null;
}
public void setImage(MasterSecret masterSecret, Uri image)
throws IOException, BitmapDecodingException, MediaTooLargeException
public void setMedia(@NonNull final MasterSecret masterSecret,
@NonNull final Uri uri,
@NonNull final MediaType mediaType,
@NonNull final MediaConstraints constraints,
final boolean isCapture)
{
if (MediaUtil.isGif(MediaUtil.getMimeType(context, image))) {
setMedia(new GifSlide(context, masterSecret, image), masterSecret);
} else {
setMedia(new ImageSlide(context, masterSecret, image), masterSecret);
}
}
new AsyncTask<Void, Void, Slide>() {
@Override protected void onPreExecute() {
slideDeck.clear();
thumbnail.clear();
thumbnail.showProgressSpinner();
attachmentView.setVisibility(View.VISIBLE);
public void setVideo(Uri video) throws IOException, MediaTooLargeException {
setMedia(new VideoSlide(context, video));
}
if (isCapture) captureUri = uri;
if (!uri.equals(captureUri)) cleanup();
}
public void setAudio(Uri audio) throws IOException, MediaTooLargeException {
setMedia(new AudioSlide(context, audio));
}
@Override protected @Nullable Slide doInBackground(Void... params) {
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) {
setMedia(slide, null);
}
public void setMedia(final Slide slide, @Nullable MasterSecret masterSecret) {
slideDeck.clear();
slideDeck.addSlide(slide);
attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(slide, masterSecret);
attachmentListener.onAttachmentChanged();
@Override protected void onPostExecute(@Nullable final Slide slide) {
if (slide == null) {
attachmentView.setVisibility(View.GONE);
Toast.makeText(context,
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_SHORT).show();
} else if (!areConstraintsSatisfied(context, masterSecret, slide, constraints)) {
attachmentView.setVisibility(View.GONE);
Toast.makeText(context,
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() {
@ -118,7 +141,7 @@ public class AttachmentManager {
}
public SlideDeck getSlideDeck() {
public @NonNull SlideDeck getSlideDeck() {
return slideDeck;
}
@ -143,10 +166,6 @@ public class AttachmentManager {
return captureUri;
}
public void setCaptureUri(Uri captureUri) {
this.captureUri = captureUri;
}
public void capturePhoto(Activity activity, Recipients recipients, int requestCode) {
try {
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 {
@Override
public void onClick(View v) {
@ -194,4 +223,22 @@ public class AttachmentManager {
public interface AttachmentListener {
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.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore.Audio;
import android.support.annotation.DrawableRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart;
public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri) throws IOException, MediaTooLargeException {
super(context, constructPartFromUri(context, uri));
public AudioSlide(Context context, Uri uri, long dataSize) throws IOException {
super(context, constructPartFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize));
}
public AudioSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
public AudioSlide(Context context, PduPart part) {
super(context, part);
}
@Override
@ -55,30 +53,4 @@ public class AudioSlide extends Slide {
public @DrawableRes int getPlaceholderRes(Theme theme) {
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.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 ws.com.google.android.mms.pdu.PduPart;
public class GifSlide extends ImageSlide {
public GifSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
public GifSlide(Context context, PduPart part) {
super(context, part);
}
public GifSlide(Context context, MasterSecret masterSecret, Uri uri)
throws IOException, BitmapDecodingException, MediaTooLargeException
{
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.");
}
public GifSlide(Context context, Uri uri, long dataSize) throws IOException {
super(context, uri, dataSize);
}
@Override public Uri getThumbnailUri() {

View File

@ -22,9 +22,6 @@ import android.net.Uri;
import android.support.annotation.DrawableRes;
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;
@ -34,12 +31,12 @@ import ws.com.google.android.mms.pdu.PduPart;
public class ImageSlide extends Slide {
private static final String TAG = ImageSlide.class.getSimpleName();
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
public ImageSlide(Context context, PduPart part) {
super(context, part);
}
public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException {
super(context, masterSecret, constructPartFromUri(context, uri));
public ImageSlide(Context context, Uri uri, long size) throws IOException {
super(context, constructPartFromUri(context, uri, ContentType.IMAGE_JPEG, size));
}
@Override
@ -62,20 +59,4 @@ public class ImageSlide extends Slide {
public boolean hasImage() {
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
public int getGifMaxSize() {
return 1 * MB;
return 5 * MB;
}
@Override
public int getVideoMaxSize() {
return MmsMediaConstraints.MAX_MESSAGE_SIZE;
return 100 * MB;
}
@Override
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.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
@ -32,20 +34,14 @@ import ws.com.google.android.mms.pdu.PduPart;
public abstract class Slide {
protected final PduPart part;
protected final Context context;
protected MasterSecret masterSecret;
protected final PduPart part;
protected final Context context;
public Slide(Context context, @NonNull PduPart part) {
this.part = part;
this.context = context;
}
public Slide(Context context, @NonNull MasterSecret masterSecret, @NonNull PduPart part) {
this(context, part);
this.masterSecret = masterSecret;
}
public String getContentType() {
return new String(part.getContentType());
}
@ -90,18 +86,24 @@ public abstract class Slide {
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) {
size += read;
if (size > max) throw new MediaTooLargeException("Media exceeds maximum message size.");
}
protected static PduPart constructPartFromUri(@NonNull Context context,
@NonNull Uri uri,
@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
@ -125,7 +127,4 @@ public abstract class Slide {
return Util.hashCode(getContentType(), hasAudio(), hasImage(),
hasVideo(), isDraft(), getUri(), getThumbnailUri(), getTransferProgress());
}
}

View File

@ -47,10 +47,10 @@ public class SlideDeck {
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++) {
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);
}
}

View File

@ -16,31 +16,27 @@
*/
package org.thoughtcrime.securesms.mms;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.DrawableRes;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart;
public class VideoSlide extends Slide {
public VideoSlide(Context context, Uri uri) throws IOException, MediaTooLargeException {
super(context, constructPartFromUri(context, uri));
public VideoSlide(Context context, Uri uri, long dataSize) throws IOException {
super(context, constructPartFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize));
}
public VideoSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
public VideoSlide(Context context, PduPart part) {
super(context, part);
}
@Override
@ -57,30 +53,4 @@ public class VideoSlide extends Slide {
public boolean hasVideo() {
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);
}
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;
if (isGif(contentType)) {
slide = new GifSlide(context, masterSecret, part);
slide = new GifSlide(context, part);
} else if (ContentType.isImageType(contentType)) {
slide = new ImageSlide(context, masterSecret, part);
slide = new ImageSlide(context, part);
} else if (ContentType.isVideoType(contentType)) {
slide = new VideoSlide(context, masterSecret, part);
slide = new VideoSlide(context, part);
} else if (ContentType.isAudioType(contentType)) {
slide = new AudioSlide(context, masterSecret, part);
slide = new AudioSlide(context, part);
}
return slide;
@ -90,6 +90,22 @@ public class MediaUtil {
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) {
return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif");
}
@ -111,7 +127,11 @@ public class MediaUtil {
}
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;
}