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 eu.siacs.conversations.Config;
 15import eu.siacs.conversations.R;
 16import eu.siacs.conversations.crypto.PgpEngine;
 17import eu.siacs.conversations.entities.DownloadableFile;
 18import eu.siacs.conversations.entities.Message;
 19import eu.siacs.conversations.persistance.FileBackend;
 20import eu.siacs.conversations.ui.UiCallback;
 21import eu.siacs.conversations.utils.MimeUtils;
 22import eu.siacs.conversations.utils.TranscoderStrategies;
 23
 24import java.io.File;
 25import java.io.FileNotFoundException;
 26import java.util.Objects;
 27import java.util.concurrent.ExecutionException;
 28import java.util.concurrent.Future;
 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(
 42            XmppConnectionService xmppConnectionService,
 43            Uri uri,
 44            String type,
 45            Message message,
 46            UiCallback<Message> callback) {
 47        this.uri = uri;
 48        this.type = type;
 49        this.mXmppConnectionService = xmppConnectionService;
 50        this.message = message;
 51        this.callback = callback;
 52        final String mimeType =
 53                MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
 54        final int autoAcceptFileSize =
 55                mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
 56        this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri);
 57        this.isVideoMessage =
 58                (mimeType != null && mimeType.startsWith("video/"))
 59                        && originalFileSize > autoAcceptFileSize
 60                        && !"uncompressed".equals(getVideoCompression());
 61    }
 62
 63    boolean isVideoMessage() {
 64        return this.isVideoMessage;
 65    }
 66
 67    private void processAsFile() {
 68        final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri);
 69        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
 81                        .getFileBackend()
 82                        .copyFileToPrivateStorage(message, uri, type);
 83                mXmppConnectionService.getFileBackend().updateFileParams(message);
 84                if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 85                    final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine();
 86                    if (pgpEngine != null) {
 87                        pgpEngine.encrypt(message, callback);
 88                    } else if (callback != null) {
 89                        callback.error(R.string.unable_to_connect_to_keychain, null);
 90                    }
 91                } else {
 92                    mXmppConnectionService.sendMessage(message);
 93                    callback.success(message);
 94                }
 95            } catch (final FileBackend.FileCopyException e) {
 96                callback.error(e.getResId(), message);
 97            }
 98        }
 99    }
100
101    private void fallbackToProcessAsFile() {
102        final var file = mXmppConnectionService.getFileBackend().getFile(message);
103        if (file.exists() && file.delete()) {
104            Log.d(Config.LOGTAG, "deleted preexisting file " + file.getAbsolutePath());
105        }
106        XmppConnectionService.FILE_ATTACHMENT_EXECUTOR.execute(this::processAsFile);
107    }
108
109    private void processAsVideo() throws FileNotFoundException {
110        Log.d(Config.LOGTAG, "processing file as video");
111        mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification();
112        mXmppConnectionService
113                .getFileBackend()
114                .setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4"));
115        final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
116        if (Objects.requireNonNull(file.getParentFile()).mkdirs()) {
117            Log.d(Config.LOGTAG, "created parent directory for video file");
118        }
119
120        final boolean highQuality = "720".equals(getVideoCompression());
121
122        final Future<Void> future;
123        try {
124            future =
125                    Transcoder.into(file.getAbsolutePath())
126                            .addDataSource(mXmppConnectionService, uri)
127                            .setVideoTrackStrategy(
128                                    highQuality
129                                            ? TranscoderStrategies.VIDEO_720P
130                                            : TranscoderStrategies.VIDEO_360P)
131                            .setAudioTrackStrategy(
132                                    highQuality
133                                            ? TranscoderStrategies.AUDIO_HQ
134                                            : TranscoderStrategies.AUDIO_MQ)
135                            .setListener(this)
136                            .transcode();
137        } catch (final RuntimeException e) {
138            // transcode can already throw if there is an invalid file format or a platform bug
139            mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
140            fallbackToProcessAsFile();
141            return;
142        }
143        try {
144            future.get();
145        } catch (final InterruptedException e) {
146            throw new AssertionError(e);
147        } catch (final ExecutionException e) {
148            if (e.getCause() instanceof Error) {
149                mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
150                fallbackToProcessAsFile();
151            } else {
152                Log.d(Config.LOGTAG, "ignoring execution exception. Handled by onTranscodeFiled()");
153            }
154        }
155    }
156
157    @Override
158    public void onTranscodeProgress(double progress) {
159        final int p = (int) Math.round(progress * 100);
160        if (p > currentProgress) {
161            currentProgress = p;
162            mXmppConnectionService
163                    .getNotificationService()
164                    .updateFileAddingNotification(p, message);
165        }
166    }
167
168    @Override
169    public void onTranscodeCompleted(int successCode) {
170        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
171        final File file = mXmppConnectionService.getFileBackend().getFile(message);
172        long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize();
173        Log.d(
174                Config.LOGTAG,
175                "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize);
176        if (originalFileSize != 0 && convertedFileSize >= originalFileSize) {
177            if (file.delete()) {
178                Log.d(
179                        Config.LOGTAG,
180                        "original file size was smaller. deleting and processing as file");
181                fallbackToProcessAsFile();
182                return;
183            } else {
184                Log.d(Config.LOGTAG, "unable to delete converted file");
185            }
186        }
187        mXmppConnectionService.getFileBackend().updateFileParams(message);
188        if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
189            mXmppConnectionService.getPgpEngine().encrypt(message, callback);
190        } else {
191            mXmppConnectionService.sendMessage(message);
192            callback.success(message);
193        }
194    }
195
196    @Override
197    public void onTranscodeCanceled() {
198        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
199        fallbackToProcessAsFile();
200    }
201
202    @Override
203    public void onTranscodeFailed(@NonNull final Throwable exception) {
204        mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification();
205        Log.d(Config.LOGTAG, "video transcoding failed", exception);
206        fallbackToProcessAsFile();
207    }
208
209    @Override
210    public void run() {
211        if (this.isVideoMessage()) {
212            try {
213                processAsVideo();
214            } catch (final FileNotFoundException e) {
215                processAsFile();
216            }
217        } else {
218            processAsFile();
219        }
220    }
221
222    private String getVideoCompression() {
223        return getVideoCompression(mXmppConnectionService);
224    }
225
226    public static String getVideoCompression(final Context context) {
227        final SharedPreferences preferences =
228                PreferenceManager.getDefaultSharedPreferences(context);
229        return preferences.getString(
230                "video_compression", context.getResources().getString(R.string.video_compression));
231    }
232}