Detailed changes
@@ -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<NodeConfiguration.AccessModel> 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() {
@@ -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<NodeConfiguration.AccessModel> 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<byte[]> 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);
}
}
@@ -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<Data> getNodeConfiguration(final String node) {
+ return pubSubManager().getNodeConfiguration(pepService(), node);
+ }
+
public boolean hasPublishOptions() {
return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_PUBLISH_OPTIONS);
}
@@ -308,12 +308,19 @@ public class PubSubManager extends AbstractManager {
private ListenableFuture<Void> 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<Data> 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());
}
@@ -10,7 +10,7 @@ import java.util.Set;
public class NodeConfiguration implements Map<String, Object> {
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<String, Object> {
new NodeConfiguration(
new ImmutableMap.Builder<String, Object>()
.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<String, Object>()
.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<String, Object>()
.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<String, Object>()
.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<String, Object> {
public Set<Entry<String, Object>> entrySet() {
return this.delegate.entrySet();
}
+
+ public enum AccessModel {
+ OPEN,
+ WHITELIST,
+ PRESENCE
+ }
}
@@ -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() {
@@ -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) {