Add photo media quality selector when sending images.

This commit is contained in:
Cody Henthorne
2021-05-07 14:03:53 -04:00
committed by Greyson Parrelli
parent 8c9df8d3be
commit dd934e0095
43 changed files with 630 additions and 55 deletions

View File

@@ -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;
}
}

View File

@@ -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);
});

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View File

@@ -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())));
}
}