Video trimming behind feature flag.

This commit is contained in:
Alan Evans
2020-02-13 14:22:21 -04:00
committed by Greyson Parrelli
parent 7f867a6185
commit 40fd7ca332
41 changed files with 1966 additions and 268 deletions

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.mediasend;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public final class ImageEditorModelRenderMediaTransform implements MediaTransform {
private static final String TAG = Log.tag(ImageEditorModelRenderMediaTransform.class);
private final EditorModel modelToRender;
ImageEditorModelRenderMediaTransform(@NonNull EditorModel modelToRender) {
this.modelToRender = modelToRender;
}
@WorkerThread
@Override
public @NonNull Media transform(@NonNull Context context, @NonNull Media media) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap bitmap = modelToRender.render(context);
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
Uri uri = BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption(), Optional.absent());
} catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
return media;
} finally {
bitmap.recycle();
try {
outputStream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}

View File

@@ -3,9 +3,14 @@ package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
/**
* Represents a piece of media that the user has on their device.
@@ -22,8 +27,9 @@ public class Media implements Parcelable {
private final long size;
private final long duration;
private Optional<String> bucketId;
private Optional<String> caption;
private Optional<String> bucketId;
private Optional<String> caption;
private Optional<AttachmentDatabase.TransformProperties> transformProperties;
public Media(@NonNull Uri uri,
@NonNull String mimeType,
@@ -33,17 +39,19 @@ public class Media implements Parcelable {
long size,
long duration,
Optional<String> bucketId,
Optional<String> caption)
Optional<String> caption,
Optional<AttachmentDatabase.TransformProperties> transformProperties)
{
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.duration = duration;
this.bucketId = bucketId;
this.caption = caption;
this.uri = uri;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.duration = duration;
this.bucketId = bucketId;
this.caption = caption;
this.transformProperties = transformProperties;
}
protected Media(Parcel in) {
@@ -56,6 +64,12 @@ public class Media implements Parcelable {
duration = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
try {
String json = in.readString();
transformProperties = json == null ? Optional.absent() : Optional.fromNullable(JsonUtil.fromJson(json, AttachmentDatabase.TransformProperties.class));
} catch (IOException e) {
throw new AssertionError(e);
}
}
public Uri getUri() {
@@ -98,6 +112,10 @@ public class Media implements Parcelable {
this.caption = Optional.fromNullable(caption);
}
public Optional<AttachmentDatabase.TransformProperties> getTransformProperties() {
return transformProperties;
}
@Override
public int describeContents() {
return 0;
@@ -114,6 +132,7 @@ public class Media implements Parcelable {
dest.writeLong(duration);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
dest.writeString(transformProperties.transform(JsonUtil::toJson).orNull());
}
public static final Creator<Media> CREATOR = new Creator<Media>() {

View File

@@ -4,31 +4,28 @@ import android.Manifest;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.OpenableColumns;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import android.util.Pair;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -77,12 +74,12 @@ public class MediaRepository {
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMostRecentItem(context)));
}
void renderMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, EditorModel> modelsToRender,
@NonNull Callback<LinkedHashMap<Media, Media>> callback)
static void transformMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, MediaTransform> modelsToTransform,
@NonNull Callback<LinkedHashMap<Media, Media>> callback)
{
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(renderMedia(context, currentMedia, modelsToRender)));
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(transformMedia(context, currentMedia, modelsToTransform)));
}
@WorkerThread
@@ -220,7 +217,7 @@ public class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0;
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent()));
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent(), Optional.absent()));
}
}
@@ -249,35 +246,16 @@ public class MediaRepository {
}
@WorkerThread
private LinkedHashMap<Media, Media> renderMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, EditorModel> modelsToRender)
private static LinkedHashMap<Media, Media> transformMedia(@NonNull Context context,
@NonNull List<Media> currentMedia,
@NonNull Map<Media, MediaTransform> modelsToTransform)
{
LinkedHashMap<Media, Media> updatedMedia = new LinkedHashMap<>(currentMedia.size());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (Media media : currentMedia) {
EditorModel modelToRender = modelsToRender.get(media);
if (modelToRender != null) {
Bitmap bitmap = modelToRender.render(context);
try {
outputStream.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
Uri uri = BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption());
updatedMedia.put(media, updated);
} catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
updatedMedia.put(media, media);
} finally {
bitmap.recycle();
}
MediaTransform transformer = modelsToTransform.get(media);
if (transformer != null) {
updatedMedia.put(media, transformer.transform(context, media));
} else {
updatedMedia.put(media, media);
}
@@ -333,7 +311,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
}
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
@@ -359,7 +337,7 @@ public class MediaRepository {
height = dimens.second;
}
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption(), Optional.absent());
}
private static class FolderResult {

View File

@@ -87,6 +87,7 @@ import java.util.Map;
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller,
ImageEditorFragment.Controller,
MediaSendVideoFragment.Controller,
CameraFragment.Controller,
CameraContactSelectionFragment.Controller,
ViewTreeObserver.OnGlobalLayoutListener,
@@ -346,6 +347,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
navigateToMediaSend(Locale.getDefault());
}
@Override
public void onVideoBeginEdit(@NonNull Uri uri) {
viewModel.onVideoBeginEdit(uri);
}
@Override
public void onTouchEventsNeeded(boolean needed) {
MediaSendFragment fragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
@@ -414,6 +420,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
length,
0,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent(),
Optional.absent()
);
} catch (IOException e) {
@@ -512,7 +519,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
MediaSendFragment fragment = getMediaSendFragment();
if (fragment != null) {
viewModel.onSendClicked(buildModelsToRender(fragment), recipients).observe(this, result -> {
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients).observe(this, result -> {
finish();
});
} else {
@@ -533,13 +540,13 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
sendButton.setEnabled(false);
viewModel.onSendClicked(buildModelsToRender(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
}
private Map<Media, EditorModel> buildModelsToRender(@NonNull MediaSendFragment fragment) {
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
List<Media> mediaList = fragment.getAllMedia();
Map<Uri, Object> savedState = fragment.getSavedState();
Map<Media, EditorModel> modelsToRender = new HashMap<>();
Map<Media, MediaTransform> modelsToRender = new HashMap<>();
for (Media media : mediaList) {
Object state = savedState.get(media.getUri());
@@ -547,7 +554,14 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
if (state instanceof ImageEditorFragment.Data) {
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
if (model != null && model.isChanged()) {
modelsToRender.put(media, model);
modelsToRender.put(media, new ImageEditorModelRenderMediaTransform(model));
}
}
if (state instanceof MediaSendVideoFragment.Data) {
MediaSendVideoFragment.Data data = (MediaSendVideoFragment.Data) state;
if (data.durationEdited) {
modelsToRender.put(media, new VideoTrimTransform(data));
}
}
}

View File

@@ -1,28 +1,45 @@
package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.scribbles.VideoEditorHud;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Throttler;
import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException;
public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment {
public class MediaSendVideoFragment extends Fragment implements VideoEditorHud.EventListener,
MediaSendPageFragment {
private static final String TAG = MediaSendVideoFragment.class.getSimpleName();
private static final String TAG = Log.tag(MediaSendVideoFragment.class);
private static final String KEY_URI = "uri";
private Uri uri;
private final Throttler videoScanThrottle = new Throttler(150);
private final Handler handler = new Handler();
private Controller controller;
private Data data = new Data();
private Uri uri;
private VideoPlayer player;
private VideoEditorHud hud;
private Runnable updatePosition;
public static MediaSendVideoFragment newInstance(@NonNull Uri uri) {
Bundle args = new Bundle();
@@ -34,6 +51,15 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!(getActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement Controller interface.");
}
controller = (Controller) getActivity();
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.mediasend_video_fragment, container, false);
@@ -43,19 +69,50 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
uri = getArguments().getParcelable(KEY_URI);
player = view.findViewById(R.id.video_player);
uri = requireArguments().getParcelable(KEY_URI);
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
((VideoPlayer) view).setWindow(requireActivity().getWindow());
((VideoPlayer) view).setVideoSource(slide, true);
player.setWindow(requireActivity().getWindow());
player.setVideoSource(slide, true);
if (FeatureFlags.videoTrimming() && MediaConstraints.isVideoTranscodeAvailable()) {
hud = view.findViewById(R.id.video_editor_hud);
hud.setEventListener(this);
updateHud(data);
if (data.durationEdited) {
player.clip(data.startTimeUs, data.endTimeUs, true);
}
try {
hud.setVideoSource(slide);
hud.setVisibility(View.VISIBLE);
startPositionUpdates();
} catch (IOException e) {
Log.w(TAG, e);
}
player.setPlayerCallback(new VideoPlayer.PlayerCallback() {
@Override
public void onPlaying() {
hud.playing();
}
@Override
public void onStopped() {
hud.stopped();
}
});
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (getView() != null) {
((VideoPlayer) getView()).cleanup();
if (player != null) {
player.cleanup();
}
}
@@ -63,6 +120,32 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
public void onPause() {
super.onPause();
notifyHidden();
stopPositionUpdates();
}
@Override
public void onResume() {
super.onResume();
startPositionUpdates();
}
private void startPositionUpdates() {
if (hud != null && Build.VERSION.SDK_INT >= 23) {
stopPositionUpdates();
updatePosition = new Runnable() {
@Override
public void run() {
hud.setPosition(player.getPlaybackPositionUs());
handler.postDelayed(this, 100);
}
};
handler.post(updatePosition);
}
}
private void stopPositionUpdates() {
handler.removeCallbacks(updatePosition);
}
@Override
@@ -84,22 +167,106 @@ public class MediaSendVideoFragment extends Fragment implements MediaSendPageFra
@Override
public @Nullable View getPlaybackControls() {
VideoPlayer player = (VideoPlayer) getView();
if (hud != null && hud.getVisibility() == View.VISIBLE) return null;
return player != null ? player.getControlView() : null;
}
@Override
public @Nullable Object saveState() {
return null;
return data;
}
@Override
public void restoreState(@NonNull Object state) { }
public void restoreState(@NonNull Object state) {
if (state instanceof Data) {
data = (Data) state;
if (Build.VERSION.SDK_INT >= 23) {
updateHud(data);
}
} else {
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
}
}
@RequiresApi(api = 23)
private void updateHud(Data data) {
if (hud != null && data.totalDurationUs > 0 && data.durationEdited) {
hud.setDurationRange(data.totalDurationUs, data.startTimeUs, data.endTimeUs);
}
}
@Override
public void notifyHidden() {
if (getView() != null) {
((VideoPlayer) getView()).pause();
if (player != null) {
player.pause();
}
}
@Override
public void onEditVideoDuration(long totalDurationUs, long startTimeUs, long endTimeUs, boolean fromEdited, boolean editingComplete) {
controller.onTouchEventsNeeded(!editingComplete);
boolean wasEdited = data.durationEdited;
boolean durationEdited = startTimeUs > 0 || endTimeUs < totalDurationUs;
data.durationEdited = durationEdited;
data.totalDurationUs = totalDurationUs;
data.startTimeUs = startTimeUs;
data.endTimeUs = endTimeUs;
if (editingComplete) {
videoScanThrottle.clear();
}
videoScanThrottle.publish(() -> {
player.pause();
if (!editingComplete) {
player.removeClip(false);
}
player.setPlaybackPosition(fromEdited || editingComplete ? startTimeUs / 1000 : endTimeUs / 1000);
if (editingComplete) {
if (durationEdited) {
player.clip(startTimeUs, endTimeUs, true);
} else {
player.removeClip(true);
}
}
});
if (!wasEdited && durationEdited) {
controller.onVideoBeginEdit(uri);
}
}
@Override
public void onPlay() {
player.playFromStart();
}
@Override
public void onSeek(long position, boolean dragComplete) {
if (dragComplete) {
videoScanThrottle.clear();
}
videoScanThrottle.publish(() -> {
player.pause();
player.setPlaybackPosition(position);
});
}
static class Data {
boolean durationEdited;
long totalDurationUs;
long startTimeUs;
long endTimeUs;
}
public interface Controller {
void onTouchEventsNeeded(boolean needed);
void onVideoBeginEdit(@NonNull Uri uri);
}
}

View File

@@ -1,24 +1,22 @@
package org.thoughtcrime.securesms.mediasend;
import android.app.Application;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -34,7 +32,6 @@ import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
@@ -303,7 +300,7 @@ class MediaSendViewModel extends ViewModel {
captionVisible = false;
List<Media> uncaptioned = Stream.of(getSelectedMediaOrDefault())
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent()))
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent(), Optional.absent()))
.toList();
selectedMedia.setValue(uncaptioned);
@@ -405,6 +402,10 @@ class MediaSendViewModel extends ViewModel {
hudState.setValue(buildHudState());
}
void onVideoBeginEdit(@NonNull Uri uri) {
cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, Optional.absent(), Optional.absent(), Optional.absent()));
}
void onMediaCaptured(@NonNull Media media) {
lastCameraCapture = Optional.of(media);
@@ -449,7 +450,7 @@ class MediaSendViewModel extends ViewModel {
savedDrawState.putAll(state);
}
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, EditorModel> modelsToRender, @NonNull List<Recipient> recipients) {
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients) {
if (isSms && recipients.size() > 0) {
throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
}
@@ -463,9 +464,13 @@ class MediaSendViewModel extends ViewModel {
Util.runOnMainDelayed(dialogRunnable, 250);
repository.renderMedia(application, initialMedia, modelsToRender, (oldToNew) -> {
MediaRepository.transformMedia(application, initialMedia, modelsToTransform, (oldToNew) -> {
List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
for (Media media : updatedMedia){
Log.w(TAG, media.getUri().toString() + " : " + media.getTransformProperties().transform(t->"" + t.isVideoTrim()).or("null"));
}
if (isSms || MessageSender.isLocalSelfSend(application, recipient, isSms)) {
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.");
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce()));
@@ -477,7 +482,7 @@ class MediaSendViewModel extends ViewModel {
if (splitMessage.getTextSlide().isPresent()) {
Slide slide = splitMessage.getTextSlide().get();
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent()), recipient);
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent(), Optional.absent()), recipient);
}
uploadRepository.applyMediaUpdates(oldToNew, recipient);

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.mediasend;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
public interface MediaTransform {
@WorkerThread
@NonNull Media transform(@NonNull Context context, @NonNull Media media);
}

View File

@@ -78,7 +78,9 @@ class MediaUploadRepository {
void applyMediaUpdates(@NonNull Map<Media, Media> oldToNew, @Nullable Recipient recipient) {
executor.execute(() -> {
for (Map.Entry<Media, Media> entry : oldToNew.entrySet()) {
if (!entry.getKey().equals(entry.getValue()) || !uploadResults.containsKey(entry.getValue())) {
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);
}
@@ -187,9 +189,9 @@ class MediaUploadRepository {
}
}
private static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
public static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
if (MediaUtil.isVideoType(media.getMimeType())) {
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull()).asAttachment();
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull(), media.getTransformProperties().orNull()).asAttachment();
} else if (MediaUtil.isGif(media.getMimeType())) {
return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull()).asAttachment();
} else if (MediaUtil.isImageType(media.getMimeType())) {

View File

@@ -0,0 +1,33 @@
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.whispersystems.libsignal.util.guava.Optional;
public final class VideoTrimTransform implements MediaTransform {
private final MediaSendVideoFragment.Data data;
VideoTrimTransform(@NonNull MediaSendVideoFragment.Data data) {
this.data = data;
}
@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.getBucketId(),
media.getCaption(),
Optional.of(new AttachmentDatabase.TransformProperties(false, data.durationEdited, data.startTimeUs, data.endTimeUs)));
}
}