Detailed changes
@@ -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
@@ -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() {
@@ -107,11 +107,11 @@ public class FileBackend {
}
public static boolean allFilesUnderSize(
- Context context, List<Attachment> attachments, long max) {
+ Context context, List<Attachment> 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
}
@@ -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() {
@@ -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);
@@ -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);
}
@@ -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<List<Info>> uploadAvatar(final Uri image) {
+ private ListenableFuture<Collection<Info>> uploadAvatar(final Uri image) {
return Futures.transformAsync(
hasAlphaChannel(image),
hasAlphaChannel -> uploadAvatar(image, hasAlphaChannel),
MoreExecutors.directExecutor());
}
- private ListenableFuture<List<Info>> uploadAvatar(
+ private ListenableFuture<Collection<Info>> uploadAvatar(
final Uri image, final boolean hasAlphaChannel) {
final var avatarFutures = new ImmutableList.Builder<ListenableFuture<Info>>();
final ListenableFuture<Info> avatarThumbnailFuture;
- final ListenableFuture<Info> 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<Info> 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<Boolean> 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,
@@ -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<Jid, InfoQuery> addressInfoQuery;
+
+ public Service(final Map.Entry<Jid, InfoQuery> addressInfoQuery) {
+ this.addressInfoQuery = addressInfoQuery;
+ }
+
+ public Jid getAddress() {
+ return this.addressInfoQuery.getKey();
+ }
+
+ public InfoQuery getInfoQuery() {
+ return this.addressInfoQuery.getValue();
+ }
+
+ public boolean supportsPurpose(final Class<? extends Purpose> 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;
@@ -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);
}