Add photo media quality selector when sending images.
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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<TransformProperties> 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<MediaKeyboard> 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<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
|
||||
List<Media> mediaList = fragment.getAllMedia();
|
||||
Map<Uri, Object> savedState = fragment.getSavedState();
|
||||
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment, @Nullable SentMediaQuality sentMediaQuality) {
|
||||
List<Media> mediaList = fragment.getAllMedia();
|
||||
Map<Uri, Object> savedState = fragment.getSavedState();
|
||||
Map<Media, MediaTransform> 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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> hudState;
|
||||
private final SingleLiveEvent<Error> error;
|
||||
private final SingleLiveEvent<Event> event;
|
||||
private final MutableLiveData<SentMediaQuality> sentMediaQuality;
|
||||
private final Map<Uri, Object> 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<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients, @NonNull List<Mention> 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<SentMediaQuality> getSentMediaQuality() {
|
||||
return sentMediaQuality;
|
||||
}
|
||||
|
||||
@NonNull MediaConstraints getMediaConstraints() {
|
||||
return mediaConstraints;
|
||||
}
|
||||
|
@ -583,10 +599,10 @@ class MediaSendViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
private HudState buildHudState() {
|
||||
List<Media> 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<Media> 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() {
|
||||
|
|
|
@ -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<Media, Media> oldToNew, @Nullable Recipient recipient) {
|
||||
executor.execute(() -> {
|
||||
for (Map.Entry<Media, Media> 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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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<InstanceState> CREATOR = new Parcelable.Creator<InstanceState>() {
|
||||
public InstanceState createFromParcel(Parcel in) {
|
||||
return new InstanceState(in);
|
||||
}
|
||||
|
||||
public InstanceState[] newArray(int size) {
|
||||
return new InstanceState[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_text_primary" android:state_checked="true" />
|
||||
<item android:color="@color/transparent" />
|
||||
</selector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/signal_text_primary" android:state_checked="true" />
|
||||
<item android:color="@color/signal_text_secondary" />
|
||||
</selector>
|
Przed Szerokość: | Wysokość: | Rozmiar: 3.4 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 1.9 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 1.9 KiB |
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/signal_inverse_transparent_40">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#000000" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/checkable_outline" />
|
||||
</ripple>
|
Przed Szerokość: | Wysokość: | Rozmiar: 5.0 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 4.9 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 9.1 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 8.6 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 14 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 13 KiB |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="18dp" />
|
||||
<stroke android:color="@color/checkable_stroke_color" android:width="1dp" />
|
||||
</shape>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/checkable_outline" android:state_checked="true" />
|
||||
</selector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.9,21l3.9,-5l5,6.7H8.2l3.9,-5L14.9,21z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M27.5,4.5h-23v23h23V4.5zM3,3v26h26V3H3z"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.1,21.9l3.2,-4.4l4.2,5.9h-13L9.8,19L12.1,21.9z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M27.5,4.5H3V3h26v26h-1.5V4.5z"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.5,10.5h-17v17h17V10.5zM3,9v20h20V9H3z"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<vector android:height="28dp" android:viewportHeight="28"
|
||||
android:viewportWidth="28" android:width="28dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M14,1a13,13 0,1 0,9.046 22.3l-0.3,1.232L22.75,26.5h1.5L24.241,21L18.775,21v1.5h1.491l1.858,-0.378A11.587,11.587 0,1 1,24.34 19L26,19A12.994,12.994 0,0 0,14 1ZM13.178,11.565a8.783,8.783 0,0 1,0.836 0.917,5.774 5.774,0 0,1 0.809,-0.917 4.692,4.692 0,0 1,3.345 -1.484,3.71 3.71,0 0,1 0,7.419 4.785,4.785 0,0 1,-3.345 -1.457,7.813 7.813,0 0,1 -0.809,-0.944 12.411,12.411 0,0 1,-0.836 0.944A4.775,4.775 0,0 1,9.859 17.5a3.71,3.71 0,1 1,0 -7.419A4.683,4.683 0,0 1,13.178 11.565ZM9.859,15.965A3.253,3.253 0,0 0,12.1 14.91a8.024,8.024 0,0 0,0.943 -1.133,7.663 7.663,0 0,0 -0.943,-1.106 3.205,3.205 0,0 0,-2.24 -1.052,2.172 2.172,0 1,0 0,4.343ZM18.168,11.622A3.211,3.211 0,0 0,15.9 12.671a9.093,9.093 0,0 0,-0.944 1.106A7.969,7.969 0,0 0,15.9 14.91a3.258,3.258 0,0 0,2.266 1.052,2.172 2.172,0 0,0 0,-4.343Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<vector android:height="28dp" android:viewportHeight="28"
|
||||
android:viewportWidth="28" android:width="28dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M11.165,19L9.627,19L9.627,10.912L9.543,10.912a2.687,2.687 0,0 1,-2.117 0.754v-1.26a2.546,2.546 0,0 0,2.31 -1.562h1.429ZM16.785,14.348 L18.485,11.383L20.1,11.383l-2.376,3.808L20.122,19L18.5,19l-1.716,-2.847L15.074,19L13.447,19l2.37,-3.809 -2.339,-3.808L15.1,11.383ZM14,1a13,13 0,1 0,9.046 22.3l-0.3,1.232L22.75,26.5h1.5L24.241,21L18.775,21v1.5h1.491l1.858,-0.378A11.587,11.587 0,1 1,24.34 19L26,19A12.994,12.994 0,0 0,14 1Z"/>
|
||||
</vector>
|
|
@ -61,15 +61,31 @@
|
|||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/mediasend_reveal_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:padding="2dp"
|
||||
app:tint="@color/core_white"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:src="@drawable/ic_view_once_28" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/mediasend_quality_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
tools:src="@drawable/ic_view_infinite_32" />
|
||||
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" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_compose_container"
|
||||
|
@ -194,7 +210,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout="@layout/conversation_mention_suggestions_stub"/>
|
||||
android:layout="@layout/conversation_mention_suggestions_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="?attr/dialogPreferredPadding">
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CheckedLinearLayout
|
||||
android:id="@+id/quality_selector_dialog_standard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/checkable_outline_background"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/quality_selector_dialog_high"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<CheckedTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/QualitySelectorBottomSheetDialog__standard"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="@color/quality_selector_button_text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/QualitySelectorBottomSheetDialog__faster_less_data"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||
android:textColor="@color/signal_text_secondary" />
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.CheckedLinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CheckedLinearLayout
|
||||
android:id="@+id/quality_selector_dialog_high"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/checkable_outline_background"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/quality_selector_dialog_standard"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<CheckedTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/QualitySelectorBottomSheetDialog__high"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="@color/quality_selector_button_text" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/QualitySelectorBottomSheetDialog__slower_more_data"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||
android:textColor="@color/signal_text_secondary" />
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.CheckedLinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/QualitySelectorBottomSheetDialog__photo_quality"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/quality_selector_dialog_standard" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -3375,6 +3375,13 @@
|
|||
<!-- GroupDescriptionDialog -->
|
||||
<string name="GroupDescriptionDialog__group_description">Group description</string>
|
||||
|
||||
<!-- QualitySelectorBottomSheetDialog -->
|
||||
<string name="QualitySelectorBottomSheetDialog__standard">Standard</string>
|
||||
<string name="QualitySelectorBottomSheetDialog__faster_less_data">Faster, less data</string>
|
||||
<string name="QualitySelectorBottomSheetDialog__high">High</string>
|
||||
<string name="QualitySelectorBottomSheetDialog__slower_more_data">Slower, more data</string>
|
||||
<string name="QualitySelectorBottomSheetDialog__photo_quality">Photo quality</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -151,6 +151,10 @@
|
|||
<item name="android:textSize">13sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Subtitle.Bold">
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Subtitle2" parent="@style/TextAppearance.MaterialComponents.Subtitle2">
|
||||
</style>
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|