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 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}