AttachFileToConversationRunnable.java

  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}