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.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.videoconverter.BadVideoException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; final class MediaResizer { @NonNull private final Context context; @NonNull private final MediaConstraints constraints; MediaResizer(@NonNull Context context, @NonNull MediaConstraints constraints) { this.context = context; this.constraints = constraints; } List scaleAndStripExifToDatabase(@NonNull AttachmentDatabase attachmentDatabase, @NonNull List attachments) throws UndeliverableMessageException { List 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 (IOException | MmsException | BadVideoException 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); } }