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);
67 callback.success(message);
68 });
69 } else if (path != null && !FileBackend.isPathBlacklisted(path)) {
70 message.setRelativeFilePath(path);
71 mXmppConnectionService.getFileBackend().updateFileParams(message);
72 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
73 mXmppConnectionService.getPgpEngine().encrypt(message, callback);
74 } else {
75 mXmppConnectionService.sendMessage(message);
76 callback.success(message);
77 }
78 } else {
79 try {
80 mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type);
81 mXmppConnectionService.getFileBackend().updateFileParams(message);
82 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
83 final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
84 if (pgpEngine != null) {
85 pgpEngine.encrypt(message, callback);
86 } else if (callback != null) {
87 callback.error(R.string.unable_to_connect_to_keychain, null);
88 }
89 } else {
90 mXmppConnectionService.sendMessage(message);
91 callback.success(message);
92 }
93 } catch (FileBackend.FileCopyException e) {
94 callback.error(e.getResId(), message);
95 }
96 }
97 }
98
99 private void processAsVideo() throws FileNotFoundException {
100 Log.d(Config.LOGTAG, "processing file as video");
101 mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification();
102 mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
103 final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
104 if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
105 Log.d(Config.LOGTAG, "created parent directory for video file");
106 }
107
108 final boolean highQuality = "720".equals(getVideoCompression());
109
110 final Future<Void> future;
111 try {
112 future = Transcoder.into(file.getAbsolutePath()).
113 addDataSource(mXmppConnectionService, uri)
114 .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P)
115 .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ)
116 .setListener(this)
117 .transcode();
118 } catch (final RuntimeException e) {
119 // transcode can already throw if there is an invalid file format or a platform bug
120 mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
121 processAsFile();
122 return;
123 }
124 try {
125 future.get();
126 } catch (final InterruptedException e) {
127 throw new AssertionError(e);
128 } catch (final ExecutionException e) {
129 if (e.getCause() instanceof Error) {
130 mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
131 processAsFile();
132 } else {
133 Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e);
134 }
135 }
136 }
137
138 @Override
139 public void onTranscodeProgress(double progress) {
140 final int p = (int) Math.round(progress * 100);
141 if (p > currentProgress) {
142 currentProgress = p;
143 mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message);
144 }
145 }
146
147 @Override
148 public void onTranscodeCompleted(int successCode) {
149 mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
150 final File file = mXmppConnectionService.getFileBackend().getFile(message);
151 long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
152 Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
153 if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
154 if (file.delete()) {
155 Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file");
156 processAsFile();
157 return;
158 } else {
159 Log.d(Config.LOGTAG, "unable to delete converted file");
160 }
161 }
162 mXmppConnectionService.getFileBackend().updateFileParams(message);
163 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
164 mXmppConnectionService.getPgpEngine().encrypt(message, callback);
165 } else {
166 mXmppConnectionService.sendMessage(message);
167 callback.success(message);
168 }
169 }
170
171 @Override
172 public void onTranscodeCanceled() {
173 mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
174 processAsFile();
175 }
176
177 @Override
178 public void onTranscodeFailed(@NonNull final Throwable exception) {
179 mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
180 Log.d(Config.LOGTAG, "video transcoding failed", exception);
181 processAsFile();
182 }
183
184 @Override
185 public void run() {
186 if (this.isVideoMessage()) {
187 try {
188 processAsVideo();
189 } catch (FileNotFoundException e) {
190 processAsFile();
191 }
192 } else {
193 processAsFile();
194 }
195 }
196
197 private String getVideoCompression() {
198 return getVideoCompression(mXmppConnectionService);
199 }
200
201 public static String getVideoCompression(final Context context) {
202 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
203 return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
204 }
205}