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