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