diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7967d06cb8..31b3b9e7bd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -454,6 +454,7 @@
Add a caption...
+ An item was removed because it exceeded the size limit
All media
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 336d44332b..95de9b70d2 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -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);
}
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index 58ebf6dbf2..f35808bba1 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -537,6 +537,7 @@ public class ConversationFragment extends Fragment
System.currentTimeMillis(),
attachment.getWidth(),
attachment.getHeight(),
+ attachment.getSize(),
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
}
diff --git a/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
index b49d0334c5..987d3cab22 100644
--- a/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
+++ b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java
@@ -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()));
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/Media.java b/src/org/thoughtcrime/securesms/mediasend/Media.java
index e080baca4d..3563d5ffb1 100644
--- a/src/org/thoughtcrime/securesms/mediasend/Media.java
+++ b/src/org/thoughtcrime/securesms/mediasend/Media.java
@@ -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 bucketId;
private Optional caption;
- public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, Optional bucketId, Optional caption) {
+ public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional bucketId, Optional 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 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());
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
index 5af64e20e2..e76bc62a03 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -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 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);
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
index 80afe3db17..7e8e1662fa 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
@@ -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, @NonNull Callback> 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 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 getPopulatedMedia(@NonNull Context context, @NonNull List 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 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 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 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;
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 9cc6e144bb..3441cccf5b 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -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 = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
if (!Util.isEmpty(media)) {
@@ -211,7 +215,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
private void navigateToMediaSend(List media, String body, TransportOption transport) {
- viewModel.setInitialSelectedMedia(media);
+ viewModel.setInitialSelectedMedia(this, media);
MediaSendFragment sendFragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
index 097b32fa82..34b1718e08 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
@@ -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");
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index 2dabfb460a..7bf95e6337 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -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 position;
private final MutableLiveData> bucketId;
private final MutableLiveData> folders;
+ private final SingleLiveEvent error;
private final Map 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 newMedia) {
- List filteredMedia = getFilteredMedia(newMedia);
- boolean allBucketsPopulated = Stream.of(filteredMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
+ void setMediaConstraints(@NonNull MediaConstraints mediaConstraints) {
+ this.mediaConstraints = mediaConstraints;
+ }
+
+ void setInitialSelectedMedia(@NonNull Context context, @NonNull List newMedia) {
+ repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
+ List filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
- selectedMedia.setValue(filteredMedia);
- bucketId.setValue(allBucketsPopulated ? computeBucketId(filteredMedia) : Optional.absent());
+ 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 List newMedia) {
- List filteredMedia = getFilteredMedia(newMedia);
+ void onSelectedMediaChanged(@NonNull Context context, @NonNull List newMedia) {
+ List 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 getError() {
+ return error;
+ }
+
private Optional computeBucketId(@NonNull List media) {
if (media.isEmpty() || !media.get(0).getBucketId().isPresent()) return Optional.absent();
@@ -124,11 +149,20 @@ class MediaSendViewModel extends ViewModel {
return Optional.of(candidate);
}
- private @NonNull List getFilteredMedia(@NonNull List media) {
+ private @NonNull List getFilteredMedia(@NonNull Context context, @NonNull List 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 {