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