Properly check attachment size during media send.

Prevent users from trying to send videos that exceed the size limit.

Also, this commit properly populates height/width on media shared into
the app.

Fixes #8573
This commit is contained in:
Greyson Parrelli 2019-02-11 15:05:37 -08:00
parent a3768c7d74
commit 6896f8ea15
10 changed files with 169 additions and 25 deletions

View file

@ -454,6 +454,7 @@
<!-- MediaSendActivity -->
<string name="MediaSendActivity_add_a_caption">Add a caption...</string>
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">An item was removed because it exceeded the size limit</string>
<!-- MediaRepository -->
<string name="MediaRepository_all_media">All media</string>

View file

@ -1723,7 +1723,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, Optional.absent(), Optional.absent());
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
@ -2368,7 +2368,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
linkPreviewViewModel.onUserCancel();
Media media = new Media(uri, mimeType, dateTaken, width, height, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
// TODO: Carry over size?
Media media = new Media(uri, mimeType, dateTaken, width, height, 0, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
}
}

View file

@ -537,6 +537,7 @@ public class ConversationFragment extends Fragment
System.currentTimeMillis(),
attachment.getWidth(),
attachment.getHeight(),
attachment.getSize(),
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
}

View file

@ -105,6 +105,7 @@ public class MediaPreviewViewModel extends ViewModel {
mediaRecord.getDate(),
mediaRecord.getAttachment().getWidth(),
mediaRecord.getAttachment().getHeight(),
mediaRecord.getAttachment().getSize(),
Optional.absent(),
Optional.fromNullable(mediaRecord.getAttachment().getCaption()));
}

View file

@ -19,16 +19,18 @@ public class Media implements Parcelable {
private final long date;
private final int width;
private final int height;
private final long size;
private Optional<String> bucketId;
private Optional<String> caption;
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, Optional<String> bucketId, Optional<String> caption) {
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional<String> bucketId, Optional<String> caption) {
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.bucketId = bucketId;
this.caption = caption;
}
@ -39,6 +41,7 @@ public class Media implements Parcelable {
date = in.readLong();
width = in.readInt();
height = in.readInt();
size = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
}
@ -63,6 +66,10 @@ public class Media implements Parcelable {
return height;
}
public long getSize() {
return size;
}
public Optional<String> getBucketId() {
return bucketId;
}
@ -87,6 +94,7 @@ public class Media implements Parcelable {
dest.writeLong(date);
dest.writeInt(width);
dest.writeInt(height);
dest.writeLong(size);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
}

View file

@ -149,7 +149,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onMediaChosen(@NonNull Media media) {
controller.onMediaSelected(bucketId, Collections.singleton(media));
viewModel.onSelectedMediaChanged(Collections.singletonList(media));
viewModel.onSelectedMediaChanged(requireContext(), Collections.singletonList(media));
}
@Override
@ -165,7 +165,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
actionMode.setTitle(String.valueOf(selected.size()));
}
viewModel.onSelectedMediaChanged(selected);
viewModel.onSelectedMediaChanged(requireContext(), selected);
}
@Override
@ -221,7 +221,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
if (menuItem.getItemId() == R.id.mediapicker_menu_confirm) {
List<Media> selected = new ArrayList<>(adapter.getSelected());
actionMode.finish();
viewModel.onSelectedMediaChanged(selected);
viewModel.onSelectedMediaChanged(requireContext(), selected);
controller.onMediaSelected(bucketId, selected);
return true;
}
@ -232,7 +232,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
adapter.setSelected(Collections.emptySet());
viewModel.onSelectedMediaChanged(Collections.emptyList());
viewModel.onSelectedMediaChanged(requireContext(), Collections.emptyList());
if (Build.VERSION.SDK_INT >= 21) {
requireActivity().getWindow().setStatusBarColor(statusBarColor);

View file

@ -7,20 +7,24 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Pair;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -47,6 +51,19 @@ class MediaRepository {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
}
/**
* Given an existing list of {@link Media}, this will ensure that the media is populate with as
* much data as we have, like width/height.
*/
void getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull Callback<List<Media>> callback) {
if (Stream.of(media).allMatch(this::isPopulated)) {
callback.onComplete(media);
return;
}
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
}
@WorkerThread
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
FolderResult imageFolders = getFolders(context, Images.Media.EXTERNAL_CONTENT_URI);
@ -151,11 +168,11 @@ class MediaRepository {
String[] projection;
if (hasOrienation) {
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION};
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.SIZE};
} else {
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN };
projection = Build.VERSION.SDK_INT >= 16 ? new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}
: new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.SIZE};
}
if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) {
@ -171,19 +188,36 @@ class MediaRepository {
int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
int width = 0;
int height = 0;
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
if (Build.VERSION.SDK_INT >= 16) {
width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
}
media.add(new Media(uri, mimetype, dateTaken, width, height, Optional.of(bucketId), Optional.absent()));
media.add(new Media(uri, mimetype, dateTaken, width, height, size, Optional.of(bucketId), Optional.absent()));
}
}
return media;
}
@WorkerThread
private List<Media> getPopulatedMedia(@NonNull Context context, @NonNull List<Media> media) {
return Stream.of(media).map(m -> {
try {
if (isPopulated(m)) {
return m;
} else if (PartAuthority.isLocalUri(m.getUri())) {
return getLocallyPopulatedMedia(context, m);
} else {
return getContentResolverPopulatedMedia(context, m);
}
} catch (IOException e) {
return m;
}
}).toList();
}
@TargetApi(16)
@SuppressWarnings("SuspiciousNameCombination")
@ -199,6 +233,59 @@ class MediaRepository {
else return Images.Media.WIDTH;
}
private boolean isPopulated(@NonNull Media media) {
return media.getWidth() > 0 && media.getHeight() > 0 && media.getSize() > 0;
}
private Media getLocallyPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
int width = media.getWidth();
int height = media.getHeight();
long size = media.getSize();
if (size <= 0) {
Optional<Long> optionalSize = Optional.fromNullable(PartAuthority.getAttachmentSize(context, media.getUri()));
size = optionalSize.isPresent() ? optionalSize.get() : 0;
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, media.getUri());
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, media.getMimeType(), media.getUri());
width = dimens.first;
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
}
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
int width = media.getWidth();
int height = media.getHeight();
long size = media.getSize();
if (size <= 0) {
try (Cursor cursor = context.getContentResolver().query(media.getUri(), null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
}
}
}
if (size <= 0) {
size = MediaUtil.getMediaSize(context, media.getUri());
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, media.getMimeType(), media.getUri());
width = dimens.first;
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
}
private static class FolderResult {
private final String cameraBucketId;
private final Uri thumbnail;

View file

@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage;
@ -105,6 +106,9 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
body = getIntent().getStringExtra(KEY_BODY);
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
viewModel.setMediaConstraints(transport.isSms() ? MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1))
: MediaConstraints.getPushMediaConstraints());
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
if (!Util.isEmpty(media)) {
@ -211,7 +215,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
private void navigateToMediaSend(List<Media> media, String body, TransportOption transport) {
viewModel.setInitialSelectedMedia(media);
viewModel.setInitialSelectedMedia(this, media);
MediaSendFragment sendFragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()

View file

@ -27,6 +27,7 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
@ -334,6 +335,12 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId.get()));
}
});
viewModel.getError().observe(this, error -> {
if (error == MediaSendViewModel.Error.ITEM_TOO_LARGE) {
Toast.makeText(requireContext(), R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
}
});
}
private EmojiEditText getActiveInputField() {
@ -428,7 +435,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
Uri uri = PersistentBlobProvider.getInstance(context).create(context, baos.toByteArray(), MediaUtil.IMAGE_JPEG, null);
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), media.getBucketId(), media.getCaption());
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption());
updatedMedia.add(updated);
renderTimer.split("item");

View file

@ -11,7 +11,9 @@ import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -31,8 +33,11 @@ class MediaSendViewModel extends ViewModel {
private final MutableLiveData<Integer> position;
private final MutableLiveData<Optional<String>> bucketId;
private final MutableLiveData<List<MediaFolder>> folders;
private final SingleLiveEvent<Error> error;
private final Map<Uri, Object> savedDrawState;
private MediaConstraints mediaConstraints;
private MediaSendViewModel(@NonNull MediaRepository repository) {
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
@ -40,21 +45,37 @@ class MediaSendViewModel extends ViewModel {
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
position.setValue(-1);
}
void setInitialSelectedMedia(@NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(newMedia);
boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
selectedMedia.setValue(filteredMedia);
bucketId.setValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
void setMediaConstraints(@NonNull MediaConstraints mediaConstraints) {
this.mediaConstraints = mediaConstraints;
}
void onSelectedMediaChanged(@NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(newMedia);
void setInitialSelectedMedia(@NonNull Context context, @NonNull List<Media> newMedia) {
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.postValue(Error.ITEM_TOO_LARGE);
}
boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
selectedMedia.postValue(filteredMedia);
bucketId.postValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
});
}
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
List<Media> filteredMedia = getFilteredMedia(context, newMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
}
selectedMedia.setValue(filteredMedia);
position.setValue(filteredMedia.isEmpty() ? -1 : 0);
@ -111,6 +132,10 @@ class MediaSendViewModel extends ViewModel {
return bucketId;
}
LiveData<Error> getError() {
return error;
}
private Optional<String> computeBucketId(@NonNull List<Media> media) {
if (media.isEmpty() || !media.get(0).getBucketId().isPresent()) return Optional.absent();
@ -124,13 +149,22 @@ class MediaSendViewModel extends ViewModel {
return Optional.of(candidate);
}
private @NonNull List<Media> getFilteredMedia(@NonNull List<Media> media) {
private @NonNull List<Media> getFilteredMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull MediaConstraints mediaConstraints) {
return Stream.of(media).filter(m -> MediaUtil.isGif(m.getMimeType()) ||
MediaUtil.isImageType(m.getMimeType()) ||
MediaUtil.isVideoType(m.getMimeType())).toList();
MediaUtil.isVideoType(m.getMimeType()))
.filter(m -> {
return (MediaUtil.isImageType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType())) ||
(MediaUtil.isGif(m.getMimeType()) && m.getSize() < mediaConstraints.getGifMaxSize(context)) ||
(MediaUtil.isVideoType(m.getMimeType()) && m.getSize() < mediaConstraints.getVideoMaxSize(context));
}).toList();
}
enum Error {
ITEM_TOO_LARGE
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final MediaRepository repository;