diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 139c13379943957b8c5a08dab2dc8c1f5699b3a6..dadd83e64c0bc97c3e575709305c7f99b88f10cf 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -13,11 +13,15 @@ import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; import com.canhub.cropper.CropImageContract; import com.canhub.cropper.CropImageContractOptions; import com.canhub.cropper.CropImageOptions; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; @@ -25,14 +29,15 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.manager.AvatarManager; +import im.conversations.android.xmpp.NodeConfiguration; public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { - public static final int REQUEST_CHOOSE_PICTURE = 0x1337; - private ActivityPublishProfilePictureBinding binding; private Uri avatarUri; + private NodeConfiguration.AccessModel accessModel; private Uri defaultUri; private Account account; private boolean support = false; @@ -136,6 +141,10 @@ public class PublishProfilePictureActivity extends XmppActivity this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); if (savedInstanceState != null) { this.avatarUri = savedInstanceState.getParcelable("uri"); + final var accessModel = savedInstanceState.getString("access-model"); + if (accessModel != null) { + this.accessModel = NodeConfiguration.AccessModel.valueOf(accessModel); + } } } @@ -178,6 +187,9 @@ public class PublishProfilePictureActivity extends XmppActivity if (this.avatarUri != null) { outState.putParcelable("uri", this.avatarUri); } + if (this.accessModel != null) { + outState.putString("access-model", this.accessModel.toString()); + } super.onSaveInstanceState(outState); } @@ -209,18 +221,57 @@ public class PublishProfilePictureActivity extends XmppActivity @Override protected void onBackendConnected() { - this.account = extractAccount(getIntent()); - if (this.account != null) { - reloadAvatar(); + final var account = extractAccount(getIntent()); + this.account = account; + if (account != null) { + loadCurrentAccessModel(account); + reloadAvatar(account); } } + private void loadCurrentAccessModel(final Account account) { + binding.contactOnly.setVisibility(View.INVISIBLE); + final var currentPepAccessModel = getPepAccessModelOrCached(account); + Futures.addCallback( + currentPepAccessModel, + new FutureCallback<>() { + @Override + public void onSuccess(final NodeConfiguration.AccessModel result) { + accessModel = result; // cache for after rotation + Log.d(Config.LOGTAG, "current access model: " + result); + binding.contactOnly.setChecked( + result == NodeConfiguration.AccessModel.PRESENCE); + binding.contactOnly.jumpDrawablesToCurrentState(); + binding.contactOnly.setVisibility(View.VISIBLE); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not fetch access model", t); + binding.contactOnly.setChecked(false); + binding.contactOnly.setVisibility(View.VISIBLE); + } + }, + ContextCompat.getMainExecutor(getApplication())); + } + + private ListenableFuture getPepAccessModelOrCached( + final Account account) { + final var cached = this.accessModel; + if (cached != null) { + return Futures.immediateFuture(cached); + } + return account.getXmppConnection().getManager(AvatarManager.class).getPepAccessModel(); + } + private void reloadAvatar() { - this.support = - this.account.getXmppConnection() != null - && this.account.getXmppConnection().getFeatures().pep(); + reloadAvatar(this.account); + } + + private void reloadAvatar(final Account account) { + this.support = account.getXmppConnection().getFeatures().pep(); if (this.avatarUri == null) { - if (this.account.getAvatar() != null || this.defaultUri == null) { + if (account.getAvatar() != null || this.defaultUri == null) { loadImageIntoPreview(null); } else { this.avatarUri = this.defaultUri; @@ -308,6 +359,7 @@ public class PublishProfilePictureActivity extends XmppActivity final boolean status = enabled && !publishing; this.binding.publishButton.setText(publishing ? R.string.publishing : res); this.binding.publishButton.setEnabled(status); + this.binding.contactOnly.setEnabled(status); } public void refreshUiReal() { 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 39085ccbd759acd548513f86e089775ac7031930..6eebf7d76e1ad26bae8450f4ef0e6a50ea362108 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -8,7 +8,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.heifwriter.AvifWriter; import androidx.heifwriter.HeifWriter; +import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; @@ -108,6 +110,23 @@ public class AvatarManager extends AbstractManager { this.service = service; } + public ListenableFuture getPepAccessModel() { + final var nodeConfiguration = + getManager(PepManager.class).getNodeConfiguration(Namespace.AVATAR_DATA); + return Futures.transform( + nodeConfiguration, + data -> { + final var accessModel = data.getValue(NodeConfiguration.ACCESS_MODEL); + if (Strings.isNullOrEmpty(accessModel)) { + throw new IllegalStateException( + "Access model missing from node configuration"); + } + return NodeConfiguration.AccessModel.valueOf( + CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, accessModel)); + }, + MoreExecutors.directExecutor()); + } + private ListenableFuture fetch(final Jid address, final String itemId) { final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class); return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor()); @@ -476,8 +495,8 @@ public class AvatarManager extends AbstractManager { sha1, avatarFile.length(), ImageFormat.of(format).toContentType(), - image.getHeight(), - image.getWidth()); + image.getWidth(), + image.getHeight()); } throw new IllegalStateException( String.format("Could not move file to %s", avatarFile.getAbsolutePath())); @@ -501,7 +520,10 @@ public class AvatarManager extends AbstractManager { heifWriter.addBitmap(image); heifWriter.stop(3_000); } - return storeAsAvatar(randomFile, ImageFormat.HEIF, image.getHeight(), image.getWidth()); + final var width = image.getWidth(); + final var height = image.getHeight(); + checkDecoding(randomFile, ImageFormat.HEIF, width, height); + return storeAsAvatar(randomFile, ImageFormat.HEIF, width, height); } private Info resizeAndStoreAvatarAsAvif(final Bitmap image, final int quality) @@ -521,26 +543,33 @@ public class AvatarManager extends AbstractManager { avifWriter.addBitmap(image); avifWriter.stop(3_000); } + final var width = image.getWidth(); + final var height = image.getHeight(); + checkDecoding(randomFile, ImageFormat.AVIF, width, height); + return storeAsAvatar(randomFile, ImageFormat.AVIF, width, height); + } + + private void checkDecoding( + final File randomFile, final ImageFormat format, final int width, final int height) { var readCheck = BitmapFactory.decodeFile(randomFile.getAbsolutePath()); if (readCheck == null) { - throw new AvifCompressionException("AVIF image was null after trying to decode"); + throw new ImageCompressionException( + String.format("%s image was null after trying to decode", format)); } - if (readCheck.getWidth() != image.getWidth() - || readCheck.getHeight() != image.getHeight()) { + if (readCheck.getWidth() != width || readCheck.getHeight() != height) { readCheck.recycle(); - throw new AvifCompressionException("AVIF had wrong image bounds"); + throw new ImageCompressionException(String.format("%s had wrong image bounds", format)); } readCheck.recycle(); - return storeAsAvatar(randomFile, ImageFormat.AVIF, image.getHeight(), image.getWidth()); } private Info storeAsAvatar( - final File randomFile, final ImageFormat type, final int height, final int width) + final File randomFile, final ImageFormat type, final int width, final int height) throws IOException { final var sha1 = Files.asByteSource(randomFile).hash(Hashing.sha1()).toString(); final var avatarFile = FileBackend.getAvatarFile(context, sha1); if (moveAvatarIntoCache(randomFile, avatarFile)) { - return new Info(sha1, avatarFile.length(), type.toContentType(), height, width); + return new Info(sha1, avatarFile.length(), type.toContentType(), width, height); } throw new IllegalStateException( String.format("Could not move file to %s", avatarFile.getAbsolutePath())); @@ -840,8 +869,8 @@ public class AvatarManager extends AbstractManager { AVATAR_COMPRESSION_EXECUTOR); } - private static final class AvifCompressionException extends IllegalStateException { - AvifCompressionException(final String message) { + private static final class ImageCompressionException extends IllegalStateException { + ImageCompressionException(final String message) { super(message); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java index b77286c53d41fa02330a6bcfe14079b0c63948ce..7ee0dc504da6a81c63ec9a984908ec8d94f45765 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java @@ -9,6 +9,7 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.NodeConfiguration; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Map; @@ -55,6 +56,10 @@ public class PepManager extends AbstractManager { return Futures.transform(future, iq -> null, MoreExecutors.directExecutor()); } + public ListenableFuture getNodeConfiguration(final String node) { + return pubSubManager().getNodeConfiguration(pepService(), node); + } + public boolean hasPublishOptions() { return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_PUBLISH_OPTIONS); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java index 08bf5ab92fac05016c81450d1bea169a47691edb..f2c4e14c396b9dde4df320a93434eea837bf70d5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -308,12 +308,19 @@ public class PubSubManager extends AbstractManager { private ListenableFuture reconfigureNode( final Jid address, final String node, final NodeConfiguration nodeConfiguration) { + return Futures.transformAsync( + getNodeConfiguration(address, node), + data -> setNodeConfiguration(address, node, data.submit(nodeConfiguration)), + MoreExecutors.directExecutor()); + } + + public ListenableFuture getNodeConfiguration(final Jid address, final String node) { final Iq iq = new Iq(Iq.Type.GET); iq.setTo(address); final var pubSub = iq.addExtension(new PubSubOwner()); final var configure = pubSub.addExtension(new Configure()); configure.setNode(node); - return Futures.transformAsync( + return Futures.transform( connection.sendIqPacket(iq), result -> { final var pubSubOwnerResult = result.getExtension(PubSubOwner.class); @@ -325,8 +332,7 @@ public class PubSubManager extends AbstractManager { throw new IllegalStateException( "No configuration found in configuration request result"); } - final var data = configureResult.getData(); - return setNodeConfiguration(address, node, data.submit(nodeConfiguration)); + return configureResult.getData(); }, MoreExecutors.directExecutor()); } diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java index e96e453e200547056d0ce45837d63265d3997f97..3c3f1bf53cc68e41f1d74e3543a29c906ed046d9 100644 --- a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java +++ b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java @@ -10,7 +10,7 @@ import java.util.Set; public class NodeConfiguration implements Map { private static final String PERSIST_ITEMS = "pubsub#persist_items"; - private static final String ACCESS_MODEL = "pubsub#access_model"; + public static final String ACCESS_MODEL = "pubsub#access_model"; private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item"; private static final String MAX_ITEMS = "pubsub#max_items"; private static final String NOTIFY_DELETE = "pubsub#notify_delete"; @@ -20,25 +20,25 @@ public class NodeConfiguration implements Map { new NodeConfiguration( new ImmutableMap.Builder() .put(PERSIST_ITEMS, Boolean.TRUE) - .put(ACCESS_MODEL, "open") + .put(ACCESS_MODEL, AccessModel.OPEN) .build()); public static final NodeConfiguration PRESENCE = new NodeConfiguration( new ImmutableMap.Builder() .put(PERSIST_ITEMS, Boolean.TRUE) - .put(ACCESS_MODEL, "presence") + .put(ACCESS_MODEL, AccessModel.PRESENCE) .build()); public static final NodeConfiguration WHITELIST = new NodeConfiguration( new ImmutableMap.Builder() .put(PERSIST_ITEMS, Boolean.TRUE) - .put(ACCESS_MODEL, "whitelist") + .put(ACCESS_MODEL, AccessModel.WHITELIST) .build()); public static final NodeConfiguration WHITELIST_MAX_ITEMS = new NodeConfiguration( new ImmutableMap.Builder() .put(PERSIST_ITEMS, Boolean.TRUE) - .put(ACCESS_MODEL, "whitelist") + .put(ACCESS_MODEL, AccessModel.WHITELIST) .put(SEND_LAST_PUBLISHED_ITEM, "never") .put(MAX_ITEMS, "max") .put(NOTIFY_DELETE, Boolean.TRUE) @@ -115,4 +115,10 @@ public class NodeConfiguration implements Map { public Set> entrySet() { return this.delegate.entrySet(); } + + public enum AccessModel { + OPEN, + WHITELIST, + PRESENCE + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java index 446c04486530ac746e01228c427b5ed45a50a4bf..40fd4d0b7af131e41fadfc2689f7f4c20e42dc76 100644 --- a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java @@ -17,14 +17,14 @@ public class Info extends Extension { final String id, final long bytes, final String type, - final int height, - final int width) { + final int width, + final int height) { this(); this.setId(id); this.setBytes(bytes); this.setType(type); - this.setHeight(height); this.setWidth(width); + this.setHeight(height); } public long getHeight() { diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index d0d546b37bdf5f9b944966828b9719aeb3db6923..806c79af0386748065c9be17b8502a4b804d2b0e 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -1,5 +1,6 @@ package im.conversations.android.xmpp.model.data; +import com.google.common.base.CaseFormat; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import im.conversations.android.annotation.XmlElement; @@ -67,6 +68,9 @@ public class Data extends Extension { final var valueExtension = field.addExtension(new Value()); if (value instanceof String) { valueExtension.setContent((String) value); + } else if (value instanceof Enum e) { + valueExtension.setContent( + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString())); } else if (value instanceof Integer) { valueExtension.setContent(String.valueOf(value)); } else if (value instanceof Boolean) {