diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java index ea61a072b1..7b605f058c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java @@ -7,6 +7,9 @@ import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.CenterInside; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; @@ -53,7 +56,14 @@ public class BorderlessImageView extends FrameLayout { public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { boolean showControls = slide.asAttachment().getDataUri() == null; - image.setImageResource(glideRequests, slide, showControls, false); + if (slide.hasSticker()) { + image.setFit(new CenterInside()); + image.setImageResource(glideRequests, slide, showControls, false); + } else { + image.setFit(new CenterCrop()); + image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().getWidth(), slide.asAttachment().getHeight()); + } + missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 4ab1b7db5a..4227f3c7bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -398,6 +398,10 @@ public class ThumbnailView extends FrameLayout { getTransferControls().showProgressSpinner(); } + public void setFit(@NonNull BitmapTransformation fit) { + this.fit = fit; + } + protected void setRadius(int radius) { this.radius = radius; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 541cba215a..208ec9161b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -41,7 +41,6 @@ import android.provider.Browser; import android.provider.ContactsContract; import android.provider.Telephony; import android.text.Editable; -import android.text.TextUtils; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; @@ -63,6 +62,7 @@ import android.widget.Toast; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; @@ -73,6 +73,7 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.lifecycle.ViewModelProviders; import com.annimon.stream.Stream; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; @@ -246,7 +247,10 @@ import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -1997,7 +2001,12 @@ public class ConversationActivity extends PassphraseRequiredActivity 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, 0, 0, borderless, Optional.absent(), Optional.absent(), Optional.absent()); + String mimeType = MediaUtil.getMimeType(this, uri); + if (mimeType == null) { + mimeType = mediaType.toFallbackMimeType(); + } + + Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, Optional.absent(), Optional.absent(), Optional.absent()); startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER); return new SettableFuture<>(false); } else { @@ -2363,7 +2372,7 @@ public class ConversationActivity extends PassphraseRequiredActivity } private ListenableFuture sendMediaMessage(final boolean forceSms, - String body, + @NonNull String body, SlideDeck slideDeck, QuoteModel quote, List contacts, @@ -2651,10 +2660,10 @@ public class ConversationActivity extends PassphraseRequiredActivity @Override public void onMediaSelected(@NonNull Uri uri, String contentType) { - if (!TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif")) { - setMedia(uri, MediaType.GIF); - } else if (MediaUtil.isImageType(contentType)) { - setMedia(uri, MediaType.IMAGE); + if (MediaUtil.isGif(contentType) || MediaUtil.isImageType(contentType)) { + SimpleTask.run(getLifecycle(), + () -> getKeyboardImageDetails(uri), + details -> sendKeyboardImage(uri, contentType, details)); } else if (MediaUtil.isVideoType(contentType)) { setMedia(uri, MediaType.VIDEO); } else if (MediaUtil.isAudioType(contentType)) { @@ -3066,6 +3075,55 @@ public class ConversationActivity extends PassphraseRequiredActivity } } + @WorkerThread + private @Nullable KeyboardImageDetails getKeyboardImageDetails(@NonNull Uri uri) { + try { + Bitmap bitmap = glideRequests.asBitmap() + .load(uri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .submit() + .get(1000, TimeUnit.MILLISECONDS); + int topLeft = bitmap.getPixel(0, 0); + return new KeyboardImageDetails(bitmap.getWidth(), bitmap.getHeight(), Color.alpha(topLeft) < 255); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + return null; + } + } + + private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) { + if (details == null || !details.hasTransparency) { + setMedia(uri, Objects.requireNonNull(MediaType.from(contentType))); + return; + } + + long expiresIn = recipient.get().getExpireMessages() * 1000L; + int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1); + boolean initiating = threadId == -1; + QuoteModel quote = inputPanel.getQuote().orNull(); + SlideDeck slideDeck = new SlideDeck(); + + if (MediaUtil.isGif(contentType)) { + slideDeck.addSlide(new GifSlide(this, uri, 0, details.width, details.height, details.hasTransparency, null)); + } else if (MediaUtil.isImageType(contentType)) { + slideDeck.addSlide(new ImageSlide(this, uri, contentType, 0, details.width, details.height, details.hasTransparency, null, null)); + } else { + throw new AssertionError("Only images are supported!"); + } + + sendMediaMessage(isSmsForced(), + "", + slideDeck, + quote, + Collections.emptyList(), + Collections.emptyList(), + expiresIn, + false, + subscriptionId, + initiating, + true); + } + private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener { @Override public void onDismissed(final List unverifiedIdentities) { @@ -3155,4 +3213,16 @@ public class ConversationActivity extends PassphraseRequiredActivity messageRequestBottomView.setRecipient(recipient); } + + private static class KeyboardImageDetails { + private final int width; + private final int height; + private final boolean hasTransparency; + + private KeyboardImageDetails(int width, int height, boolean hasTransparency) { + this.width = width; + this.height = height; + this.hasTransparency = hasTransparency; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 90cc1a6b47..40ac31e1c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -700,6 +700,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati } else { //noinspection ConstantConditions stickerStub.get().setSlide(glideRequests, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide()); + stickerStub.get().setThumbnailClickListener((v, slide) -> performClick()); } stickerStub.get().setDownloadClickListener(downloadClickListener); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index c5a180d0f9..1d5b5dae3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -522,7 +522,19 @@ public class AttachmentManager { } public enum MediaType { - IMAGE, GIF, AUDIO, VIDEO, DOCUMENT, VCARD; + IMAGE(MediaUtil.IMAGE_JPEG), + GIF(MediaUtil.IMAGE_GIF), + AUDIO(MediaUtil.AUDIO_AAC), + VIDEO(MediaUtil.VIDEO_MP4), + DOCUMENT(MediaUtil.UNKNOWN), + VCARD(MediaUtil.VCARD); + + private final String fallbackMimeType; + + MediaType(String fallbackMimeType) { + this.fallbackMimeType = fallbackMimeType; + } + public @NonNull Slide createSlide(@NonNull Context context, @NonNull Uri uri, @@ -559,5 +571,8 @@ public class AttachmentManager { return DOCUMENT; } + public String toFallbackMimeType() { + return fallbackMimeType; + } } } diff --git a/app/src/main/res/layout/conversation_item_received_multimedia.xml b/app/src/main/res/layout/conversation_item_received_multimedia.xml index 8cf555cb7d..61ed8c838d 100644 --- a/app/src/main/res/layout/conversation_item_received_multimedia.xml +++ b/app/src/main/res/layout/conversation_item_received_multimedia.xml @@ -135,8 +135,8 @@ + app:thumbnail_fit="fit_center" + app:minWidth="@dimen/media_bubble_min_width" + app:maxWidth="@dimen/media_bubble_max_width" + app:minHeight="@dimen/media_bubble_min_height" + app:maxHeight="@dimen/media_bubble_max_height" />