Separate compression job.

master
Alan Evans 2019-08-06 16:52:15 -04:00 committed by Greyson Parrelli
parent 7f0a7b0c13
commit 942154a61f
17 changed files with 424 additions and 308 deletions

View File

@ -2,14 +2,15 @@ package org.thoughtcrime.securesms.components;
import android.animation.LayoutTransition;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.pnikosis.materialishprogress.ProgressWheel;
@ -28,7 +29,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TransferControlView extends FrameLayout {
public final class TransferControlView extends FrameLayout {
private static final int UPLOAD_TASK_WEIGHT = 1;
/**
* A weighting compared to {@link #UPLOAD_TASK_WEIGHT}
*/
private static final int COMPRESSION_TASK_WEIGHT = 3;
@Nullable private List<Slide> slides;
@Nullable private View current;
@ -37,7 +45,8 @@ public class TransferControlView extends FrameLayout {
private final View downloadDetails;
private final TextView downloadDetailsText;
private final Map<Attachment, Float> downloadProgress;
private final Map<Attachment, Float> networkProgress;
private final Map<Attachment, Float> compresssionProgress;
public TransferControlView(Context context) {
this(context, null);
@ -56,7 +65,9 @@ public class TransferControlView extends FrameLayout {
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.downloadProgress = new HashMap<>();
this.networkProgress = new HashMap<>();
this.compresssionProgress = new HashMap<>();
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
@ -98,19 +109,20 @@ public class TransferControlView extends FrameLayout {
this.slides = slides;
if (!isUpdateToExistingSet(slides)) {
downloadProgress.clear();
Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f));
networkProgress.clear();
compresssionProgress.clear();
Stream.of(slides).forEach(s -> networkProgress.put(s.asAttachment(), 0f));
}
for (Slide slide : slides) {
if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
downloadProgress.put(slide.asAttachment(), 1f);
networkProgress.put(slide.asAttachment(), 1f);
}
}
switch (getTransferState(slides)) {
case AttachmentDatabase.TRANSFER_PROGRESS_STARTED:
showProgressSpinner(calculateProgress(downloadProgress));
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
break;
case AttachmentDatabase.TRANSFER_PROGRESS_PENDING:
case AttachmentDatabase.TRANSFER_PROGRESS_FAILED:
@ -124,7 +136,7 @@ public class TransferControlView extends FrameLayout {
}
public void showProgressSpinner() {
showProgressSpinner(calculateProgress(downloadProgress));
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
}
public void showProgressSpinner(float progress) {
@ -158,12 +170,12 @@ public class TransferControlView extends FrameLayout {
}
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
if (slides.size() != downloadProgress.size()) {
if (slides.size() != networkProgress.size()) {
return false;
}
for (Slide slide : slides) {
if (!downloadProgress.containsKey(slide.asAttachment())) {
if (!networkProgress.containsKey(slide.asAttachment())) {
return false;
}
}
@ -207,19 +219,36 @@ public class TransferControlView extends FrameLayout {
current = view;
}
private float calculateProgress(@NonNull Map<Attachment, Float> downloadProgress) {
float totalProgress = 0;
for (float progress : downloadProgress.values()) {
totalProgress += progress / downloadProgress.size();
private static float calculateProgress(@NonNull Map<Attachment, Float> uploadDownloadProgress, Map<Attachment, Float> compresssionProgress) {
float totalDownloadProgress = 0;
float totalCompressionProgress = 0;
for (float progress : uploadDownloadProgress.values()) {
totalDownloadProgress += progress;
}
return totalProgress;
for (float progress : compresssionProgress.values()) {
totalCompressionProgress += progress;
}
float weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress;
float weightedTotal = UPLOAD_TASK_WEIGHT * uploadDownloadProgress.size() + COMPRESSION_TASK_WEIGHT * compresssionProgress.size();
return weightedProgress / weightedTotal;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
if (downloadProgress.containsKey(event.attachment)) {
downloadProgress.put(event.attachment, ((float) event.progress) / event.total);
progressWheel.setInstantProgress(calculateProgress(downloadProgress));
if (networkProgress.containsKey(event.attachment)) {
float proportionCompleted = ((float) event.progress) / event.total;
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compresssionProgress.put(event.attachment, proportionCompleted);
} else {
networkProgress.put(event.attachment, proportionCompleted);
}
progressWheel.setInstantProgress(calculateProgress(networkProgress, compresssionProgress));
}
}
}

View File

@ -1340,9 +1340,9 @@ public class ConversationItem extends LinearLayout
database.markAsOutbox(messageRecord.getId());
database.markAsForcedSms(messageRecord.getId());
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MmsSendJob(messageRecord.getId()));
MmsSendJob.enqueue(context,
ApplicationContext.getInstance(context).getJobManager(),
messageRecord.getId());
} else {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
database.markAsInsecure(messageRecord.getId());

View File

@ -497,8 +497,8 @@ public class AttachmentDatabase extends Database {
return insertedAttachments;
}
public @NonNull DatabaseAttachment updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@NonNull MediaStream mediaStream)
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@NonNull MediaStream mediaStream)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -518,29 +518,8 @@ public class AttachmentDatabase extends Database {
contentValues.put(DATA_RANDOM, dataInfo.random);
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings());
return new DatabaseAttachment(databaseAttachment.getAttachmentId(),
databaseAttachment.getMmsId(),
databaseAttachment.hasData(),
databaseAttachment.hasThumbnail(),
mediaStream.getMimeType(),
databaseAttachment.getTransferState(),
dataInfo.length,
databaseAttachment.getFileName(),
databaseAttachment.getLocation(),
databaseAttachment.getKey(),
databaseAttachment.getRelay(),
databaseAttachment.getDigest(),
databaseAttachment.getFastPreflightId(),
databaseAttachment.isVoiceNote(),
mediaStream.getWidth(),
mediaStream.getHeight(),
databaseAttachment.isQuote(),
databaseAttachment.getCaption(),
databaseAttachment.getSticker());
}
public void updateAttachmentFileName(@NonNull AttachmentId attachmentId,
@Nullable String fileName)
{

View File

@ -1,18 +1,24 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.attachments.Attachment;
public class PartProgressEvent {
public final class PartProgressEvent {
public final Attachment attachment;
public final Type type;
public final long total;
public final long progress;
public PartProgressEvent(@NonNull Attachment attachment, long total, long progress) {
public enum Type {
COMPRESSION,
NETWORK
}
public PartProgressEvent(@NonNull Attachment attachment, @NonNull Type type, long total, long progress) {
this.attachment = attachment;
this.type = type;
this.total = total;
this.progress = progress;
}

View File

@ -0,0 +1,231 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
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.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.video.InMemoryTranscoder;
import org.thoughtcrime.securesms.video.VideoSizeException;
import org.thoughtcrime.securesms.video.VideoSourceException;
import org.thoughtcrime.securesms.video.videoconverter.EncodingException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public final class AttachmentCompressionJob extends BaseJob {
public static final String KEY = "AttachmentCompressionJob";
@SuppressWarnings("unused")
private static final String TAG = Log.tag(AttachmentCompressionJob.class);
private static final String KEY_ROW_ID = "row_id";
private static final String KEY_UNIQUE_ID = "unique_id";
private static final String KEY_MMS = "mms";
private static final String KEY_MMS_SUBSCRIPTION_ID = "mms_subscription_id";
private final AttachmentId attachmentId;
private final boolean mms;
private final int mmsSubscriptionId;
public static AttachmentCompressionJob fromAttachment(@NonNull DatabaseAttachment databaseAttachment,
boolean mms,
int mmsSubscriptionId)
{
return new AttachmentCompressionJob(databaseAttachment.getAttachmentId(),
MediaUtil.isVideo(databaseAttachment) && MediaConstraints.isVideoTranscodeAvailable(),
mms,
mmsSubscriptionId);
}
private AttachmentCompressionJob(@NonNull AttachmentId attachmentId,
boolean isVideoTranscode,
boolean mms,
int mmsSubscriptionId)
{
this(new Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setQueue(isVideoTranscode ? "VIDEO_TRANSCODE" : null)
.build(),
attachmentId,
mms,
mmsSubscriptionId);
}
private AttachmentCompressionJob(@NonNull Parameters parameters,
@NonNull AttachmentId attachmentId,
boolean mms,
int mmsSubscriptionId)
{
super(parameters);
this.attachmentId = attachmentId;
this.mms = mms;
this.mmsSubscriptionId = mmsSubscriptionId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
.putBoolean(KEY_MMS, mms)
.putInt(KEY_MMS_SUBSCRIPTION_ID, mmsSubscriptionId)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws Exception {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
if (databaseAttachment == null) {
throw new IllegalStateException("Cannot find the specified attachment.");
}
MediaConstraints mediaConstraints = mms ? MediaConstraints.getMmsMediaConstraints(mmsSubscriptionId)
: MediaConstraints.getPushMediaConstraints();
scaleAndStripExif(database, mediaConstraints, databaseAttachment);
}
@Override
public void onCanceled() { }
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
return exception instanceof IOException;
}
private void scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull MediaConstraints constraints,
@NonNull DatabaseAttachment attachment)
throws UndeliverableMessageException
{
try {
if (MediaUtil.isVideo(attachment) && MediaConstraints.isVideoTranscodeAvailable()) {
transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault());
} else if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, stripped);
}
} else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, resized);
} else {
throw new UndeliverableMessageException("Size constraints could not be met!");
}
} catch (IOException | MmsException e) {
throw new UndeliverableMessageException(e);
}
}
@RequiresApi(26)
private static void transcodeVideoIfNeededToDatabase(@NonNull Context context,
@NonNull AttachmentDatabase attachmentDatabase,
@NonNull DatabaseAttachment attachment,
@NonNull MediaConstraints constraints,
@NonNull EventBus eventBus)
throws UndeliverableMessageException
{
try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
notification.setIndeterminateProgress();
try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId())) {
if (dataSource == null) {
throw new UndeliverableMessageException("Cannot get media data source for attachment.");
}
try (InMemoryTranscoder transcoder = new InMemoryTranscoder(context, dataSource, constraints.getCompressedVideoMaxSize(context))) {
if (transcoder.isTranscodeRequired()) {
MediaStream mediaStream = transcoder.transcode(percent -> {
notification.setProgress(100, percent);
eventBus.postSticky(new PartProgressEvent(attachment,
PartProgressEvent.Type.COMPRESSION,
100,
percent));
});
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
}
}
}
} catch (VideoSourceException | EncodingException e) {
if (attachment.getSize() > constraints.getVideoMaxSize(context)) {
throw new UndeliverableMessageException("Duration not found, attachment too large to skip transcode", e);
} else {
Log.w(TAG, "Problem with video source, but video small enough to skip transcode", e);
}
} catch (IOException | MmsException | VideoSizeException e) {
throw new UndeliverableMessageException("Failed to transcode", e);
}
}
private static MediaStream getResizedMedia(@NonNull Context context,
@NonNull Attachment attachment,
@NonNull MediaConstraints constraints)
throws IOException
{
if (!constraints.canResize(attachment)) {
throw new UnsupportedOperationException("Cannot resize this content type");
}
try {
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context,
new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()),
constraints);
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()),
MediaUtil.IMAGE_JPEG,
scaleResult.getWidth(),
scaleResult.getHeight());
} catch (BitmapDecodingException e) {
throw new IOException(e);
}
}
public static final class Factory implements Job.Factory<AttachmentCompressionJob> {
@Override
public @NonNull AttachmentCompressionJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AttachmentCompressionJob(parameters,
new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)),
data.getBoolean(KEY_MMS),
data.getInt(KEY_MMS_SUBSCRIPTION_ID));
}
}
}

View File

@ -162,7 +162,7 @@ public class AttachmentDownloadJob extends BaseJob {
SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)));
InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)));
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {

View File

@ -17,12 +17,9 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@ -32,11 +29,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
public class AttachmentUploadJob extends BaseJob {
/**
* Uploads an attachment without alteration.
* <p>
* Queue {@link AttachmentCompressionJob} before to compress.
*/
public final class AttachmentUploadJob extends BaseJob {
public static final String KEY = "AttachmentUploadJob";
public static final String KEY = "AttachmentUploadJobV2";
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
@SuppressWarnings("unused")
private static final String TAG = Log.tag(AttachmentUploadJob.class);
private static final String KEY_ROW_ID = "row_id";
private static final String KEY_UNIQUE_ID = "unique_id";
@ -46,26 +49,13 @@ public class AttachmentUploadJob extends BaseJob {
*/
private static final int FOREGROUND_LIMIT = 10 * 1024 * 1024;
/**
* The {@link PartProgressEvent} on the {@link EventBus} is shared between transcoding and uploading.
* <p>
* This number is the ratio that represents the transcoding effort, after which it will hand
* over to the to complete the progress.
*/
private static final double ENCODING_PROGRESS_RATIO = 0.75;
private final AttachmentId attachmentId;
private final AttachmentId attachmentId;
public static AttachmentUploadJob fromAttachment(DatabaseAttachment databaseAttachment) {
return new AttachmentUploadJob(databaseAttachment.getAttachmentId(), MediaUtil.isVideo(databaseAttachment) && MediaConstraints.isVideoTranscodeAvailable());
}
private AttachmentUploadJob(AttachmentId attachmentId, boolean isVideoTranscode) {
public AttachmentUploadJob(AttachmentId attachmentId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setQueue(isVideoTranscode ? "VIDEO_TRANSCODE" : null)
.build(),
attachmentId);
}
@ -97,13 +87,8 @@ public class AttachmentUploadJob extends BaseJob {
throw new InvalidAttachmentException("Cannot find the specified attachment.");
}
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
boolean videoTranscodeOccurred = databaseAttachment != scaledAttachment && MediaUtil.isVideo(scaledAttachment);
double progressStartPoint = videoTranscodeOccurred ? ENCODING_PROGRESS_RATIO : 0;
try (NotificationController notification = getNotificationForAttachment(scaledAttachment)) {
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment, notification, progressStartPoint);
try (NotificationController notification = getNotificationForAttachment(databaseAttachment)) {
SignalServiceAttachment localAttachment = getAttachmentFor(databaseAttachment, notification);
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker());
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
@ -127,14 +112,7 @@ public class AttachmentUploadJob extends BaseJob {
return exception instanceof IOException;
}
/**
* @param progressStartPoint A value from 0..1 that represents any progress already shown.
* The {@link PartProgressEvent} of this task will fit in the remaining
* 1 - progressStartPoint.
*/
private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, double progressStartPoint)
throws InvalidAttachmentException
{
private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification) throws InvalidAttachmentException {
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
@ -148,8 +126,7 @@ public class AttachmentUploadJob extends BaseJob {
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener((total, progress) -> {
long cumulativeProgress = (long) ((1.0 - progressStartPoint) * progress + total * progressStartPoint);
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, cumulativeProgress));
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
if (notification != null) {
notification.setProgress(total, progress);
}
@ -160,25 +137,6 @@ public class AttachmentUploadJob extends BaseJob {
}
}
private Attachment scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull MediaConstraints constraints,
@NonNull DatabaseAttachment attachment)
throws UndeliverableMessageException
{
MediaResizer mediaResizer = new MediaResizer(context, constraints);
MediaResizer.ProgressListener progressListener = (progress, total) -> {
PartProgressEvent event = new PartProgressEvent(attachment,
total,
(long) (progress * ENCODING_PROGRESS_RATIO));
EventBus.getDefault().postSticky(event);
};
return mediaResizer.scaleAndStripExifToDatabase(attachmentDatabase,
attachment,
progressListener);
}
private class InvalidAttachmentException extends Exception {
InvalidAttachmentException(String message) {
super(message);

View File

@ -29,6 +29,7 @@ public final class JobManagerFactories {
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
@ -82,6 +83,8 @@ public final class JobManagerFactories {
// Dead jobs
put("PushContentReceiveJob", new FailingJob.Factory());
put("AttachmentUploadJob", new FailingJob.Factory());
put("MmsSendJob", new FailingJob.Factory());
}};
}

View File

@ -1,154 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.logging.Log;
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.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.video.InMemoryTranscoder;
import org.thoughtcrime.securesms.video.VideoSourceException;
import org.thoughtcrime.securesms.video.VideoSizeException;
import org.thoughtcrime.securesms.video.videoconverter.EncodingException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
final class MediaResizer {
private static final String TAG = Log.tag(MediaResizer.class);
@NonNull private final Context context;
@NonNull private final MediaConstraints constraints;
MediaResizer(@NonNull Context context,
@NonNull MediaConstraints constraints)
{
this.context = context;
this.constraints = constraints;
}
List<Attachment> scaleAndStripExifToDatabase(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull List<Attachment> attachments)
throws UndeliverableMessageException
{
List<Attachment> results = new ArrayList<>(attachments.size());
for (Attachment attachment : attachments) {
results.add(scaleAndStripExifToDatabase(attachmentDatabase, (DatabaseAttachment) attachment, null));
}
return results;
}
DatabaseAttachment scaleAndStripExifToDatabase(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull DatabaseAttachment attachment,
@Nullable ProgressListener transcodeProgressListener)
throws UndeliverableMessageException
{
try {
if (MediaUtil.isVideo(attachment) && MediaConstraints.isVideoTranscodeAvailable()) {
return transcodeVideoIfNeededToDatabase(attachmentDatabase, attachment, transcodeProgressListener);
} else if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment);
return attachmentDatabase.updateAttachmentData(attachment, stripped);
} else {
return attachment;
}
} else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment);
return attachmentDatabase.updateAttachmentData(attachment, resized);
} else {
throw new UndeliverableMessageException("Size constraints could not be met!");
}
} catch (IOException | MmsException e) {
throw new UndeliverableMessageException(e);
}
}
@RequiresApi(26)
private @NonNull DatabaseAttachment transcodeVideoIfNeededToDatabase(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull DatabaseAttachment attachment,
@Nullable ProgressListener progressListener)
throws UndeliverableMessageException
{
try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
notification.setIndeterminateProgress();
try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId())) {
if (dataSource == null) {
throw new UndeliverableMessageException("Cannot get media data source for attachment.");
}
try (InMemoryTranscoder transcoder = new InMemoryTranscoder(context, dataSource, constraints.getCompressedVideoMaxSize(context))) {
if (transcoder.isTranscodeRequired()) {
MediaStream mediaStream = transcoder.transcode(percent -> {
notification.setProgress(100, percent);
if (progressListener != null) {
progressListener.onProgress(percent, 100);
}
});
return attachmentDatabase.updateAttachmentData(attachment, mediaStream);
} else {
return attachment;
}
}
}
} catch (VideoSourceException | EncodingException e) {
if (attachment.getSize() > constraints.getVideoMaxSize(context)) {
throw new UndeliverableMessageException("Duration not found, attachment too large to skip transcode", e);
} else {
Log.w(TAG, "Duration not found, video small enough to skip transcode", e);
return attachment;
}
} catch (IOException | MmsException | VideoSizeException e) {
throw new UndeliverableMessageException("Failed to transcode", e);
}
}
private MediaStream getResizedMedia(@NonNull Context context, @NonNull Attachment attachment)
throws IOException
{
if (!constraints.canResize(attachment)) {
throw new UnsupportedOperationException("Cannot resize this content type");
}
try {
// XXX - This is loading everything into memory! We want the send path to be stream-like.
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), constraints);
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()), MediaUtil.IMAGE_JPEG, scaleResult.getWidth(), scaleResult.getHeight());
} catch (BitmapDecodingException e) {
throw new IOException(e);
}
}
public interface ProgressListener {
void onProgress(long progress, long total);
}
}

View File

@ -1,16 +1,14 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
import com.annimon.stream.Stream;
import com.google.android.mms.ContentType;
import com.google.android.mms.InvalidHeaderValueException;
import com.google.android.mms.pdu_alt.CharacterSets;
@ -25,11 +23,17 @@ import com.google.android.mms.smil.SmilHelper;
import com.klinker.android.send_message.Utils;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MmsException;
@ -50,17 +54,17 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class MmsSendJob extends SendJob {
public final class MmsSendJob extends SendJob {
public static final String KEY = "MmsSendJob";
public static final String KEY = "MmsSendJobV2";
private static final String TAG = MmsSendJob.class.getSimpleName();
private static final String KEY_MESSAGE_ID = "message_id";
private long messageId;
private final long messageId;
public MmsSendJob(long messageId) {
private MmsSendJob(long messageId) {
this(new Job.Parameters.Builder()
.setQueue("mms-operation")
.addConstraint(NetworkConstraint.KEY)
@ -69,6 +73,29 @@ public class MmsSendJob extends SendJob {
messageId);
}
/** Enqueues compression jobs for attachments and finally the MMS send job. */
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message;
try {
message = database.getOutgoingMessage(messageId);
} catch (MmsException | NoSuchMessageException e) {
throw new AssertionError(e);
}
List<Job> compressionJobs = Stream.of(message.getAttachments())
.map(a -> (Job) AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, true, message.getSubscriptionId()))
.toList();
MmsSendJob sendJob = new MmsSendJob(messageId);
jobManager.startChain(compressionJobs)
.then(sendJob)
.enqueue();
}
private MmsSendJob(@NonNull Job.Parameters parameters, long messageId) {
super(parameters);
this.messageId = messageId;
@ -196,7 +223,7 @@ public class MmsSendJob extends SendJob {
String lineNumber = getMyNumber(context);
Address destination = message.getRecipient().getAddress();
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId());
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
List<Attachment> scaledAttachments = message.getAttachments();
if (!TextUtils.isEmpty(lineNumber)) {
req.setFrom(new EncodedStringValue(lineNumber));

View File

@ -94,12 +94,15 @@ public class PushGroupSendJob extends PushSendJob {
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> AttachmentUploadJob.fromAttachment((DatabaseAttachment) a)).toList();
List<AttachmentCompressionJob> compressionJobs = Stream.of(attachments).map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)).toList();
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList();
if (attachmentJobs.isEmpty()) {
jobManager.add(new PushGroupSendJob(messageId, destination, filterAddress));
} else {
jobManager.startChain(attachmentJobs)
jobManager.startChain(compressionJobs)
.then(attachmentJobs)
.then(new PushGroupSendJob(messageId, destination, filterAddress))
.enqueue();
}

View File

@ -81,12 +81,15 @@ public class PushMediaSendJob extends PushSendJob {
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> AttachmentUploadJob.fromAttachment((DatabaseAttachment) a)).toList();
List<AttachmentCompressionJob> compressionJobs = Stream.of(attachments).map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)).toList();
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList();
if (attachmentJobs.isEmpty()) {
jobManager.add(new PushMediaSendJob(messageId, destination));
} else {
jobManager.startChain(attachmentJobs)
jobManager.startChain(compressionJobs)
.then(attachmentJobs)
.then(new PushMediaSendJob(messageId, destination))
.enqueue();
}

View File

@ -135,7 +135,7 @@ public abstract class PushSendJob extends SendJob {
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)))
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);

View File

@ -46,14 +46,4 @@ public abstract class SendJob extends BaseJob {
database.markAttachmentUploaded(messageId, attachment);
}
}
protected List<Attachment> scaleAndStripExifFromAttachments(@NonNull MediaConstraints constraints,
@NonNull List<Attachment> attachments)
throws UndeliverableMessageException
{
MediaResizer mediaResizer = new MediaResizer(context, constraints);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
return mediaResizer.scaleAndStripExifToDatabase(attachmentDatabase, attachments);
}
}

View File

@ -16,39 +16,39 @@
*/
package org.thoughtcrime.securesms.sms;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@ -171,14 +171,17 @@ public class MessageSender {
mmsDatabase.endTransaction();
}
List<AttachmentUploadJob> uploadJobs = new ArrayList<>(databaseAttachments.size());
List<AttachmentCopyJob> copyJobs = new ArrayList<>(databaseAttachments.size());
List<Job> messageJobs = new ArrayList<>(databaseAttachments.get(0).size());
List<Job> compressionJobs = new ArrayList<>(databaseAttachments.size());
List<Job> uploadJobs = new ArrayList<>(databaseAttachments.size());
List<Job> copyJobs = new ArrayList<>(databaseAttachments.size());
List<Job> messageJobs = new ArrayList<>(databaseAttachments.get(0).size());
for (List<DatabaseAttachment> attachmentList : databaseAttachments) {
DatabaseAttachment source = attachmentList.get(0);
uploadJobs.add(AttachmentUploadJob.fromAttachment(source));
compressionJobs.add(AttachmentCompressionJob.fromAttachment(source, false, -1));
uploadJobs.add(new AttachmentUploadJob(source.getAttachmentId()));
if (attachmentList.size() > 1) {
AttachmentId sourceId = source.getAttachmentId();
@ -209,7 +212,10 @@ public class MessageSender {
copyJobs.size(),
messageJobs.size()));
JobManager.Chain chain = ApplicationContext.getInstance(context).getJobManager().startChain(uploadJobs);
JobManager.Chain chain = ApplicationContext.getInstance(context)
.getJobManager()
.startChain(compressionJobs)
.then(uploadJobs);
if (copyJobs.size() > 0) {
chain = chain.then(copyJobs);
@ -289,7 +295,7 @@ public class MessageSender {
private static void sendMms(Context context, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new MmsSendJob(messageId));
MmsSendJob.enqueue(context, jobManager, messageId);
}
private static boolean isPushTextSend(Context context, Recipient recipient, boolean keyExchange) {

View File

@ -116,7 +116,7 @@ public final class InMemoryTranscoder implements Closeable {
memoryFile = MemoryFileDescriptor.newMemoryFileDescriptor(context,
"TRANSCODE",
memoryFileEstimate);
memoryFileEstimate);
final long startTime = System.currentTimeMillis();
final FileDescriptor memoryFileFileDescriptor = memoryFile.getFileDescriptor();

View File

@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Application;
import org.junit.Test;
import org.thoughtcrime.securesms.jobmanager.Job;
import java.util.Map;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public final class JobManagerFactoriesTest {
@Test
public void PushContentReceiveJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("PushContentReceiveJob") instanceof FailingJob.Factory);
}
@Test
public void AttachmentUploadJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("AttachmentUploadJob") instanceof FailingJob.Factory);
}
@Test
public void MmsSendJob_is_retired() {
Map<String, Job.Factory> factories = JobManagerFactories.getJobFactories(mock(Application.class));
assertTrue(factories.get("MmsSendJob") instanceof FailingJob.Factory);
}
}