mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 11:20:47 +01:00
Video trimming behind feature flag.
This commit is contained in:
committed by
Greyson Parrelli
parent
7f867a6185
commit
40fd7ca332
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user