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