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