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