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