AttachFileToConversationRunnable.java

  1package eu.siacs.conversations.services;
  2
  3import android.content.Context;
  4import android.content.SharedPreferences;
  5import android.net.Uri;
  6import android.preference.PreferenceManager;
  7import android.util.Log;
  8
  9import androidx.annotation.NonNull;
 10
 11import com.otaliastudios.transcoder.Transcoder;
 12import com.otaliastudios.transcoder.TranscoderListener;
 13
 14import org.jetbrains.annotations.NotNull;
 15
 16import java.io.File;
 17import java.io.FileNotFoundException;
 18import java.util.Objects;
 19import java.util.concurrent.ExecutionException;
 20import java.util.concurrent.Future;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.R;
 24import eu.siacs.conversations.crypto.PgpEngine;
 25import eu.siacs.conversations.entities.DownloadableFile;
 26import eu.siacs.conversations.entities.Message;
 27import eu.siacs.conversations.persistance.FileBackend;
 28import eu.siacs.conversations.ui.UiCallback;
 29import eu.siacs.conversations.utils.MimeUtils;
 30import eu.siacs.conversations.utils.TranscoderStrategies;
 31
 32public class AttachFileToConversationRunnable implements Runnable, TranscoderListener {
 33
 34    private final XmppConnectionService mXmppConnectionService;
 35    private final Message message;
 36    private final Uri uri;
 37    private final String type;
 38    private final UiCallback<Message> callback;
 39    private final boolean isVideoMessage;
 40    private final long originalFileSize;
 41    private int currentProgress = -1;
 42
 43    AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback<Message> callback) {
 44        this.uri = uri;
 45        this.type = type;
 46        this.mXmppConnectionService = xmppConnectionService;
 47        this.message = message;
 48        this.callback = callback;
 49        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
 50        final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
 51        this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
 52        this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression());
 53    }
 54
 55    boolean isVideoMessage() {
 56        return this.isVideoMessage;
 57    }
 58
 59    private void processAsFile() {
 60        final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
 61        if (path != null && !FileBackend.isPathBlacklisted(path)) {
 62            message.setRelativeFilePath(path);
 63            mXmppConnectionService.getFileBackend().updateFileParams(message);
 64            if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 65                mXmppConnectionService.getPgpEngine().encrypt(message, callback);
 66            } else {
 67                mXmppConnectionService.sendMessage(message);
 68                callback.success(message);
 69            }
 70        } else {
 71            try {
 72                mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
 73                mXmppConnectionService.getFileBackend().updateFileParams(message);
 74                if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 75                    final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
 76                    if (pgpEngine != null) {
 77                        pgpEngine.encrypt(message, callback);
 78                    } else if (callback != null) {
 79                        callback.error(R.string.unable_to_connect_to_keychain, null);
 80                    }
 81                } else {
 82                    mXmppConnectionService.sendMessage(message);
 83                    callback.success(message);
 84                }
 85            } catch (FileBackend.FileCopyException e) {
 86                callback.error(e.getResId(), message);
 87            }
 88        }
 89    }
 90
 91    private void processAsVideo() throws FileNotFoundException {
 92        Log.d(Config.LOGTAG, "processing file as video");
 93        mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification();
 94        mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
 95        final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
 96        if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
 97            Log.d(Config.LOGTAG, "created parent directory for video file");
 98        }
 99
100        final boolean highQuality = "720".equals(getVideoCompression());
101
102        final Future<Void> future;
103        try {
104            future = Transcoder.into(file.getAbsolutePath()).
105                addDataSource(mXmppConnectionService, uri)
106                .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
107                .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
108                .setListener(this)
109                .transcode();
110        } catch (final RuntimeException e) {
111            // transcode can already throw if there is an invalid file format or a platform bug
112            mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
113            processAsFile();
114            return;
115        }
116        try {
117            future.get();
118        } catch (final InterruptedException e) {
119            throw new AssertionError(e);
120        } catch (final ExecutionException e) {
121            if (e.getCause() instanceof Error) {
122                mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
123                processAsFile();
124            } else {
125                Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
126            }
127        }
128    }
129
130    @Override
131    public void onTranscodeProgress(double progress) {
132        final int p = (int) Math.round(progress * 100);
133        if (p > currentProgress) {
134            currentProgress = p;
135            mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
136        }
137    }
138
139    @Override
140    public void onTranscodeCompleted(int successCode) {
141        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
142        final File file = mXmppConnectionService.getFileBackend().getFile(message);
143        long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
144        Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
145        if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
146            if (file.delete()) {
147                Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file");
148                processAsFile();
149                return;
150            } else {
151                Log.d(Config.LOGTAG, "unable to delete converted file");
152            }
153        }
154        mXmppConnectionService.getFileBackend().updateFileParams(message);
155        if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
156            mXmppConnectionService.getPgpEngine().encrypt(message, callback);
157        } else {
158            mXmppConnectionService.sendMessage(message);
159            callback.success(message);
160        }
161    }
162
163    @Override
164    public void onTranscodeCanceled() {
165        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
166        processAsFile();
167    }
168
169    @Override
170    public void onTranscodeFailed(@NonNull @NotNull Throwable exception) {
171        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
172        Log.d(Config.LOGTAG, "video transcoding failed", exception);
173        processAsFile();
174    }
175
176    @Override
177    public void run() {
178        if (this.isVideoMessage()) {
179            try {
180                processAsVideo();
181            } catch (FileNotFoundException e) {
182                processAsFile();
183            }
184        } else {
185            processAsFile();
186        }
187    }
188
189    private String getVideoCompression() {
190        return getVideoCompression(mXmppConnectionService);
191    }
192
193    public static String getVideoCompression(final Context context) {
194        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
195        return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
196    }
197}