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}