diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 637f5e7ea58366844fc133601c5d011d41b785aa..032f11cf5cc35c2295ff0e075a3a8a8593cf591a 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -114,7 +114,7 @@ public final class Config { public static final boolean ENABLE_CAPS_CACHE = true; - public static final boolean DISABLE_HTTP_UPLOAD = false; + public static final boolean ENABLE_HTTP_UPLOAD = true; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = false; // log all stanzas that were received while the app is in background diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index f3e235573d4971934999f7a6529b88ddd3f9ff6b..9c009fcfc280ecb2600316661c6ff2d0bad3cef6 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.HttpUploadManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import java.util.ArrayList; import java.util.Collection; @@ -209,8 +210,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN))); } - public boolean httpUploadAvailable(long size) { - return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size); + public boolean httpUploadAvailable(long fileSize) { + return xmppConnection.getManager(HttpUploadManager.class).isAvailableForSize(fileSize); } public boolean httpUploadAvailable() { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 5d9d2955326b3ef016573f4d9a2d3f717ea99f22..a12436da2eead0df3ae21ca9aba8c32de9f29d8a 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -107,11 +107,11 @@ public class FileBackend { } public static boolean allFilesUnderSize( - Context context, List attachments, long max) { + Context context, List attachments, final Long max) { final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context) .equals("uncompressed"); - if (max <= 0) { + if (max == null || max <= 0) { Log.d(Config.LOGTAG, "server did not report max file size for http upload"); return true; // exception to be compatible with HTTP Upload < v0.2 } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0e5df61721920558628f29547ac9c26d97e8c423..11ff15622f7c0d987bb7f5e182406195087994f3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -127,6 +127,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.manager.HttpUploadManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; @@ -2229,9 +2230,9 @@ public class ConversationFragment extends XmppFragment if (!message.hasFileOnRemoteHost() && xmppConnection != null && conversation.getMode() == Conversational.MODE_SINGLE - && (!xmppConnection - .getFeatures() - .httpUpload(message.getFileParams().getSize()) + && (!conversation + .getAccount() + .httpUploadAvailable(message.getFileParams().getSize()) || forceP2P)) { activity.selectPresence( conversation, @@ -2917,9 +2918,14 @@ public class ConversationFragment extends XmppFragment mSendingPgpMessage.set(false); } - public long getMaxHttpUploadSize(Conversation conversation) { - final XmppConnection connection = conversation.getAccount().getXmppConnection(); - return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); + public Long getMaxHttpUploadSize(final Conversation conversation) { + + final var connection = conversation.getAccount().getXmppConnection(); + final var httpUploadService = connection.getManager(HttpUploadManager.class).getService(); + if (httpUploadService == null) { + return -1L; + } + return httpUploadService.getMaxFileSize(); } private void updateEditablity() { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index f6cf8d329758e9282fe31a6c3e1cb0313244ee03..1f2a58d6cd6d1ed08230e123d7e540cce92168ef 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -81,6 +81,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.manager.CarbonsManager; +import eu.siacs.conversations.xmpp.manager.HttpUploadManager; import eu.siacs.conversations.xmpp.manager.RegistrationManager; import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.stanza.Presence; @@ -1295,13 +1296,15 @@ public class EditAccountActivity extends OmemoActivity } else { this.binding.serverInfoPep.setText(R.string.server_info_unavailable); } - if (features.httpUpload(0)) { - final long maxFileSize = features.getMaxHttpUploadSize(); - if (maxFileSize > 0) { + final var httpUploadManager = connection.getManager(HttpUploadManager.class); + final var uploadService = httpUploadManager.getService(); + if (uploadService != null) { + final Long maxFileSize = uploadService.getMaxFileSize(); + if (maxFileSize == null) { + this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); + } else { this.binding.serverInfoHttpUpload.setText( UIHelper.filesizeToString(maxFileSize)); - } else { - this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); } } else { this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c77c5bd357dec7c40c95f4bc40247ed02f31deb5..06c0bd6c4714ebdfce53dc2aed25651d280ac36b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3063,58 +3063,6 @@ public class XmppConnection implements Runnable { return HttpUrl.parse(address); } - public boolean httpUpload(long fileSize) { - if (Config.DISABLE_HTTP_UPLOAD) { - return false; - } - final var result = - getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); - if (result == null) { - return false; - } - final long maxSize; - try { - maxSize = - Long.parseLong( - result.getValue() - .getServiceDiscoveryExtension( - Namespace.HTTP_UPLOAD, "max-file-size")); - } catch (final Exception e) { - return true; - } - if (fileSize <= maxSize) { - return true; - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": http upload is not available for files with" - + " size " - + fileSize - + " (max is " - + maxSize - + ")"); - return false; - } - } - - public long getMaxHttpUploadSize() { - final var result = - getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); - if (result == null) { - return -1; - } - try { - return Long.parseLong( - result.getValue() - .getServiceDiscoveryExtension( - Namespace.HTTP_UPLOAD, "max-file-size")); - } catch (final Exception e) { - return -1; - // ignored - } - } - public boolean stanzaIds() { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 568743911a0594741a40e83c4fe817e5550461dd..39085ccbd759acd548513f86e089775ac7031930 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.manager; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; @@ -520,6 +521,16 @@ public class AvatarManager extends AbstractManager { avifWriter.addBitmap(image); avifWriter.stop(3_000); } + var readCheck = BitmapFactory.decodeFile(randomFile.getAbsolutePath()); + if (readCheck == null) { + throw new AvifCompressionException("AVIF image was null after trying to decode"); + } + if (readCheck.getWidth() != image.getWidth() + || readCheck.getHeight() != image.getHeight()) { + readCheck.recycle(); + throw new AvifCompressionException("AVIF had wrong image bounds"); + } + readCheck.recycle(); return storeAsAvatar(randomFile, ImageFormat.AVIF, image.getHeight(), image.getWidth()); } @@ -535,34 +546,49 @@ public class AvatarManager extends AbstractManager { String.format("Could not move file to %s", avatarFile.getAbsolutePath())); } - private ListenableFuture> uploadAvatar(final Uri image) { + private ListenableFuture> uploadAvatar(final Uri image) { return Futures.transformAsync( hasAlphaChannel(image), hasAlphaChannel -> uploadAvatar(image, hasAlphaChannel), MoreExecutors.directExecutor()); } - private ListenableFuture> uploadAvatar( + private ListenableFuture> uploadAvatar( final Uri image, final boolean hasAlphaChannel) { final var avatarFutures = new ImmutableList.Builder>(); final ListenableFuture avatarThumbnailFuture; - final ListenableFuture avatarFuture; if (hasAlphaChannel) { avatarThumbnailFuture = resizeAndStoreAvatarAsync( image, Config.AVATAR_THUMBNAIL_SIZE / 2, ImageFormat.PNG); - avatarFuture = - resizeAndStoreAvatarAsync(image, Config.AVATAR_FULL_SIZE / 2, ImageFormat.PNG); } else { - final int autoAcceptFileSize = - context.getResources().getInteger(R.integer.auto_accept_filesize); avatarThumbnailFuture = resizeAndStoreAvatarAsync( image, Config.AVATAR_THUMBNAIL_SIZE, ImageFormat.JPEG, Config.AVATAR_THUMBNAIL_CHAR_LIMIT); + } + + final var uploadManager = getManager(HttpUploadManager.class); + + final var uploadService = uploadManager.getService(); + if (uploadService == null || !uploadService.supportsPurpose(Profile.class)) { + Log.d( + Config.LOGTAG, + getAccount().getJid() + ": 'profile' upload purpose not supported"); + return Futures.transform( + avatarThumbnailFuture, ImmutableList::of, MoreExecutors.directExecutor()); + } + + final ListenableFuture avatarFuture; + if (hasAlphaChannel) { + avatarFuture = + resizeAndStoreAvatarAsync(image, Config.AVATAR_FULL_SIZE / 2, ImageFormat.PNG); + } else { + final int autoAcceptFileSize = + context.getResources().getInteger(R.integer.auto_accept_filesize); avatarFuture = resizeAndStoreAvatarAsync( image, Config.AVATAR_FULL_SIZE, ImageFormat.JPEG, autoAcceptFileSize); @@ -589,7 +615,16 @@ public class AvatarManager extends AbstractManager { final var avatarAvifWithUrlFuture = Futures.transformAsync( avatarAvifFuture, this::upload, MoreExecutors.directExecutor()); - avatarFutures.add(avatarAvifWithUrlFuture); + final var caughtAvifWithUrlFuture = + Futures.catching( + avatarAvifWithUrlFuture, + Exception.class, + ex -> { + Log.d(Config.LOGTAG, "ignoring AVIF compression failure", ex); + return null; + }, + MoreExecutors.directExecutor()); + avatarFutures.add(caughtAvifWithUrlFuture); } } avatarFutures.add(avatarThumbnailFuture); @@ -597,7 +632,11 @@ public class AvatarManager extends AbstractManager { Futures.transformAsync(avatarFuture, this::upload, MoreExecutors.directExecutor()); avatarFutures.add(avatarWithUrlFuture); - return Futures.allAsList(avatarFutures.build()); + final var all = Futures.allAsList(avatarFutures.build()); + return Futures.transform( + all, + input -> Collections2.filter(input, Objects::nonNull), + MoreExecutors.directExecutor()); } private ListenableFuture hasAlphaChannel(final Uri image) { @@ -801,6 +840,12 @@ public class AvatarManager extends AbstractManager { AVATAR_COMPRESSION_EXECUTOR); } + private static final class AvifCompressionException extends IllegalStateException { + AvifCompressionException(final String message) { + super(message); + } + } + public enum ImageFormat { PNG, JPEG, diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java index ea33b9e4b9ca03ff1a53da1a7e66b879e201b396..ab65bd97bdc039b27576954bc97006d4e25ed207 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Longs; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -16,6 +17,8 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.ExtensionFactory; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.upload.Request; import im.conversations.android.xmpp.model.upload.purpose.Purpose; @@ -147,6 +150,15 @@ public class HttpUploadManager extends AbstractManager { MoreExecutors.directExecutor()); } + public Service getService() { + if (Config.ENABLE_HTTP_UPLOAD) { + final var entry = + getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); + return entry == null ? null : new Service(entry); + } + return null; + } + private static String convertFilename(final String name) { int pos = name.indexOf('.'); if (pos < 0) { @@ -165,6 +177,63 @@ public class HttpUploadManager extends AbstractManager { } } + public boolean isAvailableForSize(final long size) { + final var result = getManager(HttpUploadManager.class).getService(); + if (result == null) { + return false; + } + final Long maxSize = result.getMaxFileSize(); + if (maxSize == null) { + return true; + } + if (size <= maxSize) { + return true; + } else { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": http upload is not available for files with" + + " size " + + size + + " (max is " + + maxSize + + ")"); + return false; + } + } + + public static final class Service { + private final Map.Entry addressInfoQuery; + + public Service(final Map.Entry addressInfoQuery) { + this.addressInfoQuery = addressInfoQuery; + } + + public Jid getAddress() { + return this.addressInfoQuery.getKey(); + } + + public InfoQuery getInfoQuery() { + return this.addressInfoQuery.getValue(); + } + + public boolean supportsPurpose(final Class purpose) { + final var id = ExtensionFactory.id(purpose); + if (id == null) { + throw new IllegalStateException("Purpose has not been annotated as @XmlElement"); + } + final var feature = String.format("%s#%s", id.namespace, id.name); + return getInfoQuery().hasFeature(feature); + } + + public Long getMaxFileSize() { + final var value = + getInfoQuery() + .getServiceDiscoveryExtension(Namespace.HTTP_UPLOAD, "max-file-size"); + return value == null ? null : Longs.tryParse(value); + } + } + public static class Slot { public final HttpUrl put; public final HttpUrl get; diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index bbdecb6884fcb44f740b96e5794742d83a5d0bd7..a8cc527c781f3921e5d895528314aa70fca91d6f 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -39,7 +39,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { sosModified = false; } final boolean gainedFeature = - account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, features.httpUpload(0)); + account.setOption( + Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.httpUploadAvailable(0)); if (loggedInSuccessfully || gainedFeature || sosModified) { service.databaseBackend.updateAccount(account); }