diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java index 1339e69705..dd7d52d7ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java @@ -35,7 +35,9 @@ public class EmojiEditText extends AppCompatEditText { a.recycle(); if (forceCustom || !TextSecurePreferences.isSystemEmojiPreferred(getContext())) { - setFilters(appendEmojiFilter(this.getFilters())); + if (!isInEditMode()) { + setFilters(appendEmojiFilter(this.getFilters())); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index dad58459cf..5bcd296b8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -92,7 +92,7 @@ public class EmojiTextView extends AppCompatTextView { @Override public void setText(@Nullable CharSequence text, BufferType type) { EmojiProvider provider = EmojiProvider.getInstance(getContext()); - EmojiParser.CandidateList candidates = provider.getCandidates(text); + EmojiParser.CandidateList candidates = !isInEditMode() ? provider.getCandidates(text) : null; if (scaleEmojis && candidates != null && candidates.allEmojis) { int emojis = candidates.size(); 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 8226e3364f..e9adcf81db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -228,7 +228,6 @@ import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView; import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment; import org.thoughtcrime.securesms.providers.BlobProvider; -import org.thoughtcrime.securesms.ratelimit.RecaptchaProofActivity; import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment; import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment; @@ -730,7 +729,7 @@ public class ConversationActivity extends PassphraseRequiredActivity } else if (MediaUtil.isGif(mediaItem.getMimeType())) { slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull())); } else if (MediaUtil.isImageType(mediaItem.getMimeType())) { - slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null)); + slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null, mediaItem.getTransformProperties().orNull())); } else { Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 15550b1efc..5ccb911c9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormDat import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.CursorUtil; @@ -1393,33 +1394,43 @@ public class AttachmentDatabase extends Database { public static final class TransformProperties { + private static final int DEFAULT_MEDIA_QUALITY = SentMediaQuality.STANDARD.getCode(); + @JsonProperty private final boolean skipTransform; @JsonProperty private final boolean videoTrim; @JsonProperty private final long videoTrimStartTimeUs; @JsonProperty private final long videoTrimEndTimeUs; + @JsonProperty private final int sentMediaQuality; @JsonCreator public TransformProperties(@JsonProperty("skipTransform") boolean skipTransform, @JsonProperty("videoTrim") boolean videoTrim, @JsonProperty("videoTrimStartTimeUs") long videoTrimStartTimeUs, - @JsonProperty("videoTrimEndTimeUs") long videoTrimEndTimeUs) + @JsonProperty("videoTrimEndTimeUs") long videoTrimEndTimeUs, + @JsonProperty("sentMediaQuality") int sentMediaQuality) { this.skipTransform = skipTransform; this.videoTrim = videoTrim; this.videoTrimStartTimeUs = videoTrimStartTimeUs; this.videoTrimEndTimeUs = videoTrimEndTimeUs; + this.sentMediaQuality = sentMediaQuality; } public static @NonNull TransformProperties empty() { - return new TransformProperties(false, false, 0, 0); + return new TransformProperties(false, false, 0, 0, DEFAULT_MEDIA_QUALITY); } public static @NonNull TransformProperties forSkipTransform() { - return new TransformProperties(true, false, 0, 0); + return new TransformProperties(true, false, 0, 0, DEFAULT_MEDIA_QUALITY); } public static @NonNull TransformProperties forVideoTrim(long videoTrimStartTimeUs, long videoTrimEndTimeUs) { - return new TransformProperties(false, true, videoTrimStartTimeUs, videoTrimEndTimeUs); + return new TransformProperties(false, true, videoTrimStartTimeUs, videoTrimEndTimeUs, DEFAULT_MEDIA_QUALITY); + } + + public static @NonNull TransformProperties forSentMediaQuality(@NonNull Optional currentProperties, @NonNull SentMediaQuality sentMediaQuality) { + TransformProperties existing = currentProperties.or(empty()); + return new TransformProperties(existing.skipTransform, existing.videoTrim, existing.videoTrimStartTimeUs, existing.videoTrimEndTimeUs, sentMediaQuality.getCode()); } public boolean shouldSkipTransform() { @@ -1442,6 +1453,10 @@ public class AttachmentDatabase extends Database { return videoTrimEndTimeUs; } + public int getSentMediaQuality() { + return sentMediaQuality; + } + @NonNull String serialize() { return JsonUtil.toJson(this); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java index 1e11f22ed1..eda39f61f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.mms.MmsException; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.service.NotificationController; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; @@ -141,7 +142,7 @@ public final class AttachmentCompressionJob extends BaseJob { } MediaConstraints mediaConstraints = mms ? MediaConstraints.getMmsMediaConstraints(mmsSubscriptionId) - : MediaConstraints.getPushMediaConstraints(); + : MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(databaseAttachment.getTransformProperties().getSentMediaQuality())); compress(database, mediaConstraints, databaseAttachment); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CompositeMediaTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CompositeMediaTransform.java new file mode 100644 index 0000000000..4b509a8923 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CompositeMediaTransform.java @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.mediasend; + +import android.content.Context; + +import androidx.annotation.NonNull; + +/** + * Allow multiple transforms to operate on {@link Media}. Care should + * be taken on the order and implementation of combined transformers to prevent + * one undoing the work of the other. + */ +public final class CompositeMediaTransform implements MediaTransform { + + private final MediaTransform[] transforms; + + CompositeMediaTransform(MediaTransform ...transforms) { + this.transforms = transforms; + } + + @Override + public @NonNull Media transform(@NonNull Context context, @NonNull Media media) { + Media updatedMedia = media; + for (MediaTransform transform : transforms) { + updatedMedia = transform.transform(context, updatedMedia); + } + return updatedMedia; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 0fcd422d8f..2c3a0d917a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -22,6 +22,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.AppCompatImageView; import androidx.core.util.Pair; import androidx.core.util.Supplier; import androidx.fragment.app.Fragment; @@ -56,6 +57,7 @@ import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.HudState; import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.ViewOnceState; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.LiveRecipient; @@ -141,6 +143,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med private TextView countButtonText; private View continueButton; private ImageView revealButton; + private AppCompatImageView qualityButton; private EmojiEditText captionText; private EmojiToggle emojiToggle; private Stub emojiDrawer; @@ -236,6 +239,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med countButtonText = findViewById(R.id.mediasend_count_button_text); continueButton = findViewById(R.id.mediasend_continue_button); revealButton = findViewById(R.id.mediasend_reveal_toggle); + qualityButton = findViewById(R.id.mediasend_quality_toggle); captionText = findViewById(R.id.mediasend_caption); emojiToggle = findViewById(R.id.mediasend_emoji_toggle); charactersLeft = findViewById(R.id.mediasend_characters_left); @@ -355,6 +359,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled()); + qualityButton.setVisibility(Util.isLowMemory(this) ? View.GONE : View.VISIBLE); + qualityButton.setOnClickListener(v -> QualitySelectorBottomSheetDialog.show(getSupportFragmentManager())); + continueButton.setOnClickListener(v -> { continueButton.setEnabled(false); if (recipientIds == null || recipientIds.isEmpty()) { @@ -599,7 +606,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med fragment.pausePlayback(); SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 300, 0); - viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()) + viewModel.onSendClicked(buildModelsToTransform(fragment, viewModel.getSentMediaQuality().getValue()), recipients, composeText.getMentions()) .observe(this, result -> { dialog.dismiss(); if (recipients.size() > 1) { @@ -610,9 +617,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med }); } - private static Map buildModelsToTransform(@NonNull MediaSendFragment fragment) { - List mediaList = fragment.getAllMedia(); - Map savedState = fragment.getSavedState(); + private static Map buildModelsToTransform(@NonNull MediaSendFragment fragment, @Nullable SentMediaQuality sentMediaQuality) { + List mediaList = fragment.getAllMedia(); + Map savedState = fragment.getSavedState(); Map modelsToRender = new HashMap<>(); for (Media media : mediaList) { @@ -631,12 +638,20 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med modelsToRender.put(media, new VideoTrimTransform(data)); } } + + if (sentMediaQuality == SentMediaQuality.HIGH) { + MediaTransform existingTransform = modelsToRender.get(media); + if (existingTransform == null) { + modelsToRender.put(media, new SentMediaQualityTransform(sentMediaQuality)); + } else { + modelsToRender.put(media, new CompositeMediaTransform(existingTransform, new SentMediaQualityTransform(sentMediaQuality))); + } + } } return modelsToRender; } - private void onAddMediaClicked(@NonNull String bucketId) { Permissions.with(this) .request(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -730,11 +745,11 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med switch (state.getViewOnceState()) { case ENABLED: revealButton.setVisibility(View.VISIBLE); - revealButton.setImageResource(R.drawable.ic_view_once_32); + revealButton.setImageResource(R.drawable.ic_view_once_28); break; case DISABLED: revealButton.setVisibility(View.VISIBLE); - revealButton.setImageResource(R.drawable.ic_view_infinite_32); + revealButton.setImageResource(R.drawable.ic_view_infinite_28); break; case GONE: revealButton.setVisibility(View.GONE); @@ -764,6 +779,8 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med } }); + viewModel.getSentMediaQuality().observe(this, q -> qualityButton.setImageResource(q == SentMediaQuality.STANDARD ? R.drawable.ic_quality_standard_32 : R.drawable.ic_quality_high_32)); + viewModel.getSelectedMedia().observe(this, media -> { mediaRailAdapter.setMedia(media); }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index cfb1b9f45a..98403d9580 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -55,7 +55,7 @@ public class MediaSendFragment extends Fragment { fragmentPager = view.findViewById(R.id.mediasend_pager); playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container); - fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager(), viewModel.isSms() ? MediaConstraints.getMmsMediaConstraints(-1) : MediaConstraints.getPushMediaConstraints()); + fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager(), viewModel.isSms() ? MediaConstraints.getMmsMediaConstraints(-1) : MediaConstraints.getPushMediaConstraints(null)); fragmentPager.setAdapter(fragmentPagerAdapter); FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java index 96065b30c4..542aadadd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; @@ -65,6 +66,7 @@ class MediaSendViewModel extends ViewModel { private final MutableLiveData hudState; private final SingleLiveEvent error; private final SingleLiveEvent event; + private final MutableLiveData sentMediaQuality; private final Map savedDrawState; private TransportOption transport; @@ -85,7 +87,6 @@ class MediaSendViewModel extends ViewModel { private RailState railState; private ViewOnceState viewOnceState; - private @Nullable Recipient recipient; private MediaSendViewModel(@NonNull Application application, @@ -104,6 +105,7 @@ class MediaSendViewModel extends ViewModel { this.hudState = new MutableLiveData<>(); this.error = new SingleLiveEvent<>(); this.event = new SingleLiveEvent<>(); + this.sentMediaQuality = new MutableLiveData<>(SentMediaQuality.STANDARD); this.savedDrawState = new HashMap<>(); this.lastCameraCapture = Optional.absent(); this.body = ""; @@ -455,6 +457,16 @@ class MediaSendViewModel extends ViewModel { savedDrawState.putAll(state); } + public void setSentMediaQuality(@NonNull SentMediaQuality newQuality) { + if (newQuality == sentMediaQuality.getValue()) { + return; + } + + sentMediaQuality.setValue(newQuality); + preUploadEnabled = false; + uploadRepository.cancelAllUploads(); + } + @NonNull LiveData onSendClicked(Map modelsToTransform, @NonNull List recipients, @NonNull List mentions) { if (isSms && recipients.size() > 0) { throw new IllegalStateException("Provided recipients to send to, but this is SMS!"); @@ -561,6 +573,10 @@ class MediaSendViewModel extends ViewModel { return viewOnceState == ViewOnceState.ENABLED; } + @NonNull LiveData getSentMediaQuality() { + return sentMediaQuality; + } + @NonNull MediaConstraints getMediaConstraints() { return mediaConstraints; } @@ -583,10 +599,10 @@ class MediaSendViewModel extends ViewModel { } private HudState buildHudState() { - List selectedMedia = getSelectedMediaOrDefault(); - int selectionCount = selectedMedia.size(); - ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState; - boolean updatedCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent())); + List selectedMedia = getSelectedMediaOrDefault(); + int selectionCount = selectedMedia.size(); + ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState; + boolean updatedCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent())); return new HudState(hudVisible, composeVisible, updatedCaptionVisible, selectionCount, updatedButtonState, railState, viewOnceState); } @@ -704,12 +720,12 @@ class MediaSendViewModel extends ViewModel { static class HudState { - private final boolean hudVisible; - private final boolean composeVisible; - private final boolean captionVisible; - private final int selectionCount; - private final ButtonState buttonState; - private final RailState railState; + private final boolean hudVisible; + private final boolean composeVisible; + private final boolean captionVisible; + private final int selectionCount; + private final ButtonState buttonState; + private final RailState railState; private final ViewOnceState viewOnceState; HudState(boolean hudVisible, @@ -720,13 +736,13 @@ class MediaSendViewModel extends ViewModel { @NonNull RailState railState, @NonNull ViewOnceState viewOnceState) { - this.hudVisible = hudVisible; - this.composeVisible = composeVisible; - this.captionVisible = captionVisible; - this.selectionCount = selectionCount; - this.buttonState = buttonState; - this.railState = railState; - this.viewOnceState = viewOnceState; + this.hudVisible = hudVisible; + this.composeVisible = composeVisible; + this.captionVisible = captionVisible; + this.selectionCount = selectionCount; + this.buttonState = buttonState; + this.railState = railState; + this.viewOnceState = viewOnceState; } public boolean isHudVisible() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java index 808d581e03..baafe68a6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java @@ -13,6 +13,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; @@ -78,16 +79,28 @@ class MediaUploadRepository { void applyMediaUpdates(@NonNull Map oldToNew, @Nullable Recipient recipient) { executor.execute(() -> { for (Map.Entry entry : oldToNew.entrySet()) { - - boolean same = entry.getKey().equals(entry.getValue()) && (!entry.getValue().getTransformProperties().isPresent() || !entry.getValue().getTransformProperties().get().isVideoEdited()); - if (!same || !uploadResults.containsKey(entry.getValue())) { - cancelUploadInternal(entry.getKey()); - uploadMediaInternal(entry.getValue(), recipient); + Media oldMedia = entry.getKey(); + Media newMedia = entry.getValue(); + boolean same = oldMedia.equals(newMedia) && hasSameTransformProperties(oldMedia, newMedia); + if (!same || !uploadResults.containsKey(newMedia)) { + cancelUploadInternal(oldMedia); + uploadMediaInternal(newMedia, recipient); } } }); } + private boolean hasSameTransformProperties(@NonNull Media oldMedia, @NonNull Media newMedia) { + TransformProperties oldProperties = oldMedia.getTransformProperties().orNull(); + TransformProperties newProperties = newMedia.getTransformProperties().orNull(); + + if (oldProperties == null || newProperties == null) { + return oldProperties == newProperties; + } + + return !newProperties.isVideoEdited() && oldProperties.getSentMediaQuality() == newProperties.getSentMediaQuality(); + } + void cancelUpload(@NonNull Media media) { executor.execute(() -> cancelUploadInternal(media)); } @@ -195,7 +208,7 @@ class MediaUploadRepository { } else if (MediaUtil.isGif(media.getMimeType())) { return new GifSlide(context, media.getUri(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull()).asAttachment(); } else if (MediaUtil.isImageType(media.getMimeType())) { - return new ImageSlide(context, media.getUri(), media.getMimeType(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull(), null).asAttachment(); + return new ImageSlide(context, media.getUri(), media.getMimeType(), media.getSize(), media.getWidth(), media.getHeight(), media.isBorderless(), media.getCaption().orNull(), null, media.getTransformProperties().orNull()).asAttachment(); } else if (MediaUtil.isTextType(media.getMimeType())) { return new TextSlide(context, media.getUri(), null, media.getSize()).asAttachment(); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/QualitySelectorBottomSheetDialog.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/QualitySelectorBottomSheetDialog.java new file mode 100644 index 0000000000..30d09cd5f7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/QualitySelectorBottomSheetDialog.java @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.mediasend; + +import android.os.Bundle; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProviders; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.SentMediaQuality; +import org.thoughtcrime.securesms.util.BottomSheetUtil; +import org.thoughtcrime.securesms.util.views.CheckedLinearLayout; + +/** + * Dialog for selecting media quality, tightly coupled with {@link MediaSendViewModel}. + */ +public final class QualitySelectorBottomSheetDialog extends BottomSheetDialogFragment { + + private MediaSendViewModel viewModel; + private CheckedLinearLayout standard; + private CheckedLinearLayout high; + + public static void show(@NonNull FragmentManager manager) { + QualitySelectorBottomSheetDialog fragment = new QualitySelectorBottomSheetDialog(); + + fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet); + super.onCreate(savedInstanceState); + } + + @Override + public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme); + LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper); + + return themedInflater.inflate(R.layout.quality_selector_dialog, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { + standard = view.findViewById(R.id.quality_selector_dialog_standard); + high = view.findViewById(R.id.quality_selector_dialog_high); + + View.OnClickListener listener = v -> { + select(v); + view.postDelayed(this::dismissAllowingStateLoss, 250); + }; + + standard.setOnClickListener(listener); + high.setOnClickListener(listener); + } + + @Override + public void onActivityCreated(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + viewModel = ViewModelProviders.of(requireActivity()).get(MediaSendViewModel.class); + viewModel.getSentMediaQuality().observe(getViewLifecycleOwner(), this::updateQuality); + } + + private void updateQuality(@NonNull SentMediaQuality sentMediaQuality) { + select(sentMediaQuality == SentMediaQuality.STANDARD ? standard : high); + } + + private void select(@NonNull View view) { + standard.setChecked(view == standard); + high.setChecked(view == high); + viewModel.setSentMediaQuality(standard == view ? SentMediaQuality.STANDARD : SentMediaQuality.HIGH); + } + + @Override + public void show(@NonNull FragmentManager manager, @Nullable String tag) { + BottomSheetUtil.show(manager, tag, this); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/SentMediaQualityTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/SentMediaQualityTransform.java new file mode 100644 index 0000000000..5009af3df7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/SentMediaQualityTransform.java @@ -0,0 +1,40 @@ +package org.thoughtcrime.securesms.mediasend; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.mms.SentMediaQuality; +import org.whispersystems.libsignal.util.guava.Optional; + +/** + * Add a {@link SentMediaQuality} value for {@link AttachmentDatabase.TransformProperties#getSentMediaQuality()} on the + * transformed media. Safe to use in a pipeline with other transforms. + */ +public final class SentMediaQualityTransform implements MediaTransform { + + private final SentMediaQuality sentMediaQuality; + + SentMediaQualityTransform(@NonNull SentMediaQuality sentMediaQuality) { + this.sentMediaQuality = sentMediaQuality; + } + + @WorkerThread + @Override + public @NonNull Media transform(@NonNull Context context, @NonNull Media media) { + return new Media(media.getUri(), + media.getMimeType(), + media.getDate(), + media.getWidth(), + media.getHeight(), + media.getSize(), + media.getDuration(), + media.isBorderless(), + media.isVideoGif(), + media.getBucketId(), + media.getCaption(), + Optional.of(AttachmentDatabase.TransformProperties.forSentMediaQuality(media.getTransformProperties(), sentMediaQuality))); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java index f2711f588d..5f1061a7cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/VideoTrimTransform.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.whispersystems.libsignal.util.guava.Optional; public final class VideoTrimTransform implements MediaTransform { @@ -30,6 +31,6 @@ public final class VideoTrimTransform implements MediaTransform { media.isVideoGif(), media.getBucketId(), media.getCaption(), - Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs))); + Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs, SentMediaQuality.STANDARD.getCode()))); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java index c3f88b3f92..acd408ab06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -28,6 +28,8 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.blurhash.BlurHash; +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.util.MediaUtil; public class ImageSlide extends Slide { @@ -47,7 +49,11 @@ public class ImageSlide extends Slide { } public ImageSlide(Context context, Uri uri, String contentType, long size, int width, int height, boolean borderless, @Nullable String caption, @Nullable BlurHash blurHash) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false, false)); + this(context, uri, contentType, size, width, height, borderless, caption, blurHash, null); + } + + public ImageSlide(Context context, Uri uri, String contentType, long size, int width, int height, boolean borderless, @Nullable String caption, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties) { + super(context, constructAttachmentFromUri(context, uri, contentType, size, width, height, true, null, caption, null, blurHash, null, false, borderless, false, false, transformProperties)); this.borderless = borderless; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java index bd5b3dffd1..088058151e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -7,6 +7,7 @@ import android.util.Pair; import androidx.annotation.IntRange; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.attachments.Attachment; @@ -23,7 +24,11 @@ public abstract class MediaConstraints { private static final String TAG = Log.tag(MediaConstraints.class); public static MediaConstraints getPushMediaConstraints() { - return new PushMediaConstraints(); + return getPushMediaConstraints(null); + } + + public static MediaConstraints getPushMediaConstraints(@Nullable SentMediaQuality sentMediaQuality) { + return new PushMediaConstraints(sentMediaQuality); } public static MediaConstraints getMmsMediaConstraints(int subscriptionId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 592bebc56e..b1cb5d5f74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -14,13 +14,13 @@ import java.util.Arrays; public class PushMediaConstraints extends MediaConstraints { - private static final int KB = 1024; - private static final int MB = 1024 * KB; + private static final int KB = 1024; + private static final int MB = 1024 * KB; private final MediaConfig currentConfig; - public PushMediaConstraints() { - currentConfig = getCurrentConfig(ApplicationDependencies.getApplication()); + public PushMediaConstraints(@Nullable SentMediaQuality sentMediaQuality) { + currentConfig = getCurrentConfig(ApplicationDependencies.getApplication(), sentMediaQuality); } @Override @@ -80,11 +80,14 @@ public class PushMediaConstraints extends MediaConstraints { return currentConfig.qualitySetting; } - private static @NonNull MediaConfig getCurrentConfig(@NonNull Context context) { + private static @NonNull MediaConfig getCurrentConfig(@NonNull Context context, @Nullable SentMediaQuality sentMediaQuality) { if (Util.isLowMemory(context)) { return MediaConfig.LEVEL_1_LOW_MEMORY; } + if (sentMediaQuality == SentMediaQuality.HIGH) { + return MediaConfig.LEVEL_3; + } return LocaleFeatureFlags.getMediaQualityLevel().orElse(MediaConfig.getDefault(context)); } @@ -93,7 +96,7 @@ public class PushMediaConstraints extends MediaConstraints { LEVEL_1(false, 1, MB, new int[] { 1600, 1024, 768, 512 }, 70), LEVEL_2(false, 2, (int) (1.5 * MB), new int[] { 2048, 1600, 1024, 768, 512 }, 75), - LEVEL_3(false, 3, (int) (2.5 * MB), new int[] { 3072, 2048, 1600, 1024, 768, 512 }, 80); + LEVEL_3(false, 3, (int) (3 * MB), new int[] { 4096, 3072, 2048, 1600, 1024, 768, 512 }, 75); private final boolean isLowMemory; private final int level; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SentMediaQuality.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SentMediaQuality.java new file mode 100644 index 0000000000..f205ff4f54 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SentMediaQuality.java @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms.mms; + +import androidx.annotation.NonNull; + +/** + * Quality levels to send media at. + */ +public enum SentMediaQuality { + STANDARD(0), + HIGH(1); + + + private final int code; + + SentMediaQuality(int code) { + this.code = code; + } + + public static @NonNull SentMediaQuality fromCode(int code) { + if (HIGH.code == code) { + return HIGH; + } + return STANDARD; + } + + public int getCode() { + return code; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 27355c3d0d..1959096267 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.PushMediaConstraints; +import org.thoughtcrime.securesms.mms.SentMediaQuality; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker; @@ -135,7 +136,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu throw new AssertionError("No KEY_IMAGE_URI supplied"); } - MediaConstraints mediaConstraints = new PushMediaConstraints(); + MediaConstraints mediaConstraints = new PushMediaConstraints(SentMediaQuality.HIGH); imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext()); imageMaxHeight = mediaConstraints.getImageMaxHeight(requireContext()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/CheckedLinearLayout.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/CheckedLinearLayout.java new file mode 100644 index 0000000000..d76b398545 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/CheckedLinearLayout.java @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.util.views; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Checkable; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +/** + * LinearLayout that supports being checkable, useful for complicated "selectedable" + * buttons that aren't really buttons. + */ +public final class CheckedLinearLayout extends LinearLayout implements Checkable { + private static final int[] CHECKED_STATE = { android.R.attr.state_checked }; + private boolean checked = false; + + public CheckedLinearLayout(Context context) { + super(context); + } + + public CheckedLinearLayout(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CheckedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected @NonNull Parcelable onSaveInstanceState() { + return new InstanceState(Objects.requireNonNull(super.onSaveInstanceState()), checked); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + InstanceState instanceState = (InstanceState) state; + super.onRestoreInstanceState(instanceState.getSuperState()); + setChecked(instanceState.checked); + } + + @Override + public void setChecked(boolean checked) { + if (this.checked != checked) { + toggle(); + } + } + + @Override + public boolean isChecked() { + return checked; + } + + @Override + public void toggle() { + checked = !checked; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof Checkable) { + ((Checkable) child).setChecked(checked); + } + } + refreshDrawableState(); + } + + @Override + protected int[] onCreateDrawableState(final int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE); + } + return drawableState; + } + + private static class InstanceState extends BaseSavedState { + private final boolean checked; + + InstanceState(@NonNull Parcelable superState, boolean checked) { + super(superState); + this.checked = checked; + } + + private InstanceState(@NonNull Parcel in) { + super(in); + checked = in.readInt() > 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(checked ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public InstanceState createFromParcel(Parcel in) { + return new InstanceState(in); + } + + public InstanceState[] newArray(int size) { + return new InstanceState[size]; + } + }; + } +} diff --git a/app/src/main/res/color/checkable_stroke_color.xml b/app/src/main/res/color/checkable_stroke_color.xml new file mode 100644 index 0000000000..8d6362ee2b --- /dev/null +++ b/app/src/main/res/color/checkable_stroke_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/quality_selector_button_text.xml b/app/src/main/res/color/quality_selector_button_text.xml new file mode 100644 index 0000000000..9c24d635f3 --- /dev/null +++ b/app/src/main/res/color/quality_selector_button_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_view_infinite_32.png b/app/src/main/res/drawable-hdpi/ic_view_infinite_32.png deleted file mode 100644 index 983bee58d4..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_view_infinite_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_view_once_32.png b/app/src/main/res/drawable-hdpi/ic_view_once_32.png deleted file mode 100644 index 16ddff5f92..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_view_once_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_view_infinite_32.png b/app/src/main/res/drawable-mdpi/ic_view_infinite_32.png deleted file mode 100644 index 64f179aef5..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_view_infinite_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_view_once_32.png b/app/src/main/res/drawable-mdpi/ic_view_once_32.png deleted file mode 100644 index 4044e60c2b..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_view_once_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-v21/checkable_outline_background.xml b/app/src/main/res/drawable-v21/checkable_outline_background.xml new file mode 100644 index 0000000000..6593d5db90 --- /dev/null +++ b/app/src/main/res/drawable-v21/checkable_outline_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_view_infinite_32.png b/app/src/main/res/drawable-xhdpi/ic_view_infinite_32.png deleted file mode 100644 index 4db9a007cb..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_view_infinite_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_view_once_32.png b/app/src/main/res/drawable-xhdpi/ic_view_once_32.png deleted file mode 100644 index cabaf709c5..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_view_once_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_view_infinite_32.png b/app/src/main/res/drawable-xxhdpi/ic_view_infinite_32.png deleted file mode 100644 index 812c7a79d9..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_view_infinite_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_view_once_32.png b/app/src/main/res/drawable-xxhdpi/ic_view_once_32.png deleted file mode 100644 index e77b837b89..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_view_once_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_view_infinite_32.png b/app/src/main/res/drawable-xxxhdpi/ic_view_infinite_32.png deleted file mode 100644 index f7ebc26695..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_view_infinite_32.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_view_once_32.png b/app/src/main/res/drawable-xxxhdpi/ic_view_once_32.png deleted file mode 100644 index fe549441d8..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_view_once_32.png and /dev/null differ diff --git a/app/src/main/res/drawable/checkable_outline.xml b/app/src/main/res/drawable/checkable_outline.xml new file mode 100644 index 0000000000..174159a9a9 --- /dev/null +++ b/app/src/main/res/drawable/checkable_outline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/checkable_outline_background.xml b/app/src/main/res/drawable/checkable_outline_background.xml new file mode 100644 index 0000000000..2a7c3b7f9e --- /dev/null +++ b/app/src/main/res/drawable/checkable_outline_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_quality_high_32.xml b/app/src/main/res/drawable/ic_quality_high_32.xml new file mode 100644 index 0000000000..d36d3c1dd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_quality_high_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_quality_standard_32.xml b/app/src/main/res/drawable/ic_quality_standard_32.xml new file mode 100644 index 0000000000..e1d2bc1080 --- /dev/null +++ b/app/src/main/res/drawable/ic_quality_standard_32.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_view_infinite_28.xml b/app/src/main/res/drawable/ic_view_infinite_28.xml new file mode 100644 index 0000000000..3eff20db72 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_infinite_28.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_once_28.xml b/app/src/main/res/drawable/ic_view_once_28.xml new file mode 100644 index 0000000000..af6b1276f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_once_28.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/mediasend_activity.xml b/app/src/main/res/layout/mediasend_activity.xml index 1bed34e4d6..13897fb090 100644 --- a/app/src/main/res/layout/mediasend_activity.xml +++ b/app/src/main/res/layout/mediasend_activity.xml @@ -61,15 +61,31 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="12dp" android:orientation="horizontal"> - - + + + android:layout_marginEnd="12dp" + android:layout_marginBottom="4dp" + android:foreground="?attr/selectableItemBackground" + app:srcCompat="@drawable/ic_quality_standard_32" + app:tint="@color/core_white" + tools:ignore="UnusedAttribute" /> + android:layout="@layout/conversation_mention_suggestions_stub" /> diff --git a/app/src/main/res/layout/quality_selector_dialog.xml b/app/src/main/res/layout/quality_selector_dialog.xml new file mode 100644 index 0000000000..e538a5164b --- /dev/null +++ b/app/src/main/res/layout/quality_selector_dialog.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 975adbd3a1..b580051b37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3375,6 +3375,13 @@ Group description + + Standard + Faster, less data + High + Slower, more data + Photo quality + diff --git a/app/src/main/res/values/text_styles.xml b/app/src/main/res/values/text_styles.xml index b2da32d425..be7b716f21 100644 --- a/app/src/main/res/values/text_styles.xml +++ b/app/src/main/res/values/text_styles.xml @@ -151,6 +151,10 @@ 13sp + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java new file mode 100644 index 0000000000..8907c217ae --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.database; + +import org.junit.Test; +import org.thoughtcrime.securesms.mms.SentMediaQuality; + +import static org.junit.Assert.assertEquals; + +public class AttachmentDatabaseTransformPropertiesTest { + + @Test + public void transformProperties_verifyStructure() { + AttachmentDatabase.TransformProperties properties = AttachmentDatabase.TransformProperties.empty(); + assertEquals("Added transform property, need to confirm default behavior for pre-existing payloads in database", + "{\"skipTransform\":false,\"videoTrim\":false,\"videoTrimStartTimeUs\":0,\"videoTrimEndTimeUs\":0,\"sentMediaQuality\":0,\"videoEdited\":false}", + properties.serialize()); + } + + @Test + public void transformProperties_verifyMissingSentMediaQualityDefaultBehavior() { + String json = "{\"skipTransform\":false,\"videoTrim\":false,\"videoTrimStartTimeUs\":0,\"videoTrimEndTimeUs\":0,\"videoEdited\":false}"; + + AttachmentDatabase.TransformProperties properties = AttachmentDatabase.TransformProperties.parse(json); + + assertEquals(0, properties.getSentMediaQuality()); + assertEquals(SentMediaQuality.STANDARD, SentMediaQuality.fromCode(properties.getSentMediaQuality())); + } + +} \ No newline at end of file