Detailed changes
@@ -0,0 +1,42 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.Collection;
+import java.util.List;
+
+public class FutureMerger {
+
+ public static <T> ListenableFuture<List<T>> successfulAsList(
+ final Collection<ListenableFuture<List<T>>> futures) {
+ return Futures.transform(
+ Futures.successfulAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder<T>();
+ for (final Collection<T> list : lists) {
+ if (list == null) {
+ continue;
+ }
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public static <T> ListenableFuture<List<T>> allAsList(
+ final Collection<ListenableFuture<Collection<T>>> futures) {
+ return Futures.transform(
+ Futures.allAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder<T>();
+ for (final Collection<T> list : lists) {
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,100 @@
+package de.gultsch.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class IntMap<E> implements Map<E, Integer> {
+
+ private final ImmutableMap<E, Integer> inner;
+
+ public IntMap(ImmutableMap<E, Integer> inner) {
+ this.inner = inner;
+ }
+
+ @Override
+ public int size() {
+ return this.inner.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.inner.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(@Nullable Object key) {
+ return this.inner.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(@Nullable Object value) {
+ return this.inner.containsValue(value);
+ }
+
+ @Nullable
+ @Override
+ public Integer get(@Nullable Object key) {
+ return this.inner.get(key);
+ }
+
+ public int getInt(@Nullable E key) {
+ final var value = this.inner.get(key);
+ return value == null ? Integer.MIN_VALUE : value;
+ }
+
+ @Nullable
+ @Override
+ public Integer put(E key, Integer value) {
+ return this.inner.put(key, value);
+ }
+
+ @Nullable
+ @Override
+ public Integer remove(@Nullable Object key) {
+ return this.inner.remove(key);
+ }
+
+ @Override
+ public void putAll(@NonNull Map<? extends E, ? extends Integer> m) {
+ this.inner.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ this.inner.clear();
+ }
+
+ @NonNull
+ @Override
+ public Set<E> keySet() {
+ return this.inner.keySet();
+ }
+
+ @NonNull
+ @Override
+ public Collection<Integer> values() {
+ return this.inner.values();
+ }
+
+ @NonNull
+ @Override
+ public Set<Entry<E, Integer>> entrySet() {
+ return this.inner.entrySet();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof IntMap<?> intMap)) return false;
+ return Objects.equal(inner, intMap.inner);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(inner);
+ }
+}
@@ -80,10 +80,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public static final String KEY_SOS_URL = "sos_url";
public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
protected final JSONObject keys;
- public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
- public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
- public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
- public final Set<Conversation> inProgressConferencePings = new HashSet<>();
protected Jid jid;
protected String password;
protected int options = 0;
@@ -10,7 +10,6 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.model.bookmark.Storage;
-import im.conversations.android.xmpp.model.bookmark2.Conference;
import im.conversations.android.xmpp.model.bookmark2.Extensions;
import java.lang.ref.WeakReference;
import java.util.Collections;
@@ -33,7 +32,7 @@ public class Bookmark extends Element implements ListItem {
this.account = account;
}
- private Bookmark(Account account) {
+ public Bookmark(Account account) {
super("conference");
this.account = account;
}
@@ -72,31 +71,6 @@ public class Bookmark extends Element implements ListItem {
return bookmark;
}
- public static Bookmark parseFromItem(
- final String id, final Conference conference, final Account account) {
- if (id == null || conference == null) {
- return null;
- }
- final Bookmark bookmark = new Bookmark(account);
- bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
- // TODO verify that we only use bare jids and ignore full jids
- if (bookmark.jid == null) {
- return null;
- }
-
- // TODO use proper API
-
- bookmark.setBookmarkName(conference.getAttribute("name"));
- bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
- bookmark.setNick(conference.findChildContent("nick"));
- bookmark.setPassword(conference.findChildContent("password"));
- final var extensions = conference.getExtensions();
- if (extensions != null) {
- bookmark.extensions = conference.getExtensions();
- }
- return bookmark;
- }
-
public Extensions getExtensions() {
return extensions;
}
@@ -260,4 +234,8 @@ public class Bookmark extends Element implements ListItem {
public String getAvatarName() {
return getDisplayName();
}
+
+ public void setExtensions(Extensions extensions) {
+ this.extensions = extensions;
+ }
}
@@ -1,12 +1,12 @@
package eu.siacs.conversations.entities;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import de.gultsch.common.IntMap;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.utils.JidHelper;
@@ -17,18 +17,39 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
import im.conversations.android.xmpp.model.data.Data;
import im.conversations.android.xmpp.model.data.Field;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Item;
+import im.conversations.android.xmpp.model.muc.Role;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Objects;
import java.util.Set;
public class MucOptions {
+ private static final IntMap<Affiliation> AFFILIATION_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder<Affiliation, Integer>()
+ .put(Affiliation.OWNER, 4)
+ .put(Affiliation.ADMIN, 3)
+ .put(Affiliation.MEMBER, 2)
+ .put(Affiliation.NONE, 1)
+ .put(Affiliation.OUTCAST, 0)
+ .build());
+
+ private static final IntMap<Role> ROLE_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder<Role, Integer>()
+ .put(Role.MODERATOR, 3)
+ .put(Role.PARTICIPANT, 2)
+ .put(Role.VISITOR, 1)
+ .put(Role.NONE, 0)
+ .build());
+
public static final String STATUS_CODE_SELF_PRESENCE = "110";
public static final String STATUS_CODE_ROOM_CREATED = "201";
public static final String STATUS_CODE_BANNED = "301";
@@ -38,6 +59,7 @@ public class MucOptions {
public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
public static final String STATUS_CODE_SHUTDOWN = "332";
public static final String STATUS_CODE_TECHNICAL_REASONS = "333";
+ // TODO this should be a list
private final Set<User> users = new HashSet<>();
private final Conversation conversation;
public OnRenameListener onRenameListener = null;
@@ -53,8 +75,8 @@ public class MucOptions {
this.account = conversation.getAccount();
this.conversation = conversation;
this.self = new User(this, createJoinJid(getProposedNick()));
- this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
- this.self.role = Role.of(conversation.getAttribute("role"));
+ this.self.affiliation = Item.affiliationOrNone(conversation.getAttribute("affiliation"));
+ this.self.role = Item.roleOrNone(conversation.getAttribute("role"));
}
public Account getAccount() {
@@ -74,7 +96,8 @@ public class MucOptions {
synchronized (users) {
if (user != null && user.getRole() == Role.NONE) {
users.remove(user);
- if (affiliation.ranks(Affiliation.MEMBER)) {
+ if (AFFILIATION_RANKS.getInt(affiliation)
+ >= AFFILIATION_RANKS.getInt(Affiliation.MEMBER)) {
user.affiliation = affiliation;
users.add(user);
}
@@ -82,8 +105,8 @@ public class MucOptions {
}
}
- public void flagNoAutoPushConfiguration() {
- mAutoPushConfiguration = false;
+ public void setAutoPushConfiguration(final boolean auto) {
+ this.mAutoPushConfiguration = auto;
}
public boolean autoPushConfiguration() {
@@ -176,7 +199,7 @@ public class MucOptions {
public boolean canInvite() {
final boolean hasPermission =
- !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
+ !membersOnly() || self.ranks(Role.MODERATOR) || allowInvites();
return hasPermission && online();
}
@@ -190,7 +213,7 @@ public class MucOptions {
}
public boolean canChangeSubject() {
- return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject();
+ return self.ranks(Role.MODERATOR) || participantsCanChangeSubject();
}
public boolean participantsCanChangeSubject() {
@@ -216,9 +239,9 @@ public class MucOptions {
if ("anyone".equals(field.getValue())) {
return true;
} else if ("participants".equals(field.getValue())) {
- return self.getRole().ranks(Role.PARTICIPANT);
+ return self.ranks(Role.PARTICIPANT);
} else if ("moderators".equals(field.getValue())) {
- return self.getRole().ranks(Role.MODERATOR);
+ return self.ranks(Role.MODERATOR);
} else {
return false;
}
@@ -232,7 +255,7 @@ public class MucOptions {
}
public boolean participating() {
- return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
+ return self.ranks(Role.PARTICIPANT) || !moderated();
}
public boolean membersOnly() {
@@ -283,7 +306,7 @@ public class MucOptions {
user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
if (membersOnly()
&& nonanonymous()
- && user.affiliation.ranks(Affiliation.MEMBER)
+ && user.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !realJidInMuc
&& !self) {
@@ -332,8 +355,8 @@ public class MucOptions {
isOnline
&& user.getFullJid() != null
&& user.getFullJid().equals(self.getFullJid());
- if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
- && user.getAffiliation().outranks(Affiliation.OUTCAST)
+ if ((!membersOnly() || user.ranks(Affiliation.MEMBER))
+ && user.outranks(Affiliation.OUTCAST)
&& !fullJidIsSelf) {
this.users.add(user);
return !realJidFound && user.realJid != null;
@@ -446,8 +469,7 @@ public class MucOptions {
synchronized (users) {
ArrayList<User> users = new ArrayList<>();
for (User user : this.users) {
- if (!user.isDomain()
- && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) {
+ if (!user.isDomain() && (includeOffline || user.ranks(Role.PARTICIPANT))) {
users.add(user);
}
}
@@ -725,7 +747,7 @@ public class MucOptions {
ArrayList<Jid> members = new ArrayList<>();
synchronized (users) {
for (User user : users) {
- if (user.affiliation.ranks(Affiliation.MEMBER)
+ if (user.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !user.realJid
.asBareJid()
@@ -738,90 +760,6 @@ public class MucOptions {
return members;
}
- public enum Affiliation {
- OWNER(4, R.string.owner),
- ADMIN(3, R.string.admin),
- MEMBER(2, R.string.member),
- OUTCAST(0, R.string.outcast),
- NONE(1, R.string.no_affiliation);
-
- private final int resId;
- private final int rank;
-
- Affiliation(int rank, int resId) {
- this.resId = resId;
- this.rank = rank;
- }
-
- public static Affiliation of(@Nullable String value) {
- if (value == null) {
- return NONE;
- }
- try {
- return Affiliation.valueOf(value.toUpperCase(Locale.US));
- } catch (IllegalArgumentException e) {
- return NONE;
- }
- }
-
- public int getResId() {
- return resId;
- }
-
- @Override
- @NonNull
- public String toString() {
- return name().toLowerCase(Locale.US);
- }
-
- public boolean outranks(Affiliation affiliation) {
- return rank > affiliation.rank;
- }
-
- public boolean ranks(Affiliation affiliation) {
- return rank >= affiliation.rank;
- }
- }
-
- public enum Role {
- MODERATOR(R.string.moderator, 3),
- VISITOR(R.string.visitor, 1),
- PARTICIPANT(R.string.participant, 2),
- NONE(R.string.no_role, 0);
-
- private final int resId;
- private final int rank;
-
- Role(int resId, int rank) {
- this.resId = resId;
- this.rank = rank;
- }
-
- public static Role of(@Nullable String value) {
- if (value == null) {
- return NONE;
- }
- try {
- return Role.valueOf(value.toUpperCase(Locale.US));
- } catch (IllegalArgumentException e) {
- return NONE;
- }
- }
-
- public int getResId() {
- return resId;
- }
-
- @Override
- public String toString() {
- return name().toLowerCase(Locale.US);
- }
-
- public boolean ranks(Role role) {
- return rank >= role.rank;
- }
- }
-
public enum Error {
NO_RESPONSE,
SERVER_NOT_FOUND,
@@ -873,16 +811,16 @@ public class MucOptions {
return this.role;
}
- public void setRole(String role) {
- this.role = Role.of(role);
+ public void setRole(final Role role) {
+ this.role = role;
}
public Affiliation getAffiliation() {
return this.affiliation;
}
- public void setAffiliation(String affiliation) {
- this.affiliation = Affiliation.of(affiliation);
+ public void setAffiliation(final Affiliation affiliation) {
+ this.affiliation = affiliation;
}
public long getPgpKeyId() {
@@ -941,6 +879,10 @@ public class MucOptions {
return options.getAccount();
}
+ public MucOptions getMucOptions() {
+ return this.options;
+ }
+
public Conversation getConversation() {
return options.getConversation();
}
@@ -992,9 +934,9 @@ public class MucOptions {
@Override
public int compareTo(@NonNull User another) {
- if (another.getAffiliation().outranks(getAffiliation())) {
+ if (another.outranks(getAffiliation())) {
return 1;
- } else if (getAffiliation().outranks(another.getAffiliation())) {
+ } else if (outranks(another.getAffiliation())) {
return -1;
} else {
return getComparableName().compareToIgnoreCase(another.getComparableName());
@@ -1045,5 +987,19 @@ public class MucOptions {
public String getOccupantId() {
return this.occupantId;
}
+
+ public boolean ranks(final Role role) {
+ return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role);
+ }
+
+ public boolean ranks(final Affiliation affiliation) {
+ return AFFILIATION_RANKS.getInt(this.affiliation)
+ >= AFFILIATION_RANKS.getInt(affiliation);
+ }
+
+ public boolean outranks(final Affiliation affiliation) {
+ return AFFILIATION_RANKS.getInt(this.affiliation)
+ > AFFILIATION_RANKS.getInt(affiliation);
+ }
}
}
@@ -5,7 +5,6 @@ import android.util.Base64;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
@@ -15,8 +14,6 @@ import eu.siacs.conversations.xmpp.forms.Data;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Set;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
@@ -57,13 +54,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq deleteNode(final String node) {
- final var packet = new Iq(Iq.Type.SET);
- final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
- pubsub.addChild("delete").setAttribute("node", node);
- return packet;
- }
-
public Iq retrieveDeviceIds(final Jid to) {
final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if (to != null) {
@@ -152,7 +142,7 @@ public class IqGenerator extends AbstractGenerator {
public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final Iq packet = new Iq(Iq.Type.SET);
- final Element query = packet.query(mam.version.namespace);
+ final Element query = packet.addChild("query", mam.version.namespace);
query.setAttribute("queryid", mam.getQueryId());
final Data data = new Data();
data.setFormType(mam.version.namespace);
@@ -181,35 +171,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
- List<Jid> jids = new ArrayList<>();
- jids.add(jid);
- return changeAffiliation(conference, jids, affiliation);
- }
-
- public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(conference.getJid().asBareJid());
- packet.setFrom(conference.getAccount().getJid());
- Element query = packet.query("http://jabber.org/protocol/muc#admin");
- for (Jid jid : jids) {
- Element item = query.addChild("item");
- item.setAttribute("jid", jid);
- item.setAttribute("affiliation", affiliation);
- }
- return packet;
- }
-
- public Iq changeRole(Conversation conference, String nick, String role) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(conference.getJid().asBareJid());
- packet.setFrom(conference.getAccount().getJid());
- Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
- item.setAttribute("nick", nick);
- item.setAttribute("role", role);
- return packet;
- }
-
public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
return pushTokenToAppServer(appServer, token, deviceId, null);
}
@@ -268,42 +229,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq queryAffiliation(Conversation conversation, String affiliation) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(conversation.getJid().asBareJid());
- packet.query("http://jabber.org/protocol/muc#admin")
- .addChild("item")
- .setAttribute("affiliation", affiliation);
- return packet;
- }
-
- public static Bundle defaultGroupChatConfiguration() {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_membersonly", "1");
- options.putString("muc#roomconfig_publicroom", "0");
- options.putString("muc#roomconfig_whois", "anyone");
- options.putString("muc#roomconfig_changesubject", "0");
- options.putString("muc#roomconfig_allowinvites", "0");
- options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
- options.putString("mam", "1"); // ejabberd community
- options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
- return options;
- }
-
- public static Bundle defaultChannelConfiguration() {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_membersonly", "0");
- options.putString("muc#roomconfig_publicroom", "1");
- options.putString("muc#roomconfig_whois", "moderators");
- options.putString("muc#roomconfig_changesubject", "0");
- options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
- options.putString("mam", "1"); // ejabberd community
- options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
- return options;
- }
-
public Iq requestPubsubConfiguration(Jid jid, String node) {
return pubsubConfiguration(jid, node, null);
}
@@ -17,6 +17,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import im.conversations.android.xmpp.model.correction.Replace;
+import im.conversations.android.xmpp.model.hints.NoStore;
import im.conversations.android.xmpp.model.hints.Store;
import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions;
@@ -95,7 +96,7 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setAxolotlMessage(axolotlMessage.toElement());
packet.setBody(OMEMO_FALLBACK_MESSAGE);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
packet.addChild("encryption", "urn:xmpp:eme:0")
.setAttribute("name", "OMEMO")
.setAttribute("namespace", AxolotlService.PEP_PREFIX);
@@ -109,7 +110,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(to);
packet.setAxolotlMessage(axolotlMessage.toElement());
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addChild(new Store());
return packet;
}
@@ -161,8 +162,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
- packet.addChild("no-store", "urn:xmpp:hints");
- packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store*
+ packet.addExtension(new NoStore());
return packet;
}
@@ -188,7 +188,7 @@ public class MessageGenerator extends AbstractGenerator {
} else {
displayed.setAttribute("id", message.getRemoteMsgId());
}
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -209,52 +209,7 @@ public class MessageGenerator extends AbstractGenerator {
for (final String ourReaction : ourReactions) {
reactions.addExtension(new Reaction(ourReaction));
}
- packet.addChild("store", "urn:xmpp:hints");
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(
- Conversation conversation, String subject) {
- im.conversations.android.xmpp.model.stanza.Message packet =
- new im.conversations.android.xmpp.model.stanza.Message();
- packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
- packet.setTo(conversation.getJid().asBareJid());
- packet.addChild("subject").setContent(subject);
- packet.setFrom(conversation.getAccount().getJid().asBareJid());
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Message directInvite(
- final Conversation conversation, final Jid contact) {
- im.conversations.android.xmpp.model.stanza.Message packet =
- new im.conversations.android.xmpp.model.stanza.Message();
- packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
- packet.setTo(contact);
- packet.setFrom(conversation.getAccount().getJid());
- Element x = packet.addChild("x", "jabber:x:conference");
- x.setAttribute("jid", conversation.getJid().asBareJid());
- String password = conversation.getMucOptions().getPassword();
- if (password != null) {
- x.setAttribute("password", password);
- }
- if (contact.isFullJid()) {
- packet.addChild("no-store", "urn:xmpp:hints");
- packet.addChild("no-copy", "urn:xmpp:hints");
- }
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Message invite(
- final Conversation conversation, final Jid contact) {
- final var packet = new im.conversations.android.xmpp.model.stanza.Message();
- packet.setTo(conversation.getJid().asBareJid());
- packet.setFrom(conversation.getAccount().getJid());
- Element x = new Element("x");
- x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
- Element invite = new Element("invite");
- invite.setAttribute("to", contact.asBareJid());
- x.addChild(invite);
- packet.addChild(x);
+ packet.addExtension(new Store());
return packet;
}
@@ -277,7 +232,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setFrom(account.getJid());
packet.setTo(to);
packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -291,7 +246,7 @@ public class MessageGenerator extends AbstractGenerator {
finish.setAttribute("id", sessionId);
final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
reasonElement.addChild(reason.toString());
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -311,7 +266,7 @@ public class MessageGenerator extends AbstractGenerator {
.setAttribute("media", media.toString());
}
packet.addChild("request", "urn:xmpp:receipts");
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -326,7 +281,7 @@ public class MessageGenerator extends AbstractGenerator {
final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -341,7 +296,7 @@ public class MessageGenerator extends AbstractGenerator {
final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
}
@@ -133,7 +133,7 @@ public abstract class AbstractParser extends XmppConnection.Delegate {
return item.findChildContent("data", "urn:xmpp:avatar:data");
}
- public static MucOptions.User parseItem(Conversation conference, Element item) {
+ public static MucOptions.User parseItem(final Conversation conference, final Element item) {
return parseItem(conference, item, null);
}
@@ -156,8 +156,8 @@ public abstract class AbstractParser extends XmppConnection.Delegate {
if (Jid.Invalid.isValid(realJid)) {
user.setRealJid(realJid);
}
- user.setAffiliation(affiliation);
- user.setRole(role);
+ // user.setAffiliation(affiliation);
+ // user.setRole(role);
return user;
}
@@ -34,15 +34,18 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.PubSubManager;
import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.axolotl.Encrypted;
import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent;
+import im.conversations.android.xmpp.model.conference.DirectInvite;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.forward.Forwarded;
import im.conversations.android.xmpp.model.markers.Displayed;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import im.conversations.android.xmpp.model.oob.OutOfBandData;
import im.conversations.android.xmpp.model.pubsub.event.Event;
@@ -212,7 +215,7 @@ public class MessageParser extends AbstractParser
return null;
}
- private Invite extractInvite(final Element message) {
+ private Invite extractInvite(final im.conversations.android.xmpp.model.stanza.Message message) {
final Element mucUser = message.findChild("x", Namespace.MUC_USER);
if (mucUser != null) {
final Element invite = mucUser.findChild("invite");
@@ -231,7 +234,7 @@ public class MessageParser extends AbstractParser
return new Invite(room, password, false, from);
}
}
- final Element conference = message.findChild("x", "jabber:x:conference");
+ final var conference = message.getExtension(DirectInvite.class);
if (conference != null) {
Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
@@ -303,7 +306,7 @@ public class MessageParser extends AbstractParser
+ ": received ping worthy error for seemingly online"
+ " muc at "
+ from);
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
}
}
}
@@ -347,6 +350,12 @@ public class MessageParser extends AbstractParser
packet = f.first;
serverMsgId = result.getAttribute("id");
query.incrementMessageCount();
+
+ if (query.isImplausibleFrom(packet.getFrom())) {
+ Log.d(Config.LOGTAG, "found implausible from in MUC MAM archive");
+ return;
+ }
+
if (handleErrorMessage(account, packet)) {
return;
}
@@ -520,7 +529,7 @@ public class MessageParser extends AbstractParser
|| mucUserElement != null
|| connection
.getMucServersWithholdAccount()
- .contains(counterpart.getDomain().toString());
+ .contains(counterpart.getDomain());
final Conversation conversation =
mXmppConnectionService.findOrCreateConversation(
account,
@@ -1009,69 +1018,11 @@ public class MessageParser extends AbstractParser
}
}
}
- if (conversation != null
- && mucUserElement != null
- && Jid.Invalid.hasValidFrom(packet)
- && from.isBareJid()) {
- for (Element child : mucUserElement.getChildren()) {
- if ("status".equals(child.getName())) {
- try {
- int code = Integer.parseInt(child.getAttribute("code"));
- if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
- mXmppConnectionService.fetchConferenceConfiguration(conversation);
- break;
- }
- } catch (Exception e) {
- // ignored
- }
- } else if ("item".equals(child.getName())) {
- final var user = AbstractParser.parseItem(conversation, child);
- Log.d(
- Config.LOGTAG,
- account.getJid()
- + ": changing affiliation for "
- + user.getRealJid()
- + " to "
- + user.getAffiliation()
- + " in "
- + conversation.getJid().asBareJid());
- if (!user.realJidMatchesAccount()) {
- final var mucOptions = conversation.getMucOptions();
- final boolean isNew = mucOptions.updateUser(user);
- final var avatarService = mXmppConnectionService.getAvatarService();
- if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
- avatarService.clear(mucOptions);
- }
- avatarService.clear(user);
- mXmppConnectionService.updateMucRosterUi();
- mXmppConnectionService.updateConversationUi();
- Contact contact = user.getContact();
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
- Jid jid = user.getRealJid();
- List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
- if (cryptoTargets.remove(user.getRealJid())) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removed "
- + jid
- + " from crypto targets of "
- + conversation.getName());
- conversation.setAcceptedCryptoTargets(cryptoTargets);
- mXmppConnectionService.updateConversation(conversation);
- }
- } else if (isNew
- && user.getRealJid() != null
- && conversation.getMucOptions().isPrivateAndNonAnonymous()
- && (contact == null || !contact.mutualPresenceSubscription())
- && account.getAxolotlService()
- .hasEmptyDeviceList(user.getRealJid())) {
- account.getAxolotlService().fetchDeviceIds(user.getRealJid());
- }
- }
- }
- }
+
+ if (original.hasExtension(MucUser.class)) {
+ getManager(MultiUserChatManager.class).handleStatusMessage(original);
}
+
if (!isTypeGroupChat) {
for (Element child : packet.getChildren()) {
if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
@@ -1239,7 +1190,7 @@ public class MessageParser extends AbstractParser
}
final String nick = packet.findChildContent("nick", Namespace.NICK);
- if (nick != null && Jid.Invalid.hasValidFrom(original)) {
+ if (nick != null && Jid.Invalid.isValid(from)) {
if (mXmppConnectionService.isMuc(account, from)) {
return;
}
@@ -1599,12 +1550,15 @@ public class MessageParser extends AbstractParser
+ ": received invite to "
+ jid
+ " but muc is considered to be online");
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
} else {
conversation.getMucOptions().setPassword(password);
mXmppConnectionService.databaseBackend.updateConversation(conversation);
- mXmppConnectionService.joinMuc(
- conversation, contact != null && contact.showInContactList());
+ if (contact != null && contact.showInContactList()) {
+ getManager(MultiUserChatManager.class).joinFollowingInvite(conversation);
+ } else {
+ getManager(MultiUserChatManager.class).join(conversation);
+ }
mXmppConnectionService.updateConversationUi();
}
return true;
@@ -15,7 +15,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.XmppUri;
@@ -25,9 +24,11 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.manager.AvatarManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
import java.util.ArrayList;
@@ -81,15 +82,16 @@ public class PresenceParser extends AbstractParser
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
final String type = packet.getAttribute("type");
- final Element x = packet.findChild("x", Namespace.MUC_USER);
+ final var x = packet.getExtension(MucUser.class);
final var vCardUpdate = packet.getExtension(VCardUpdate.class);
final List<String> codes = getStatusCodes(x);
if (type == null) {
if (x != null) {
- Element item = x.findChild("item");
+ final var item = x.getItem();
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- final MucOptions.User user = parseItem(conversation, item, from);
+ final MucOptions.User user =
+ MultiUserChatManager.itemToUser(conversation, item, from);
final var occupant = packet.getOnlyExtension(OccupantId.class);
final String occupantId =
mucOptions.occupantId() && occupant != null
@@ -135,16 +137,17 @@ public class PresenceParser extends AbstractParser
}
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
&& mucOptions.autoPushConfiguration()) {
+ final var address = mucOptions.getConversation().getJid().asBareJid();
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": room '"
- + mucOptions.getConversation().getJid().asBareJid()
+ + address
+ "' created. pushing default configuration");
- mXmppConnectionService.pushConferenceConfiguration(
- mucOptions.getConversation(),
- IqGenerator.defaultChannelConfiguration(),
- null);
+ getManager(MultiUserChatManager.class)
+ .pushConfiguration(
+ conversation,
+ MultiUserChatManager.defaultChannelConfiguration());
}
if (mXmppConnectionService.getPgpEngine() != null) {
Element signed = packet.findChild("x", "jabber:x:signed");
@@ -199,7 +202,7 @@ public class PresenceParser extends AbstractParser
+ " online="
+ wasOnline);
if (wasOnline) {
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
}
} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
mucOptions.setError(MucOptions.Error.KICKED);
@@ -216,9 +219,10 @@ public class PresenceParser extends AbstractParser
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
}
} else if (!from.isBareJid()) {
- Element item = x.findChild("item");
+ final var item = x.getItem();
if (item != null) {
- mucOptions.updateUser(parseItem(conversation, item, from));
+ mucOptions.updateUser(
+ MultiUserChatManager.itemToUser(conversation, item, from));
}
MucOptions.User user = mucOptions.deleteUser(from);
if (user != null) {
@@ -19,6 +19,7 @@ import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.services.MuclumbusService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
@@ -304,10 +305,10 @@ public class ChannelDiscoveryService {
for (final var account : service.getAccounts()) {
final var connection = account.getXmppConnection();
if (connection != null && account.isEnabled()) {
- for (final String mucService : connection.getMucServers()) {
- final Jid jid = Jid.ofOrInvalid(mucService);
- if (Jid.Invalid.isValid(jid)) {
- localMucServices.put(jid, connection);
+ for (final var mucService :
+ connection.getManager(MultiUserChatManager.class).getServices()) {
+ if (Jid.Invalid.isValid(mucService)) {
+ localMucServices.put(mucService, connection);
}
}
}
@@ -134,7 +134,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.execute(query);
}
- void catchupMUC(final Conversation conversation) {
+ public void catchupMUC(final Conversation conversation) {
if (conversation.getLastMessageTransmitted().getTimestamp() < 0
&& conversation.countMessages() == 0) {
query(conversation, new MamReference(0), 0, true);
@@ -749,5 +749,16 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
boolean hasCallback() {
return this.callback != null;
}
+
+ public boolean isImplausibleFrom(final Jid from) {
+ if (muc()) {
+ if (from == null) {
+ return true;
+ }
+ return !from.asBareJid().equals(getWith());
+ } else {
+ return false;
+ }
+ }
}
}
@@ -37,7 +37,6 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.security.KeyChain;
-import android.text.TextUtils;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
@@ -76,7 +75,6 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Reaction;
import eu.siacs.conversations.generator.AbstractGenerator;
@@ -85,7 +83,6 @@ import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.ServiceOutageStatus;
-import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
@@ -110,7 +107,6 @@ import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.TorServiceUtils;
import eu.siacs.conversations.utils.WakeLockHelper;
import eu.siacs.conversations.utils.XmppUri;
@@ -133,21 +129,19 @@ import eu.siacs.conversations.xmpp.manager.AvatarManager;
import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
-import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.NickManager;
+import eu.siacs.conversations.xmpp.manager.PepManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
-import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import eu.siacs.conversations.xmpp.manager.RosterManager;
import eu.siacs.conversations.xmpp.manager.VCardManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
-import im.conversations.android.xmpp.Entity;
-import im.conversations.android.xmpp.IqErrorException;
-import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.up.Push;
-import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
import java.io.File;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -159,7 +153,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
@@ -1652,7 +1645,10 @@ public class XmppConnectionService extends Service {
}
}
- final boolean inProgressJoin = isJoinInProgress(conversation);
+ final boolean inProgressJoin =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .isJoinInProgress(conversation);
if (account.isOnlineAndConnected() && !inProgressJoin) {
switch (message.getEncryption()) {
@@ -1786,29 +1782,6 @@ public class XmppConnectionService extends Service {
}
}
- private boolean isJoinInProgress(final Conversation conversation) {
- final Account account = conversation.getAccount();
- synchronized (account.inProgressConferenceJoins) {
- if (conversation.getMode() == Conversational.MODE_MULTI) {
- final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
- final boolean pending = account.pendingConferenceJoins.contains(conversation);
- final boolean inProgressJoin = inProgress || pending;
- if (inProgressJoin) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": holding back message to group. inProgress="
- + inProgress
- + ", pending="
- + pending);
- }
- return inProgressJoin;
- } else {
- return false;
- }
- }
- }
-
public void sendUnsentMessages(final Conversation conversation) {
conversation.findWaitingMessages(message -> resendMessage(message, true));
}
@@ -1897,141 +1870,12 @@ public class XmppConnectionService extends Service {
return true;
}
- public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
- final Account account = bookmark.getAccount();
- Conversation conversation = find(bookmark);
- if (conversation != null) {
- if (conversation.getMode() != Conversation.MODE_MULTI) {
- return;
- }
- bookmark.setConversation(conversation);
- if (pep && !bookmark.autojoin()) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": archiving conference ("
- + conversation.getJid()
- + ") after receiving pep");
- archiveConversation(conversation, false);
- } else {
- final MucOptions mucOptions = conversation.getMucOptions();
- if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
- final String current = mucOptions.getActualNick();
- final String proposed = mucOptions.getProposedNickPure();
- if (current != null && !current.equals(proposed)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": proposed nick changed after bookmark push "
- + current
- + "->"
- + proposed);
- joinMuc(conversation);
- }
- } else {
- checkMucRequiresRename(conversation);
- }
- }
- } else if (bookmark.autojoin()) {
- conversation =
- findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
- bookmark.setConversation(conversation);
- }
- }
-
- public void processModifiedBookmark(final Bookmark bookmark) {
- processModifiedBookmark(bookmark, true);
- }
-
- public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
- final var account = conversation.getAccount();
- final var existingBookmark = conversation.getBookmark();
- if (existingBookmark == null) {
- final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
- bookmark.setAutojoin(true);
- createBookmark(account, bookmark);
- } else {
- if (existingBookmark.autojoin()) {
- return;
- }
- existingBookmark.setAutojoin(true);
- createBookmark(account, existingBookmark);
- }
- }
-
public void createBookmark(final Account account, final Bookmark bookmark) {
- account.putBookmark(bookmark);
- final XmppConnection connection = account.getXmppConnection();
- final ListenableFuture<Void> future;
- if (connection.getManager(BookmarkManager.class).hasFeature()) {
- future = connection.getManager(BookmarkManager.class).publish(bookmark);
- } else if (connection.getManager(LegacyBookmarkManager.class).hasConversion()) {
- future =
- connection
- .getManager(LegacyBookmarkManager.class)
- .publish(account.getBookmarks());
- } else {
- future =
- connection
- .getManager(PrivateStorageManager.class)
- .publishBookmarks(account.getBookmarks());
- }
- Futures.addCallback(
- future,
- new FutureCallback<Void>() {
- @Override
- public void onSuccess(Void result) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": created bookmark");
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": could not create bookmark",
- t);
- }
- },
- MoreExecutors.directExecutor());
+ account.getXmppConnection().getManager(BookmarkManager.class).create(bookmark);
}
public void deleteBookmark(final Account account, final Bookmark bookmark) {
- account.removeBookmark(bookmark);
- final XmppConnection connection = account.getXmppConnection();
- final ListenableFuture<Void> future;
- if (connection.getManager(BookmarkManager.class).hasFeature()) {
- future =
- connection
- .getManager(BookmarkManager.class)
- .retract(bookmark.getJid().asBareJid());
- } else if (connection.getManager(LegacyBookmarkManager.class).hasConversion()) {
- future =
- connection
- .getManager(LegacyBookmarkManager.class)
- .publish(account.getBookmarks());
- } else {
- future =
- connection
- .getManager(PrivateStorageManager.class)
- .publishBookmarks(account.getBookmarks());
- }
- Futures.addCallback(
- future,
- new FutureCallback<Void>() {
- @Override
- public void onSuccess(Void result) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark");
- }
-
- @Override
- public void onFailure(@NonNull Throwable t) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": could not delete bookmark",
- t);
- }
- },
- MoreExecutors.directExecutor());
+ account.getXmppConnection().getManager(BookmarkManager.class).delete(bookmark);
}
private void restoreFromDatabase() {
@@ -2475,7 +2319,9 @@ public class XmppConnectionService extends Service {
null));
this.conversations.add(existing);
if (existing.getMode() == Conversational.MODE_MULTI) {
- ensureBookmarkIsAutoJoin(existing);
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .ensureBookmarkIsAutoJoin(existing);
}
updateConversationUi();
return existing;
@@ -2534,17 +2380,20 @@ public class XmppConnectionService extends Service {
public void archiveConversation(
Conversation conversation, final boolean maySynchronizeWithBookmarks) {
+ final var account = conversation.getAccount();
+ final var connection = account.getXmppConnection();
getNotificationService().clear(conversation);
conversation.setStatus(Conversation.STATUS_ARCHIVED);
conversation.setNextMessage(null);
synchronized (this.conversations) {
getMessageArchiveService().kill(conversation);
if (conversation.getMode() == Conversation.MODE_MULTI) {
+ // TODO always clean up bookmarks no matter if we are currently connected
+ // TODO always delete reference to conversation in bookmark
if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
final Bookmark bookmark = conversation.getBookmark();
if (maySynchronizeWithBookmarks && bookmark != null) {
if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
- Account account = bookmark.getAccount();
bookmark.setConversation(null);
deleteBookmark(account, bookmark);
} else if (bookmark.autojoin()) {
@@ -2553,7 +2402,7 @@ public class XmppConnectionService extends Service {
}
}
}
- leaveMuc(conversation);
+ connection.getManager(MultiUserChatManager.class).leave(conversation);
} else {
if (conversation
.getContact()
@@ -2753,7 +2602,9 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (connected) {
- leaveMuc(conversation);
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .unavailable(conversation);
}
}
conversations.remove(conversation);
@@ -3101,344 +2952,16 @@ public class XmppConnectionService extends Service {
}
}
- public void mucSelfPingAndRejoin(final Conversation conversation) {
- final Account account = conversation.getAccount();
- synchronized (account.inProgressConferenceJoins) {
- if (account.inProgressConferenceJoins.contains(conversation)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": canceling muc self ping because join is already under way");
- return;
- }
- }
- synchronized (account.inProgressConferencePings) {
- if (!account.inProgressConferencePings.add(conversation)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": canceling muc self ping because ping is already under way");
- return;
- }
- }
- // TODO use PingManager
- final Jid self = conversation.getMucOptions().getSelf().getFullJid();
- final Iq ping = new Iq(Iq.Type.GET);
- ping.setTo(self);
- ping.addChild("ping", Namespace.PING);
- sendIqPacket(
- conversation.getAccount(),
- ping,
- (response) -> {
- if (response.getType() == Iq.Type.ERROR) {
- final var error = response.getError();
- if (error == null
- || error.hasChild("service-unavailable")
- || error.hasChild("feature-not-implemented")
- || error.hasChild("item-not-found")) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " came back as ignorable error");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " failed. attempting rejoin");
- joinMuc(conversation);
- }
- } else if (response.getType() == Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " came back fine");
- }
- synchronized (account.inProgressConferencePings) {
- account.inProgressConferencePings.remove(conversation);
- }
- });
- }
-
- public void joinMuc(Conversation conversation) {
- joinMuc(conversation, null, false);
- }
-
- public void joinMuc(Conversation conversation, boolean followedInvite) {
- joinMuc(conversation, null, followedInvite);
- }
-
- private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
- joinMuc(conversation, onConferenceJoined, false);
- }
-
- private void joinMuc(
- final Conversation conversation,
- final OnConferenceJoined onConferenceJoined,
- final boolean followedInvite) {
- final Account account = conversation.getAccount();
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.remove(conversation);
- }
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.remove(conversation);
- }
- if (account.getStatus() == Account.State.ONLINE) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.add(conversation);
- }
- if (Config.MUC_LEAVE_BEFORE_JOIN) {
- sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
- }
- conversation.resetMucOptions();
- if (onConferenceJoined != null) {
- conversation.getMucOptions().flagNoAutoPushConfiguration();
- }
- conversation.setHasMessagesLeftOnServer(false);
- fetchConferenceConfiguration(
- conversation,
- new OnConferenceConfigurationFetched() {
-
- private void join(Conversation conversation) {
- Account account = conversation.getAccount();
- final MucOptions mucOptions = conversation.getMucOptions();
-
- if (mucOptions.nonanonymous()
- && !mucOptions.membersOnly()
- && !conversation.getBooleanAttribute(
- "accept_non_anonymous", false)) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- }
- mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
- updateConversationUi();
- if (onConferenceJoined != null) {
- onConferenceJoined.onConferenceJoined(conversation);
- }
- return;
- }
-
- final Jid joinJid = mucOptions.getSelf().getFullJid();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": joining conversation "
- + joinJid.toString());
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence
- .Availability.ONLINE,
- mucOptions.nonanonymous()
- || onConferenceJoined != null);
- packet.setTo(joinJid);
- Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
- if (conversation.getMucOptions().getPassword() != null) {
- x.addChild("password").setContent(mucOptions.getPassword());
- }
-
- if (mucOptions.mamSupport()) {
- // Use MAM instead of the limited muc history to get history
- x.addChild("history").setAttribute("maxchars", "0");
- } else {
- // Fallback to muc history
- x.addChild("history")
- .setAttribute(
- "since",
- PresenceGenerator.getTimestamp(
- conversation
- .getLastMessageTransmitted()
- .getTimestamp()));
- }
- sendPresencePacket(account, packet);
- if (onConferenceJoined != null) {
- onConferenceJoined.onConferenceJoined(conversation);
- }
- if (!joinJid.equals(conversation.getJid())) {
- conversation.setContactJid(joinJid);
- databaseBackend.updateConversation(conversation);
- }
-
- if (mucOptions.mamSupport()) {
- getMessageArchiveService().catchupMUC(conversation);
- }
- if (mucOptions.isPrivateAndNonAnonymous()) {
- fetchConferenceMembers(conversation);
-
- if (followedInvite) {
- final Bookmark bookmark = conversation.getBookmark();
- if (bookmark != null) {
- if (!bookmark.autojoin()) {
- bookmark.setAutojoin(true);
- createBookmark(account, bookmark);
- }
- } else {
- saveConversationAsBookmark(conversation, null);
- }
- }
- }
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- sendUnsentMessages(conversation);
- }
- }
-
- @Override
- public void onConferenceConfigurationFetched(Conversation conversation) {
- if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": conversation ("
- + conversation.getJid()
- + ") got archived before IQ result");
- return;
- }
- join(conversation);
- }
-
- @Override
- public void onFetchFailed(
- final Conversation conversation, final String errorCondition) {
- if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": conversation ("
- + conversation.getJid()
- + ") got archived before IQ result");
- return;
- }
- if ("remote-server-not-found".equals(errorCondition)) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- }
- conversation
- .getMucOptions()
- .setError(MucOptions.Error.SERVER_NOT_FOUND);
- updateConversationUi();
- } else {
- join(conversation);
- fetchConferenceConfiguration(conversation);
- }
- }
- });
- updateConversationUi();
- } else {
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.add(conversation);
- }
- conversation.resetMucOptions();
- conversation.setHasMessagesLeftOnServer(false);
- updateConversationUi();
- }
- }
-
- private void fetchConferenceMembers(final Conversation conversation) {
- final Account account = conversation.getAccount();
- final AxolotlService axolotlService = account.getAxolotlService();
- final String[] affiliations = {"member", "admin", "owner"};
- final Consumer<Iq> callback =
- new Consumer<Iq>() {
-
- private int i = 0;
- private boolean success = true;
-
- @Override
- public void accept(Iq response) {
- final boolean omemoEnabled =
- conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
- Element query = response.query("http://jabber.org/protocol/muc#admin");
- if (response.getType() == Iq.Type.RESULT && query != null) {
- for (Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- MucOptions.User user =
- AbstractParser.parseItem(conversation, child);
- if (!user.realJidMatchesAccount()) {
- boolean isNew =
- conversation.getMucOptions().updateUser(user);
- Contact contact = user.getContact();
- if (omemoEnabled
- && isNew
- && user.getRealJid() != null
- && (contact == null
- || !contact.mutualPresenceSubscription())
- && axolotlService.hasEmptyDeviceList(
- user.getRealJid())) {
- axolotlService.fetchDeviceIds(user.getRealJid());
- }
- }
- }
- }
- } else {
- success = false;
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not request affiliation "
- + affiliations[i]
- + " in "
- + conversation.getJid().asBareJid());
- }
- ++i;
- if (i >= affiliations.length) {
- final var mucOptions = conversation.getMucOptions();
- final var members = mucOptions.getMembers(true);
- if (success) {
- List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
- boolean changed = false;
- for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
- iterator.hasNext(); ) {
- Jid jid = iterator.next();
- if (!members.contains(jid)
- && !members.contains(jid.getDomain())) {
- iterator.remove();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removed "
- + jid
- + " from crypto targets of "
- + conversation.getName());
- changed = true;
- }
- }
- if (changed) {
- conversation.setAcceptedCryptoTargets(cryptoTargets);
- updateConversation(conversation);
- }
- }
- getAvatarService().clear(mucOptions);
- updateMucRosterUi();
- updateConversationUi();
- }
- }
- };
- for (String affiliation : affiliations) {
- sendIqPacket(
- account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
+ public void joinMuc(final Conversation conversation) {
+ final var account = conversation.getAccount();
+ account.getXmppConnection().getManager(MultiUserChatManager.class).join(conversation);
}
public void providePasswordForMuc(final Conversation conversation, final String password) {
- if (conversation.getMode() == Conversation.MODE_MULTI) {
- conversation.getMucOptions().setPassword(password);
- if (conversation.getBookmark() != null) {
- final Bookmark bookmark = conversation.getBookmark();
- bookmark.setAutojoin(true);
- createBookmark(conversation.getAccount(), bookmark);
- }
- updateConversation(conversation);
- joinMuc(conversation);
- }
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .setPassword(conversation, password);
}
public void deleteAvatar(final Account account) {
@@ -3476,23 +2999,28 @@ public class XmppConnectionService extends Service {
}
public void deletePepNode(final Account account, final String node) {
- final Iq request = mIqGenerator.deleteNode(node);
- sendIqPacket(
- account,
- request,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
+ final var future = account.getXmppConnection().getManager(PepManager.class).delete(node);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": successfully deleted pep node "
+ node);
- } else {
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid() + ": failed to delete " + packet);
+ account.getJid().asBareJid() + ": failed to delete node " + node,
+ t);
}
- });
+ },
+ MoreExecutors.directExecutor());
}
private boolean hasEnabledAccounts() {
@@ -3565,154 +3093,17 @@ public class XmppConnectionService extends Service {
createBookmark(bookmark.getAccount(), bookmark);
}
- public boolean renameInMuc(
- final Conversation conversation,
- final String nick,
- final UiCallback<Conversation> callback) {
- final Account account = conversation.getAccount();
- final Bookmark bookmark = conversation.getBookmark();
- final MucOptions options = conversation.getMucOptions();
- final Jid joinJid = options.createJoinJid(nick);
- if (joinJid == null) {
- return false;
- }
- if (options.online()) {
- options.setOnRenameListener(
- new OnRenameListener() {
-
- @Override
- public void onSuccess() {
- callback.success(conversation);
- }
-
- @Override
- public void onFailure() {
- callback.error(R.string.nick_in_use, conversation);
- }
- });
-
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
- options.nonanonymous());
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
- if (nick.equals(MucOptions.defaultNick(account))
- && bookmark != null
- && bookmark.getNick() != null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removing nick from bookmark for "
- + bookmark.getJid());
- bookmark.setNick(null);
- createBookmark(account, bookmark);
- }
- } else {
- conversation.setContactJid(joinJid);
- databaseBackend.updateConversation(conversation);
- if (account.getStatus() == Account.State.ONLINE) {
- if (bookmark != null) {
- bookmark.setNick(nick);
- createBookmark(account, bookmark);
- }
- joinMuc(conversation);
- }
- }
- return true;
- }
-
public void checkMucRequiresRename() {
synchronized (this.conversations) {
for (final Conversation conversation : this.conversations) {
if (conversation.getMode() == Conversational.MODE_MULTI) {
- checkMucRequiresRename(conversation);
- }
- }
- }
- }
-
- private void checkMucRequiresRename(final Conversation conversation) {
- final var options = conversation.getMucOptions();
- if (!options.online()) {
- return;
- }
- final var account = conversation.getAccount();
- final String current = options.getActualNick();
- final String proposed = options.getProposedNickPure();
- if (current == null || current.equals(proposed)) {
- return;
- }
- final Jid joinJid = options.createJoinJid(proposed);
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: muc rename required %s (was: %s)",
- account.getJid().asBareJid(), joinJid, current));
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
- options.nonanonymous());
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
- }
-
- public void leaveMuc(Conversation conversation) {
- leaveMuc(conversation, false);
- }
-
- private void leaveMuc(Conversation conversation, boolean now) {
- final Account account = conversation.getAccount();
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.remove(conversation);
- }
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.remove(conversation);
- }
- if (account.getStatus() == Account.State.ONLINE || now) {
- sendPresencePacket(
- conversation.getAccount(),
- mPresenceGenerator.leave(conversation.getMucOptions()));
- conversation.getMucOptions().setOffline();
- Bookmark bookmark = conversation.getBookmark();
- if (bookmark != null) {
- bookmark.setConversation(null);
- }
- Log.d(
- Config.LOGTAG,
- conversation.getAccount().getJid().asBareJid()
- + ": leaving muc "
- + conversation.getJid());
- final var connection = account.getXmppConnection();
- if (connection != null) {
- connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
- }
- } else {
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.add(conversation);
- }
- }
- }
-
- public String findConferenceServer(final Account account) {
- String server;
- if (account.getXmppConnection() != null) {
- server = account.getXmppConnection().getMucServer();
- if (server != null) {
- return server;
- }
- }
- for (Account other : getAccounts()) {
- if (other != account && other.getXmppConnection() != null) {
- server = other.getXmppConnection().getMucServer();
- if (server != null) {
- return server;
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .checkMucRequiresRename(conversation);
}
}
}
- return null;
}
public void createPublicChannel(
@@ -3720,226 +3111,57 @@ public class XmppConnectionService extends Service {
final String name,
final Jid address,
final UiCallback<Conversation> callback) {
- joinMuc(
- findOrCreateConversation(account, address, true, false, true),
- conversation -> {
- final Bundle configuration = IqGenerator.defaultChannelConfiguration();
- if (!TextUtils.isEmpty(name)) {
- configuration.putString("muc#roomconfig_roomname", name);
+ final var future =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .createPublicChannel(address, name);
+
+ Futures.addCallback(
+ future,
+ new FutureCallback<Conversation>() {
+ @Override
+ public void onSuccess(Conversation result) {
+ callback.success(result);
}
- pushConferenceConfiguration(
- conversation,
- configuration,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- saveConversationAsBookmark(conversation, name);
- callback.success(conversation);
- }
- @Override
- public void onPushFailed() {
- if (conversation
- .getMucOptions()
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER)) {
- callback.error(
- R.string.unable_to_set_channel_configuration,
- conversation);
- } else {
- callback.error(
- R.string.joined_an_existing_channel, conversation);
- }
- }
- });
- });
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(Config.LOGTAG, "could not create public channel", t);
+ // TODO I guess itβs better to just not use callbacks here
+ callback.error(R.string.unable_to_set_channel_configuration, null);
+ }
+ },
+ MoreExecutors.directExecutor());
}
public boolean createAdhocConference(
final Account account,
final String name,
- final Iterable<Jid> jids,
+ final Collection<Jid> addresses,
final UiCallback<Conversation> callback) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": creating adhoc conference with "
- + jids.toString());
- if (account.getStatus() == Account.State.ONLINE) {
- try {
- String server = findConferenceServer(account);
- if (server == null) {
- if (callback != null) {
- callback.error(R.string.no_conference_server_found, null);
- }
- return false;
- }
- final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
- final Conversation conversation =
- findOrCreateConversation(account, jid, true, false, true);
- joinMuc(
- conversation,
- new OnConferenceJoined() {
- @Override
- public void onConferenceJoined(final Conversation conversation) {
- final Bundle configuration =
- IqGenerator.defaultGroupChatConfiguration();
- if (!TextUtils.isEmpty(name)) {
- configuration.putString("muc#roomconfig_roomname", name);
- }
- pushConferenceConfiguration(
- conversation,
- configuration,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- for (Jid invite : jids) {
- invite(conversation, invite);
- }
- for (String resource :
- account.getSelfContact()
- .getPresences()
- .toResourceArray()) {
- Jid other =
- account.getJid().withResource(resource);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": sending direct invite to "
- + other);
- directInvite(conversation, other);
- }
- saveConversationAsBookmark(conversation, name);
- if (callback != null) {
- callback.success(conversation);
- }
- }
-
- @Override
- public void onPushFailed() {
- archiveConversation(conversation);
- if (callback != null) {
- callback.error(
- R.string.conference_creation_failed,
- conversation);
- }
- }
- });
- }
- });
- return true;
- } catch (IllegalArgumentException e) {
- if (callback != null) {
- callback.error(R.string.conference_creation_failed, null);
- }
- return false;
- }
- } else {
- if (callback != null) {
- callback.error(R.string.not_connected_try_again, null);
- }
+ final var manager = account.getXmppConnection().getManager(MultiUserChatManager.class);
+ if (manager.getServices().isEmpty()) {
return false;
}
- }
- public void fetchConferenceConfiguration(final Conversation conversation) {
- fetchConferenceConfiguration(conversation, null);
- }
+ final var future = manager.createPrivateGroupChat(name, addresses);
- public void fetchConferenceConfiguration(
- final Conversation conversation, final OnConferenceConfigurationFetched callback) {
- final var account = conversation.getAccount();
- final var connection = account.getXmppConnection();
- final var address = conversation.getJid().asBareJid();
- if (connection == null) {
- return;
- }
- final var future =
- connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null);
Futures.addCallback(
future,
new FutureCallback<>() {
@Override
- public void onSuccess(InfoQuery result) {
- final var avatarHash =
- result.getServiceDiscoveryExtension(
- Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash");
- if (VCardUpdate.isValidSHA1(avatarHash)) {
- connection
- .getManager(AvatarManager.class)
- .handleVCardUpdate(address, avatarHash);
- }
- final MucOptions mucOptions = conversation.getMucOptions();
- final Bookmark bookmark = conversation.getBookmark();
- final boolean sameBefore =
- StringUtils.equals(
- bookmark == null ? null : bookmark.getBookmarkName(),
- mucOptions.getName());
-
- final var hadOccupantId = mucOptions.occupantId();
- if (mucOptions.updateConfiguration(result)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": muc configuration changed for "
- + conversation.getJid().asBareJid());
- updateConversation(conversation);
- }
-
- final var hasOccupantId = mucOptions.occupantId();
-
- if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
- final var me = mucOptions.getSelf().getFullJid();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": gained support for occupant-id in "
- + me
- + ". resending presence");
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence
- .Availability.ONLINE,
- mucOptions.nonanonymous());
- packet.setTo(me);
- sendPresencePacket(account, packet);
- }
-
- if (bookmark != null
- && (sameBefore || bookmark.getBookmarkName() == null)) {
- if (bookmark.setBookmarkName(
- StringUtils.nullOnEmpty(mucOptions.getName()))) {
- createBookmark(account, bookmark);
- }
- }
-
- if (callback != null) {
- callback.onConferenceConfigurationFetched(conversation);
- }
-
- updateConversationUi();
+ public void onSuccess(Conversation result) {
+ callback.success(result);
}
@Override
- public void onFailure(@NonNull Throwable throwable) {
- if (throwable instanceof TimeoutException) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received timeout waiting for conference"
- + " configuration fetch");
- } else if (throwable instanceof IqErrorException errorResponseException) {
- if (callback != null) {
- callback.onFetchFailed(
- conversation,
- errorResponseException.getResponse().getErrorCondition());
- }
- }
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not create private group chat", t);
+ callback.error(R.string.conference_creation_failed, null);
}
},
MoreExecutors.directExecutor());
+ return true;
}
public void pushNodeConfiguration(
@@ -35,6 +35,7 @@ import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@@ -299,7 +300,9 @@ public class ChannelDiscoveryActivity extends XmppActivity
final Conversation conversation =
xmppConnectionService.findOrCreateConversation(
account, result.getRoom(), true, true, true);
- xmppConnectionService.ensureBookmarkIsAutoJoin(conversation);
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .ensureBookmarkIsAutoJoin(conversation);
switchToConversation(conversation);
}
}
@@ -4,7 +4,6 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue;
import static eu.siacs.conversations.utils.StringUtils.changed;
import android.app.Activity;
-import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
@@ -19,9 +18,15 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
import de.gultsch.common.Linkify;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
@@ -45,10 +50,13 @@ import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -58,8 +66,6 @@ public class ConferenceDetailsActivity extends XmppActivity
implements OnConversationUpdate,
OnMucRosterUpdate,
XmppConnectionService.OnAffiliationChanged,
- XmppConnectionService.OnConfigurationPushed,
- XmppConnectionService.OnRoomDestroy,
TextWatcher,
OnMediaLoaded {
public static final String ACTION_VIEW_MUC = "view_muc";
@@ -72,24 +78,20 @@ public class ConferenceDetailsActivity extends XmppActivity
private boolean mAdvancedMode = false;
- private final UiCallback<Conversation> renameCallback =
- new UiCallback<Conversation>() {
+ private FutureCallback<Void> renameCallback =
+ new FutureCallback<Void>() {
@Override
- public void success(Conversation object) {
+ public void onSuccess(Void result) {
displayToast(getString(R.string.your_nick_has_been_changed));
- runOnUiThread(
- () -> {
- updateView();
- });
+ updateView();
}
@Override
- public void error(final int errorCode, Conversation object) {
- displayToast(getString(errorCode));
- }
+ public void onFailure(Throwable t) {
- @Override
- public void userInputRequired(PendingIntent pi, Conversation object) {}
+ // TODO check for NickInUseException and NickInvalid exception
+
+ }
};
public static void open(final Activity activity, final Conversation conversation) {
@@ -139,6 +141,20 @@ public class ConferenceDetailsActivity extends XmppActivity
}
};
+ private final FutureCallback<Void> onConfigurationPushed =
+ new FutureCallback<Void>() {
+
+ @Override
+ public void onSuccess(Void result) {
+ displayToast(getString(R.string.modified_conference_options));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ displayToast(getString(R.string.could_not_modify_conference_options));
+ }
+ };
+
private final OnClickListener mChangeConferenceSettings =
new OnClickListener() {
@Override
@@ -159,15 +175,17 @@ public class ConferenceDetailsActivity extends XmppActivity
builder.setPositiveButton(
R.string.confirm,
(dialog, which) -> {
- final Bundle options = configuration.toBundle(values);
- options.putString("muc#roomconfig_persistentroom", "1");
- if (options.containsKey("muc#roomconfig_allowinvites")) {
- options.putString(
- "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites",
- options.getString("muc#roomconfig_allowinvites"));
- }
- xmppConnectionService.pushConferenceConfiguration(
- mConversation, options, ConferenceDetailsActivity.this);
+ final var options = configuration.toBundle(values);
+ final var future =
+ mConversation
+ .getAccount()
+ .getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .pushConfiguration(mConversation, options);
+ Futures.addCallback(
+ future,
+ onConfigurationPushed,
+ ContextCompat.getMainExecutor(getApplication()));
});
builder.create().show();
}
@@ -202,12 +220,21 @@ public class ConferenceDetailsActivity extends XmppActivity
mConversation.getMucOptions().getActualNick(),
R.string.nickname,
value -> {
- if (xmppConnectionService.renameInMuc(
- mConversation, value, renameCallback)) {
- return null;
- } else {
+ if (mConversation.getMucOptions().createJoinJid(value)
+ == null) {
return getString(R.string.invalid_muc_nick);
}
+ final var future =
+ mConversation
+ .getAccount()
+ .getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .changeUsername(mConversation, value);
+ Futures.addCallback(
+ future,
+ renameCallback,
+ ContextCompat.getMainExecutor(this));
+ return null;
}));
this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
@@ -223,10 +250,7 @@ public class ConferenceDetailsActivity extends XmppActivity
.show();
return;
}
- if (!mucOptions
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER)) {
+ if (!mucOptions.getSelf().ranks(Affiliation.OWNER)) {
Toast.makeText(
this,
R.string.only_the_owner_can_change_group_chat_avatar,
@@ -344,8 +368,7 @@ public class ConferenceDetailsActivity extends XmppActivity
this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
final String name = mucOptions.getName();
this.binding.mucEditTitle.setText("");
- final boolean owner =
- mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
+ final boolean owner = mucOptions.getSelf().ranks(Affiliation.OWNER);
if (owner || printableValue(name)) {
this.binding.mucEditTitle.setVisibility(View.VISIBLE);
if (name != null) {
@@ -388,16 +411,23 @@ public class ConferenceDetailsActivity extends XmppActivity
}
private void onMucInfoUpdated(String subject, String name) {
+ final var account = mConversation.getAccount();
final MucOptions mucOptions = mConversation.getMucOptions();
if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
xmppConnectionService.pushSubjectToConference(mConversation, subject);
}
- if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)
- && changed(mucOptions.getName(), name)) {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name));
- xmppConnectionService.pushConferenceConfiguration(mConversation, options, this);
+ if (mucOptions.getSelf().ranks(Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
+ final var options =
+ new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_roomname", Strings.nullToEmpty(name))
+ .build();
+ final var future =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .pushConfiguration(mConversation, options);
+ Futures.addCallback(
+ future, onConfigurationPushed, ContextCompat.getMainExecutor(getApplication()));
}
}
@@ -426,11 +456,7 @@ public class ConferenceDetailsActivity extends XmppActivity
}
menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
menuItemDestroyRoom.setVisible(
- mConversation
- .getMucOptions()
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER));
+ mConversation.getMucOptions().getSelf().ranks(Affiliation.OWNER));
return true;
}
@@ -461,11 +487,33 @@ public class ConferenceDetailsActivity extends XmppActivity
}
protected void saveAsBookmark() {
- xmppConnectionService.saveConversationAsBookmark(
- mConversation, mConversation.getMucOptions().getName());
+ final var account = mConversation.getAccount();
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .save(mConversation, mConversation.getMucOptions().getName());
}
protected void destroyRoom() {
+ final var destroyCallBack =
+ new FutureCallback<Void>() {
+
+ @Override
+ public void onSuccess(Void result) {
+ finish();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ final boolean groupChat =
+ mConversation != null && mConversation.isPrivateAndNonAnonymous();
+ // TODO show toast directly
+ displayToast(
+ getString(
+ groupChat
+ ? R.string.could_not_destroy_room
+ : R.string.could_not_destroy_channel));
+ }
+ };
final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
@@ -474,8 +522,11 @@ public class ConferenceDetailsActivity extends XmppActivity
builder.setPositiveButton(
R.string.ok,
(dialog, which) -> {
- xmppConnectionService.destroyRoom(
- mConversation, ConferenceDetailsActivity.this);
+ final var future = xmppConnectionService.destroyRoom(mConversation);
+ Futures.addCallback(
+ future,
+ destroyCallBack,
+ ContextCompat.getMainExecutor(getApplication()));
});
builder.setNegativeButton(R.string.cancel, null);
final AlertDialog dialog = builder.create();
@@ -528,8 +579,7 @@ public class ConferenceDetailsActivity extends XmppActivity
? R.string.action_muc_details
: R.string.channel_details);
this.binding.editMucNameButton.setVisibility(
- (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)
- || mucOptions.canChangeSubject())
+ (self.ranks(Affiliation.OWNER) || mucOptions.canChangeSubject())
? View.VISIBLE
: View.GONE);
this.binding.detailsAccount.setText(getString(R.string.using_account, account));
@@ -579,7 +629,7 @@ public class ConferenceDetailsActivity extends XmppActivity
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
this.binding.mucRole.setVisibility(View.VISIBLE);
this.binding.mucRole.setText(getStatus(self));
- if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (mucOptions.getSelf().ranks(Affiliation.OWNER)) {
this.binding.mucSettings.setVisibility(View.VISIBLE);
this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
} else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
@@ -594,7 +644,7 @@ public class ConferenceDetailsActivity extends XmppActivity
} else {
this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
}
- if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (self.ranks(Affiliation.OWNER)) {
this.binding.changeConferenceButton.setVisibility(View.VISIBLE);
} else {
this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
@@ -627,9 +677,9 @@ public class ConferenceDetailsActivity extends XmppActivity
Collections.sort(
users,
(a, b) -> {
- if (b.getAffiliation().outranks(a.getAffiliation())) {
+ if (b.outranks(a.getAffiliation())) {
return 1;
- } else if (a.getAffiliation().outranks(b.getAffiliation())) {
+ } else if (a.outranks(b.getAffiliation())) {
return -1;
} else {
if (a.getAvatar() != null && b.getAvatar() == null) {
@@ -668,13 +718,32 @@ public class ConferenceDetailsActivity extends XmppActivity
if (advanced) {
return String.format(
"%s (%s)",
- context.getString(user.getAffiliation().getResId()),
- context.getString(user.getRole().getResId()));
+ context.getString(affiliationToStringRes(user.getAffiliation())),
+ context.getString(roleToStringRes(user.getRole())));
} else {
- return context.getString(user.getAffiliation().getResId());
+ return context.getString(affiliationToStringRes(user.getAffiliation()));
}
}
+ private static @StringRes int affiliationToStringRes(final Affiliation affiliation) {
+ return switch (affiliation) {
+ case OWNER -> R.string.owner;
+ case ADMIN -> R.string.admin;
+ case MEMBER -> R.string.member;
+ case NONE -> R.string.no_affiliation;
+ case OUTCAST -> R.string.outcast;
+ };
+ }
+
+ private static @StringRes int roleToStringRes(final Role role) {
+ return switch (role) {
+ case MODERATOR -> R.string.moderator;
+ case VISITOR -> R.string.visitor;
+ case PARTICIPANT -> R.string.participant;
+ case NONE -> R.string.no_role;
+ };
+ }
+
private String getStatus(User user) {
return getStatus(this, user, mAdvancedMode);
}
@@ -689,31 +758,6 @@ public class ConferenceDetailsActivity extends XmppActivity
displayToast(getString(resId, jid.asBareJid().toString()));
}
- @Override
- public void onRoomDestroySucceeded() {
- finish();
- }
-
- @Override
- public void onRoomDestroyFailed() {
- final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
- displayToast(
- getString(
- groupChat
- ? R.string.could_not_destroy_room
- : R.string.could_not_destroy_channel));
- }
-
- @Override
- public void onPushSucceeded() {
- displayToast(getString(R.string.modified_conference_options));
- }
-
- @Override
- public void onPushFailed() {
- displayToast(getString(R.string.could_not_modify_conference_options));
- }
-
private void displayToast(final String msg) {
runOnUiThread(
() -> {
@@ -128,6 +128,7 @@ 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.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
@@ -1178,9 +1179,9 @@ public class ConversationFragment extends XmppFragment
}
menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false);
+ final var connection = this.conversation.getAccount().getXmppConnection();
menuInviteContact.setVisible(
- service != null
- && service.findConferenceServer(conversation.getAccount()) != null);
+ !connection.getManager(MultiUserChatManager.class).getServices().isEmpty());
}
if (conversation.isMuted()) {
menuMute.setVisible(false);
@@ -26,6 +26,7 @@ import eu.siacs.conversations.ui.util.DelayedHintHelper;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -158,7 +159,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
}
final Editable nameText = binding.groupChatName.getText();
final String name = nameText == null ? "" : nameText.toString().trim();
- final String domain = connection.getMucServer();
+ final var domain = connection.getManager(MultiUserChatManager.class).getService();
if (domain == null) {
return "";
}
@@ -270,9 +271,8 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
private void refreshKnownHosts() {
Activity activity = getActivity();
- if (activity instanceof XmppActivity) {
- Collection<String> hosts =
- ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
+ if (activity instanceof XmppActivity xmppActivity) {
+ Collection<String> hosts = xmppActivity.xmppConnectionService.getKnownConferenceHosts();
this.knownHostsAdapter.refresh(hosts);
}
}
@@ -7,17 +7,12 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
-
import com.google.common.base.Strings;
-
-import org.openintents.openpgp.util.OpenPgpUtils;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.databinding.ItemContactBinding;
@@ -30,30 +25,36 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.muc.Role;
+import org.openintents.openpgp.util.OpenPgpUtils;
-public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
-
- static final DiffUtil.ItemCallback<MucOptions.User> DIFF = new DiffUtil.ItemCallback<MucOptions.User>() {
- @Override
- public boolean areItemsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
- final Jid fullA = a.getFullJid();
- final Jid fullB = b.getFullJid();
- final Jid realA = a.getRealJid();
- final Jid realB = b.getRealJid();
- if (fullA != null && fullB != null) {
- return fullA.equals(fullB);
- } else if (realA != null && realB != null) {
- return realA.equals(realB);
- } else {
- return false;
- }
- }
+public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder>
+ implements View.OnCreateContextMenuListener {
+
+ static final DiffUtil.ItemCallback<MucOptions.User> DIFF =
+ new DiffUtil.ItemCallback<MucOptions.User>() {
+ @Override
+ public boolean areItemsTheSame(
+ @NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+ final Jid fullA = a.getFullJid();
+ final Jid fullB = b.getFullJid();
+ final Jid realA = a.getRealJid();
+ final Jid realB = b.getRealJid();
+ if (fullA != null && fullB != null) {
+ return fullA.equals(fullB);
+ } else if (realA != null && realB != null) {
+ return realA.equals(realB);
+ } else {
+ return false;
+ }
+ }
- @Override
- public boolean areContentsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
- return a.equals(b);
- }
- };
+ @Override
+ public boolean areContentsTheSame(
+ @NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+ return a.equals(b);
+ }
+ };
private final boolean advancedMode;
private MucOptions.User selectedUser = null;
@@ -65,73 +66,104 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
- return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_contact, viewGroup, false));
+ return new ViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(viewGroup.getContext()),
+ R.layout.item_contact,
+ viewGroup,
+ false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
final MucOptions.User user = getItem(position);
AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar);
- viewHolder.binding.getRoot().setOnClickListener(v -> {
- final XmppActivity activity = XmppActivity.find(v);
- if (activity == null) {
- return;
- }
- final var contact = user.getContact();
- if (user.getRole() == MucOptions.Role.NONE && contact != null) {
- Toast.makeText(
- activity,
- activity.getString(
- R.string.user_has_left_conference,
- contact.getDisplayName()),
- Toast.LENGTH_SHORT)
- .show();
- }
- activity.highlightInMuc(user.getConversation(), user.getName());
- });
+ viewHolder
+ .binding
+ .getRoot()
+ .setOnClickListener(
+ v -> {
+ final XmppActivity activity = XmppActivity.find(v);
+ if (activity == null) {
+ return;
+ }
+ final var contact = user.getContact();
+ if (user.getRole() == Role.NONE && contact != null) {
+ Toast.makeText(
+ activity,
+ activity.getString(
+ R.string.user_has_left_conference,
+ contact.getDisplayName()),
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+ activity.highlightInMuc(user.getConversation(), user.getName());
+ });
viewHolder.binding.getRoot().setTag(user);
viewHolder.binding.getRoot().setOnCreateContextMenuListener(this);
- viewHolder.binding.getRoot().setOnLongClickListener(v -> {
- selectedUser = user;
- return false;
- });
+ viewHolder
+ .binding
+ .getRoot()
+ .setOnLongClickListener(
+ v -> {
+ selectedUser = user;
+ return false;
+ });
final String name = user.getName();
final Contact contact = user.getContact();
if (contact != null) {
final String displayName = contact.getDisplayName();
viewHolder.binding.contactDisplayName.setText(displayName);
if (name != null && !name.equals(displayName)) {
- viewHolder.binding.contactJid.setText(String.format("%s \u2022 %s", name, ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)));
+ viewHolder.binding.contactJid.setText(
+ String.format(
+ "%s \u2022 %s",
+ name,
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(),
+ user,
+ advancedMode)));
} else {
- viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+ viewHolder.binding.contactJid.setText(
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(), user, advancedMode));
}
} else {
viewHolder.binding.contactDisplayName.setText(Strings.nullToEmpty(name));
- viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+ viewHolder.binding.contactJid.setText(
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(), user, advancedMode));
}
if (advancedMode && user.getPgpKeyId() != 0) {
viewHolder.binding.key.setVisibility(View.VISIBLE);
- viewHolder.binding.key.setOnClickListener(v -> {
- final XmppActivity activity = XmppActivity.find(v);
- final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
- final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine();
- if (pgpEngine != null) {
- PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
- if (intent != null) {
- try {
- activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
- } catch (IntentSender.SendIntentException ignored) {
-
+ viewHolder.binding.key.setOnClickListener(
+ v -> {
+ final XmppActivity activity = XmppActivity.find(v);
+ final XmppConnectionService service =
+ activity == null ? null : activity.xmppConnectionService;
+ final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine();
+ if (pgpEngine != null) {
+ PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
+ if (intent != null) {
+ try {
+ activity.startIntentSenderForResult(
+ intent.getIntentSender(),
+ 0,
+ null,
+ 0,
+ 0,
+ 0,
+ Compatibility.pgpStartIntentSenderOptions());
+ } catch (IntentSender.SendIntentException ignored) {
+
+ }
+ }
}
- }
- }
- });
+ });
viewHolder.binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
} else {
viewHolder.binding.key.setVisibility(View.GONE);
}
-
-
}
public MucOptions.User getSelectedUser() {
@@ -139,8 +171,9 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- MucDetailsContextMenuHelper.onCreateContextMenu(menu,v);
+ public void onCreateContextMenu(
+ ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ MucDetailsContextMenuHelper.onCreateContextMenu(menu, v);
}
static class ViewHolder extends RecyclerView.ViewHolder {
@@ -5,18 +5,17 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemUserPreviewBinding;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+import im.conversations.android.xmpp.model.muc.Role;
public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreviewAdapter.ViewHolder>
implements View.OnCreateContextMenuListener {
@@ -52,7 +51,7 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
return;
}
final var contact = user.getContact();
- if (user.getRole() == MucOptions.Role.NONE && contact != null) {
+ if (user.getRole() == Role.NONE && contact != null) {
Toast.makeText(
activity,
activity.getString(
@@ -1,12 +1,11 @@
package eu.siacs.conversations.ui.util;
import android.content.Context;
-import android.os.Bundle;
-
import androidx.annotation.StringRes;
-
+import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.MucOptions;
+import java.util.Map;
public class MucConfiguration {
@@ -116,22 +115,23 @@ public class MucConfiguration {
return builder.toString();
}
- public Bundle toBundle(boolean[] values) {
- Bundle bundle = new Bundle();
+ public Map<String, Object> toBundle(boolean[] values) {
+ final var builder = new ImmutableMap.Builder<String, Object>();
for (int i = 0; i < values.length; ++i) {
final Option option = options[i];
- bundle.putString(option.name, option.values[values[i] ? 0 : 1]);
+ builder.put(option.name, option.values[values[i] ? 0 : 1]);
}
- return bundle;
+ builder.put("muc#roomconfig_persistentroom", true);
+ return builder.buildOrThrow();
}
private static class Option {
public final String name;
- public final String[] values;
+ public final Object[] values;
private Option(String name) {
this.name = name;
- this.values = new String[] {"1", "0"};
+ this.values = new Boolean[] {true, false};
}
private Option(String name, String on, String off) {
@@ -24,6 +24,8 @@ import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.MucUsersActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
public final class MucDetailsContextMenuHelper {
@@ -81,18 +83,17 @@ public final class MucDetailsContextMenuHelper {
}
if ((activity instanceof ConferenceDetailsActivity
|| activity instanceof MucUsersActivity)
- && user.getRole() == MucOptions.Role.NONE) {
+ && user.getRole() == Role.NONE) {
invite.setVisible(true);
}
boolean managePermissionsVisible = false;
- if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)
- && self.getAffiliation().outranks(user.getAffiliation()))
- || self.getAffiliation() == MucOptions.Affiliation.OWNER) {
+ if ((self.ranks(Affiliation.ADMIN) && self.outranks(user.getAffiliation()))
+ || self.getAffiliation() == Affiliation.OWNER) {
if (advancedMode) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ if (!user.ranks(Affiliation.MEMBER)) {
managePermissionsVisible = true;
giveMembership.setVisible(true);
- } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) {
+ } else if (user.getAffiliation() == Affiliation.MEMBER) {
managePermissionsVisible = true;
removeMembership.setVisible(true);
}
@@ -106,25 +107,21 @@ public final class MucDetailsContextMenuHelper {
}
}
}
- if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
- if (isGroupChat
- || advancedMode
- || user.getAffiliation() == MucOptions.Affiliation.OWNER) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (self.ranks(Affiliation.OWNER)) {
+ if (isGroupChat || advancedMode || user.getAffiliation() == Affiliation.OWNER) {
+ if (!user.ranks(Affiliation.OWNER)) {
managePermissionsVisible = true;
giveOwnerPrivileges.setVisible(true);
- } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER) {
+ } else if (user.getAffiliation() == Affiliation.OWNER) {
managePermissionsVisible = true;
removeOwnerPrivileges.setVisible(true);
}
}
- if (!isGroupChat
- || advancedMode
- || user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) {
+ if (!isGroupChat || advancedMode || user.getAffiliation() == Affiliation.ADMIN) {
+ if (!user.ranks(Affiliation.ADMIN)) {
managePermissionsVisible = true;
giveAdminPrivileges.setVisible(true);
- } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
+ } else if (user.getAffiliation() == Affiliation.ADMIN) {
managePermissionsVisible = true;
removeAdminPrivileges.setVisible(true);
}
@@ -132,15 +129,11 @@ public final class MucDetailsContextMenuHelper {
}
managePermissions.setVisible(managePermissionsVisible);
sendPrivateMessage.setVisible(
- !isGroupChat
- && mucOptions.allowPm()
- && user.getRole().ranks(MucOptions.Role.VISITOR));
+ !isGroupChat && mucOptions.allowPm() && user.ranks(Role.VISITOR));
} else {
sendPrivateMessage.setVisible(true);
sendPrivateMessage.setEnabled(
- user != null
- && mucOptions.allowPm()
- && user.getRole().ranks(MucOptions.Role.VISITOR));
+ user != null && mucOptions.allowPm() && user.ranks(Role.VISITOR));
}
}
@@ -171,31 +164,31 @@ public final class MucDetailsContextMenuHelper {
return true;
case R.id.give_admin_privileges:
activity.xmppConnectionService.changeAffiliationInConference(
- conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged);
+ conversation, jid, Affiliation.ADMIN, onAffiliationChanged);
return true;
case R.id.give_membership:
case R.id.remove_admin_privileges:
case R.id.revoke_owner_privileges:
activity.xmppConnectionService.changeAffiliationInConference(
- conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged);
+ conversation, jid, Affiliation.MEMBER, onAffiliationChanged);
return true;
case R.id.give_owner_privileges:
activity.xmppConnectionService.changeAffiliationInConference(
- conversation, jid, MucOptions.Affiliation.OWNER, onAffiliationChanged);
+ conversation, jid, Affiliation.OWNER, onAffiliationChanged);
return true;
case R.id.remove_membership:
activity.xmppConnectionService.changeAffiliationInConference(
- conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged);
+ conversation, jid, Affiliation.NONE, onAffiliationChanged);
return true;
case R.id.remove_from_room:
removeFromRoom(user, activity, onAffiliationChanged);
return true;
case R.id.ban_from_conference:
activity.xmppConnectionService.changeAffiliationInConference(
- conversation, jid, MucOptions.Affiliation.OUTCAST, onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
+ conversation, jid, Affiliation.OUTCAST, onAffiliationChanged);
+ if (user.getRole() != Role.NONE) {
activity.xmppConnectionService.changeRoleInConference(
- conversation, user.getName(), MucOptions.Role.NONE);
+ conversation, user.getName(), Role.NONE);
}
return true;
case R.id.send_private_message:
@@ -210,7 +203,7 @@ public final class MucDetailsContextMenuHelper {
return true;
case R.id.invite:
// TODO use direct invites for public conferences
- if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ if (user.ranks(Affiliation.MEMBER)) {
activity.xmppConnectionService.directInvite(conversation, jid.asBareJid());
} else {
activity.xmppConnectionService.invite(conversation, jid);
@@ -228,13 +221,10 @@ public final class MucDetailsContextMenuHelper {
final Conversation conversation = user.getConversation();
if (conversation.getMucOptions().membersOnly()) {
activity.xmppConnectionService.changeAffiliationInConference(
- conversation,
- user.getRealJid(),
- MucOptions.Affiliation.NONE,
- onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
+ conversation, user.getRealJid(), Affiliation.NONE, onAffiliationChanged);
+ if (user.getRole() != Role.NONE) {
activity.xmppConnectionService.changeRoleInConference(
- conversation, user.getName(), MucOptions.Role.NONE);
+ conversation, user.getName(), Role.NONE);
}
} else {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
@@ -259,11 +249,11 @@ public final class MucDetailsContextMenuHelper {
activity.xmppConnectionService.changeAffiliationInConference(
conversation,
user.getRealJid(),
- MucOptions.Affiliation.OUTCAST,
+ Affiliation.OUTCAST,
onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
+ if (user.getRole() != Role.NONE) {
activity.xmppConnectionService.changeRoleInConference(
- conversation, user.getName(), MucOptions.Role.NONE);
+ conversation, user.getName(), Role.NONE);
}
});
builder.create().show();
@@ -16,6 +16,7 @@ import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import de.gultsch.common.FutureMerger;
import de.gultsch.minidns.AndroidDNSClient;
import de.gultsch.minidns.ResolverResult;
import eu.siacs.conversations.Config;
@@ -115,7 +116,7 @@ public class Resolver {
final var startTls = resolveSrvAsFuture(domain, false);
final var directTls = resolveSrvAsFuture(domain, true);
- final var combined = merge(ImmutableList.of(startTls, directTls));
+ final var combined = FutureMerger.successfulAsList(ImmutableList.of(startTls, directTls));
final var combinedWithFallback =
Futures.transformAsync(
@@ -206,7 +207,7 @@ public class Resolver {
futuresBuilder.add(ipv6s);
}
final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
- return merge(futures);
+ return FutureMerger.successfulAsList(futures);
}
private static ListenableFuture<List<Result>> merge(
@@ -284,13 +285,13 @@ public class Resolver {
Lists.transform(
ImmutableList.copyOf(result.getAnswersOrEmptySet()),
cname -> resolveNoSrvAsFuture(cname.target, false));
- return merge(test);
+ return FutureMerger.successfulAsList(test);
},
MoreExecutors.directExecutor());
futuresBuilder.add(cNameRecordResults);
}
final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
- final var noSrvFallbacks = merge(futures);
+ final var noSrvFallbacks = FutureMerger.successfulAsList(futures);
return Futures.transform(
noSrvFallbacks,
results -> {
@@ -1,6 +1,7 @@
package eu.siacs.conversations.xml;
import androidx.annotation.NonNull;
+import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
@@ -123,6 +124,16 @@ public class Element {
return this;
}
+ public Element setAttribute(final String name, final Enum<?> e) {
+ if (e == null) {
+ this.attributes.remove(name);
+ } else {
+ this.attributes.put(
+ name, CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString()));
+ }
+ return this;
+ }
+
public Element setAttribute(String name, Jid value) {
if (name != null && value != null) {
this.attributes.put(name, value.toString());
@@ -20,6 +20,7 @@ public final class Namespace {
public static final String REACTIONS = "urn:xmpp:reactions:0";
public static final String VCARD_TEMP = "vcard-temp";
public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update";
+ public static final String DIRECT_MUC_INVITATIONS = "jabber:x:conference";
public static final String DELAY = "urn:xmpp:delay";
public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0";
public static final String STREAMS = "http://etherx.jabber.org/streams";
@@ -49,6 +50,9 @@ public final class Namespace {
public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
public static final String PUBSUB_EVENT = PUBSUB + "#event";
public static final String MUC = "http://jabber.org/protocol/muc";
+ public static final String MUC_ADMIN = MUC + "#admin";
+ public static final String MUC_OWNER = MUC + "#owner";
+ public static final String MUC_USER = MUC + "#user";
public static final String MUC_ROOM_INFO = MUC + "#roominfo";
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max";
@@ -96,7 +100,6 @@ public final class Namespace {
public static final String PING = "urn:xmpp:ping";
public static final String PUSH = "urn:xmpp:push:0";
public static final String COMMANDS = "http://jabber.org/protocol/commands";
- public static final String MUC_USER = "http://jabber.org/protocol/muc#user";
public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1";
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
public static final String PRE_AUTHENTICATED_IN_BAND_REGISTRATION = "urn:xmpp:ibr-token:0";
@@ -114,7 +117,6 @@ public final class Namespace {
public static final String MEDIA_ELEMENT = "urn:xmpp:media-element";
public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
-
public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
public static final String PRIVATE_XML_STORAGE = "jabber:iq:private";
@@ -18,6 +18,14 @@ public class IqErrorException extends Exception {
return this.response.getError();
}
+ public Condition getErrorCondition() {
+ final var error = getError();
+ if (error == null) {
+ return null;
+ }
+ return error.getCondition();
+ }
+
private static String getErrorText(final Iq response) {
final var error = response.getError();
final var text = error == null ? null : error.getText();
@@ -15,6 +15,7 @@ import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.NativeBookmarkManager;
import eu.siacs.conversations.xmpp.manager.NickManager;
import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
import eu.siacs.conversations.xmpp.manager.PepManager;
@@ -50,6 +51,7 @@ public class Managers {
MessageDisplayedSynchronizationManager.class,
new MessageDisplayedSynchronizationManager(context, connection))
.put(MultiUserChatManager.class, new MultiUserChatManager(context, connection))
+ .put(NativeBookmarkManager.class, new NativeBookmarkManager(context, connection))
.put(NickManager.class, new NickManager(context, connection))
.put(OfflineMessagesManager.class, new OfflineMessagesManager(context, connection))
.put(PepManager.class, new PepManager(context, connection))
@@ -18,7 +18,9 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ClassToInstanceMap;
+import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback;
@@ -71,6 +73,7 @@ import eu.siacs.conversations.xmpp.manager.AbstractManager;
import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import im.conversations.android.xmpp.Entity;
@@ -85,7 +88,6 @@ import im.conversations.android.xmpp.model.bind2.Bound;
import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
import im.conversations.android.xmpp.model.csi.Active;
import im.conversations.android.xmpp.model.csi.Inactive;
-import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.error.Condition;
import im.conversations.android.xmpp.model.fast.Fast;
import im.conversations.android.xmpp.model.fast.RequestToken;
@@ -135,7 +137,6 @@ import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
-import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -2632,29 +2633,10 @@ public class XmppConnection implements Runnable {
return this.managers.getInstance(clazz);
}
- public List<String> getMucServersWithholdAccount() {
- final List<String> servers = getMucServers();
- servers.remove(account.getDomain().toString());
- return servers;
- }
-
- public List<String> getMucServers() {
- List<String> servers = new ArrayList<>();
- for (final Entry<Jid, InfoQuery> entry :
- getManager(DiscoManager.class).getServerItems().entrySet()) {
- final var value = entry.getValue();
- if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
- && value.hasIdentityWithCategoryAndType("conference", "text")
- && !value.getFeatureStrings().contains("jabber:iq:gateway")
- && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
- servers.add(entry.getKey().toString());
- }
- }
- return servers;
- }
-
- public String getMucServer() {
- return Iterables.getFirst(getMucServers(), null);
+ public Set<Jid> getMucServersWithholdAccount() {
+ final var services = getManager(MultiUserChatManager.class).getServices();
+ return ImmutableSet.copyOf(
+ Collections2.filter(services, s -> !s.equals(account.getDomain())));
}
public int getTimeToNextAttempt(final boolean aggressive) {
@@ -28,7 +28,7 @@ public class AbstractBookmarkManager extends AbstractManager {
final Set<Jid> previousBookmarks = account.getBookmarkedJids();
for (final Bookmark bookmark : bookmarks.values()) {
previousBookmarks.remove(bookmark.getJid().asBareJid());
- service.processModifiedBookmark(bookmark, pep);
+ getManager(BookmarkManager.class).processModifiedBookmark(bookmark, pep);
}
if (pep) {
this.processDeletedBookmarks(previousBookmarks);
@@ -1,150 +1,176 @@
package eu.siacs.conversations.xmpp.manager;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
-import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
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.NodeConfiguration;
-import im.conversations.android.xmpp.model.bookmark2.Conference;
-import im.conversations.android.xmpp.model.bookmark2.Nick;
-import im.conversations.android.xmpp.model.bookmark2.Password;
-import im.conversations.android.xmpp.model.pubsub.Items;
-import im.conversations.android.xmpp.model.pubsub.event.Retract;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-public class BookmarkManager extends AbstractBookmarkManager {
+public class BookmarkManager extends AbstractManager {
- public BookmarkManager(final XmppConnectionService service, XmppConnection connection) {
- super(service, connection);
+ private final XmppConnectionService service;
+
+ public BookmarkManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service.getApplicationContext(), connection);
+ this.service = service;
+ }
+
+ public void request() {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ getManager(NativeBookmarkManager.class).fetch();
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid() + ": not fetching bookmarks. waiting for server to push");
+ } else {
+ getManager(PrivateStorageManager.class).fetchBookmarks();
+ }
}
- public void fetch() {
- final var future = getManager(PepManager.class).fetchItems(Conference.class);
+ public void save(final Conversation conversation, final String name) {
+ final Account account = conversation.getAccount();
+ final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
+ final String nick = conversation.getJid().getResource();
+ if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
+ bookmark.setNick(nick);
+ }
+ if (!TextUtils.isEmpty(name)) {
+ bookmark.setBookmarkName(name);
+ }
+ bookmark.setAutojoin(true);
+ this.create(bookmark);
+ bookmark.setConversation(conversation);
+ }
+
+ public void create(final Bookmark bookmark) {
+ final var account = getAccount();
+ account.putBookmark(bookmark);
+ final ListenableFuture<Void> future;
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ future = getManager(NativeBookmarkManager.class).publish(bookmark);
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks());
+ } else {
+ future =
+ getManager(PrivateStorageManager.class)
+ .publishBookmarks(account.getBookmarks());
+ }
Futures.addCallback(
future,
new FutureCallback<>() {
@Override
- public void onSuccess(final Map<String, Conference> bookmarks) {
- final var builder = new ImmutableMap.Builder<Jid, Bookmark>();
- for (final var entry : bookmarks.entrySet()) {
- final Bookmark bookmark =
- Bookmark.parseFromItem(
- entry.getKey(), entry.getValue(), getAccount());
- if (bookmark == null) {
- continue;
- }
- builder.put(bookmark.getJid(), bookmark);
- }
- processBookmarksInitial(builder.buildKeepingLast(), true);
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": created bookmark");
}
@Override
- public void onFailure(@NonNull final Throwable throwable) {
- Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable);
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not create bookmark",
+ t);
}
},
MoreExecutors.directExecutor());
}
- public void handleItems(final Items items) {
- this.handleItems(items.getItemMap(Conference.class));
- this.handleRetractions(items.getRetractions());
- }
-
- private void handleRetractions(final Collection<Retract> retractions) {
+ public void delete(final Bookmark bookmark) {
final var account = getAccount();
- for (final var retract : retractions) {
- final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
- if (id != null) {
- account.removeBookmark(id);
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
- processDeletedBookmark(id);
- service.updateConversationUi();
- }
+ account.removeBookmark(bookmark);
+ final ListenableFuture<Void> future;
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ future = getManager(NativeBookmarkManager.class).retract(bookmark.getJid().asBareJid());
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks());
+ } else {
+ future =
+ getManager(PrivateStorageManager.class)
+ .publishBookmarks(account.getBookmarks());
}
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark");
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not delete bookmark",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
}
- private void handleItems(final Map<String, Conference> items) {
+ public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
final var account = getAccount();
- for (final var item : items.entrySet()) {
- // TODO parseFromItem can be included in this Manager
- final Bookmark bookmark =
- Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
- if (bookmark == null) {
- continue;
+ final var existingBookmark = conversation.getBookmark();
+ if (existingBookmark == null) {
+ final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
+ bookmark.setAutojoin(true);
+ create(bookmark);
+ } else {
+ if (existingBookmark.autojoin()) {
+ return;
}
- account.putBookmark(bookmark);
- service.processModifiedBookmark(bookmark);
- service.updateConversationUi();
+ existingBookmark.setAutojoin(true);
+ create(existingBookmark);
}
}
- public ListenableFuture<Void> publish(final Bookmark bookmark) {
- final var address = bookmark.getJid();
- final var name = bookmark.getBookmarkName();
- final var nick = bookmark.getNick();
- final String password = bookmark.getPassword();
- final var itemId = address.toString();
- final var conference = new Conference();
- conference.setAutoJoin(bookmark.autojoin());
- if (nick != null) {
- conference.addExtension(new Nick()).setContent(nick);
- }
- if (name != null) {
- conference.setConferenceName(name);
- }
- if (password != null) {
- conference.addExtension(new Password()).setContent(password);
+ public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
+ final var existing = this.service.find(bookmark);
+ if (existing != null) {
+ if (existing.getMode() != Conversation.MODE_MULTI) {
+ return;
+ }
+ bookmark.setConversation(existing);
+ if (pep && !bookmark.autojoin()) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": archiving conference ("
+ + existing.getJid()
+ + ") after receiving pep");
+ service.archiveConversation(existing, false);
+ } else {
+ final MucOptions mucOptions = existing.getMucOptions();
+ if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
+ final String current = mucOptions.getActualNick();
+ final String proposed = mucOptions.getProposedNickPure();
+ if (current != null && !current.equals(proposed)) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": proposed nick changed after bookmark push "
+ + current
+ + "->"
+ + proposed);
+ getManager(MultiUserChatManager.class).join(existing);
+ }
+ } else {
+ getManager(MultiUserChatManager.class).checkMucRequiresRename(existing);
+ }
+ }
+ } else if (bookmark.autojoin()) {
+ final var fresh =
+ this.service.findOrCreateConversation(
+ getAccount(), bookmark.getFullJid(), true, true, false);
+ bookmark.setConversation(fresh);
}
- conference.addExtension(bookmark.getExtensions());
- return Futures.transform(
- getManager(PepManager.class)
- .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS),
- result -> null,
- MoreExecutors.directExecutor());
- }
-
- public ListenableFuture<Void> retract(final Jid address) {
- final var itemId = address.toString();
- return Futures.transform(
- getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2),
- result -> null,
- MoreExecutors.directExecutor());
- }
-
- private void deleteAllItems() {
- final var account = getAccount();
- final var previous = account.getBookmarkedJids();
- account.setBookmarks(Collections.emptyMap());
- processDeletedBookmarks(previous);
- }
-
- public void handleDelete() {
- Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": deleted bookmarks node");
- this.deleteAllItems();
- }
-
- public void handlePurge() {
- Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": purged bookmarks");
- this.deleteAllItems();
- }
-
- public boolean hasFeature() {
- final var pep = getManager(PepManager.class);
- final var disco = getManager(DiscoManager.class);
- return pep.hasPublishOptions()
- && pep.hasConfigNodeMax()
- && disco.hasAccountFeature(Namespace.BOOKMARKS2_COMPAT);
}
}
@@ -336,7 +336,7 @@ public class DiscoManager extends AbstractManager {
if (appSettings.isBroadcastLastActivity()) {
features.add(Namespace.IDLE);
}
- if (getManager(BookmarkManager.class).hasFeature()) {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
features.add(Namespace.BOOKMARKS2 + "+notify");
} else {
features.add(Namespace.BOOKMARKS + "+notify");
@@ -24,7 +24,7 @@ public class LegacyBookmarkManager extends AbstractBookmarkManager {
public void handleItems(final Items items) {
final var account = this.getAccount();
if (this.hasConversion()) {
- if (getManager(BookmarkManager.class).hasFeature()) {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
Log.w(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -1,14 +1,988 @@
package eu.siacs.conversations.xmpp.manager;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import de.gultsch.common.FutureMerger;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.StringUtils;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.IqErrorException;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.conference.DirectInvite;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.hints.NoCopy;
+import im.conversations.android.xmpp.model.hints.NoStore;
+import im.conversations.android.xmpp.model.jabber.Subject;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.History;
+import im.conversations.android.xmpp.model.muc.MultiUserChat;
+import im.conversations.android.xmpp.model.muc.Password;
+import im.conversations.android.xmpp.model.muc.Role;
+import im.conversations.android.xmpp.model.muc.admin.Item;
+import im.conversations.android.xmpp.model.muc.admin.MucAdmin;
+import im.conversations.android.xmpp.model.muc.owner.Destroy;
+import im.conversations.android.xmpp.model.muc.owner.MucOwner;
+import im.conversations.android.xmpp.model.muc.user.Invite;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
+import im.conversations.android.xmpp.model.pgp.Signed;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Message;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
public class MultiUserChatManager extends AbstractManager {
private final XmppConnectionService service;
+ private final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
+ private final Set<Conversation> inProgressConferencePings = new HashSet<>();
+
public MultiUserChatManager(final XmppConnectionService service, XmppConnection connection) {
super(service.getApplicationContext(), connection);
this.service = service;
}
+
+ public ListenableFuture<Void> join(final Conversation conversation) {
+ return join(conversation, true);
+ }
+
+ private ListenableFuture<Void> join(
+ final Conversation conversation, final boolean autoPushConfiguration) {
+ final var account = getAccount();
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.add(conversation);
+ }
+ if (Config.MUC_LEAVE_BEFORE_JOIN) {
+ unavailable(conversation);
+ }
+ conversation.resetMucOptions();
+ conversation.getMucOptions().setAutoPushConfiguration(autoPushConfiguration);
+ conversation.setHasMessagesLeftOnServer(false);
+ final var disco = fetchDiscoInfo(conversation);
+
+ final var caughtDisco =
+ Futures.catchingAsync(
+ disco,
+ IqErrorException.class,
+ ex -> {
+ if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException(
+ "conversation got archived before disco returned"));
+ }
+ Log.d(Config.LOGTAG, "error fetching disco#info", ex);
+ final var iqError = ex.getError();
+ if (iqError != null
+ && iqError.getCondition()
+ instanceof Condition.RemoteServerNotFound) {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ }
+ conversation
+ .getMucOptions()
+ .setError(MucOptions.Error.SERVER_NOT_FOUND);
+ service.updateConversationUi();
+ return Futures.immediateFailedFuture(ex);
+ } else {
+ return Futures.immediateFuture(new InfoQuery());
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ return Futures.transform(
+ caughtDisco,
+ v -> {
+ checkConfigurationSendPresenceFetchHistory(conversation);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> joinFollowingInvite(final Conversation conversation) {
+ // TODO this special treatment is probably unnecessary; just always make sure the bookmark
+ // exists
+ return Futures.transform(
+ join(conversation),
+ v -> {
+ // we used to do this only for private groups
+ final Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ if (bookmark.autojoin()) {
+ return null;
+ }
+ bookmark.setAutojoin(true);
+ getManager(BookmarkManager.class).create(bookmark);
+ } else {
+ getManager(BookmarkManager.class).save(conversation, null);
+ }
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void checkConfigurationSendPresenceFetchHistory(final Conversation conversation) {
+
+ Account account = conversation.getAccount();
+ final MucOptions mucOptions = conversation.getMucOptions();
+
+ if (mucOptions.nonanonymous()
+ && !mucOptions.membersOnly()
+ && !conversation.getBooleanAttribute("accept_non_anonymous", false)) {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ }
+ mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
+ service.updateConversationUi();
+ return;
+ }
+
+ final Jid joinJid = mucOptions.getSelf().getFullJid();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid().toString()
+ + ": joining conversation "
+ + joinJid.toString());
+
+ final var x = new MultiUserChat();
+
+ if (mucOptions.getPassword() != null) {
+ x.addExtension(new Password(mucOptions.getPassword()));
+ }
+
+ final var history = x.addExtension(new History());
+
+ if (mucOptions.mamSupport()) {
+ // Use MAM instead of the limited muc history to get history
+ history.setMaxStanzas(0);
+ } else {
+ // Fallback to muc history
+ history.setSince(conversation.getLastMessageTransmitted().getTimestamp());
+ }
+ available(joinJid, mucOptions.nonanonymous(), x);
+ if (!joinJid.equals(conversation.getJid())) {
+ conversation.setContactJid(joinJid);
+ getDatabase().updateConversation(conversation);
+ }
+
+ if (mucOptions.mamSupport()) {
+ this.service.getMessageArchiveService().catchupMUC(conversation);
+ }
+ if (mucOptions.isPrivateAndNonAnonymous()) {
+ fetchMembers(conversation);
+ }
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ this.service.sendUnsentMessages(conversation);
+ }
+ }
+
+ public ListenableFuture<Conversation> createPrivateGroupChat(
+ final String name, final Collection<Jid> addresses) {
+ final var service = getService();
+ if (service == null) {
+ return Futures.immediateFailedFuture(new IllegalStateException("No MUC service found"));
+ }
+ final var address = Jid.ofLocalAndDomain(CryptoHelper.pronounceable(), service);
+ final var conversation =
+ this.service.findOrCreateConversation(getAccount(), address, true, false, true);
+ final var join = this.join(conversation, false);
+ final var configured =
+ Futures.transformAsync(
+ join,
+ v -> {
+ final var options =
+ configWithName(defaultGroupChatConfiguration(), name);
+ return pushConfiguration(conversation, options);
+ },
+ MoreExecutors.directExecutor());
+
+ // TODO add catching to 'configured' to archive the chat again
+
+ return Futures.transform(
+ configured,
+ c -> {
+ for (var invitee : addresses) {
+ this.service.invite(conversation, invitee);
+ }
+ final var account = getAccount();
+ for (final var resource :
+ account.getSelfContact().getPresences().toResourceArray()) {
+ Jid other = getAccount().getJid().withResource(resource);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": sending direct invite to "
+ + other);
+ this.service.directInvite(conversation, other);
+ }
+ getManager(BookmarkManager.class).save(conversation, name);
+ return conversation;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Conversation> createPublicChannel(
+ final Jid address, final String name) {
+
+ final var conversation =
+ this.service.findOrCreateConversation(getAccount(), address, true, false, true);
+
+ final var join = this.join(conversation, false);
+ final var configuration =
+ Futures.transformAsync(
+ join,
+ v -> {
+ final var options = configWithName(defaultChannelConfiguration(), name);
+ return pushConfiguration(conversation, options);
+ },
+ MoreExecutors.directExecutor());
+
+ // TODO mostly ignore configuration error
+
+ return Futures.transform(
+ configuration,
+ v -> {
+ getManager(BookmarkManager.class).save(conversation, name);
+ return conversation;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void leave(final Conversation conversation) {
+ final var mucOptions = conversation.getMucOptions();
+ mucOptions.setOffline();
+ getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
+ unavailable(conversation);
+ }
+
+ public void handlePresence(final Presence presence) {}
+
+ public void handleStatusMessage(final Message message) {
+ final var from = Jid.Invalid.getNullForInvalid(message.getFrom());
+ final var mucUser = message.getExtension(MucUser.class);
+ if (from == null || from.isFullJid() || mucUser == null) {
+ return;
+ }
+ final var conversation = this.service.find(getAccount(), from);
+ if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) {
+ return;
+ }
+ for (final var status : mucUser.getStatus()) {
+ handleStatusCode(conversation, status);
+ }
+ final var item = mucUser.getItem();
+ if (item == null) {
+ return;
+ }
+ final var user = itemToUser(conversation, item, null);
+ this.handleAffiliationChange(conversation, user);
+ }
+
+ private void handleAffiliationChange(
+ final Conversation conversation, final MucOptions.User user) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid()
+ + ": changing affiliation for "
+ + user.getRealJid()
+ + " to "
+ + user.getAffiliation()
+ + " in "
+ + conversation.getJid().asBareJid());
+ if (user.realJidMatchesAccount()) {
+ return;
+ }
+ final var mucOptions = conversation.getMucOptions();
+ final boolean isNew = mucOptions.updateUser(user);
+ final var avatarService = this.service.getAvatarService();
+ if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
+ avatarService.clear(mucOptions);
+ }
+ avatarService.clear(user);
+ this.service.updateMucRosterUi();
+ this.service.updateConversationUi();
+ if (user.ranks(Affiliation.MEMBER)) {
+ fetchDeviceIdsIfNeeded(isNew, user);
+ } else {
+ final var jid = user.getRealJid();
+ final var cryptoTargets = conversation.getAcceptedCryptoTargets();
+ if (cryptoTargets.remove(user.getRealJid())) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": removed "
+ + jid
+ + " from crypto targets of "
+ + conversation.getName());
+ conversation.setAcceptedCryptoTargets(cryptoTargets);
+ getDatabase().updateConversation(conversation);
+ }
+ }
+ }
+
+ private void fetchDeviceIdsIfNeeded(final boolean isNew, final MucOptions.User user) {
+ final var contact = user.getContact();
+ final var mucOptions = user.getMucOptions();
+ final var axolotlService = connection.getAxolotlService();
+ if (isNew
+ && user.getRealJid() != null
+ && mucOptions.isPrivateAndNonAnonymous()
+ && (contact == null || !contact.mutualPresenceSubscription())
+ && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
+ axolotlService.fetchDeviceIds(user.getRealJid());
+ }
+ }
+
+ private void handleStatusCode(final Conversation conversation, final int status) {
+ if ((status >= 170 && status <= 174) || (status >= 102 && status <= 104)) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": fetching disco#info on status code "
+ + status);
+ getManager(MultiUserChatManager.class).fetchDiscoInfo(conversation);
+ }
+ }
+
+ public ListenableFuture<Void> fetchDiscoInfo(final Conversation conversation) {
+ final var address = conversation.getJid().asBareJid();
+ final var future =
+ connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null);
+ return Futures.transform(
+ future,
+ infoQuery -> {
+ setDiscoInfo(conversation, infoQuery);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setDiscoInfo(final Conversation conversation, final InfoQuery result) {
+ final var account = conversation.getAccount();
+ final var address = conversation.getJid().asBareJid();
+ final var avatarHash =
+ result.getServiceDiscoveryExtension(
+ Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash");
+ if (VCardUpdate.isValidSHA1(avatarHash)) {
+ connection.getManager(AvatarManager.class).handleVCardUpdate(address, avatarHash);
+ }
+ final MucOptions mucOptions = conversation.getMucOptions();
+ final Bookmark bookmark = conversation.getBookmark();
+ final boolean sameBefore =
+ StringUtils.equals(
+ bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
+
+ final var hadOccupantId = mucOptions.occupantId();
+ if (mucOptions.updateConfiguration(result)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": muc configuration changed for "
+ + conversation.getJid().asBareJid());
+ getDatabase().updateConversation(conversation);
+ }
+
+ final var hasOccupantId = mucOptions.occupantId();
+
+ if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
+ final var me = mucOptions.getSelf().getFullJid();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": gained support for occupant-id in "
+ + me
+ + ". resending presence");
+ this.available(me, mucOptions.nonanonymous());
+ }
+
+ if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
+ if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ }
+ this.service.updateConversationUi();
+ }
+
+ public void resendPresence(final Conversation conversation) {
+ final MucOptions mucOptions = conversation.getMucOptions();
+ if (mucOptions.online()) {
+ available(mucOptions.getSelf().getFullJid(), mucOptions.nonanonymous());
+ }
+ }
+
+ private void available(
+ final Jid address, final boolean nonAnonymous, final Extension... extensions) {
+ final var presenceManager = getManager(PresenceManager.class);
+ final var account = getAccount();
+ final String pgpSignature = account.getPgpSignature();
+ if (nonAnonymous && pgpSignature != null) {
+ final String message = account.getPresenceStatusMessage();
+ presenceManager.available(
+ address, message, combine(extensions, new Signed(pgpSignature)));
+ } else {
+ presenceManager.available(address, extensions);
+ }
+ }
+
+ public void unavailable(final Conversation conversation) {
+ final var mucOptions = conversation.getMucOptions();
+ getManager(PresenceManager.class).unavailable(mucOptions.getSelf().getFullJid());
+ }
+
+ private static Extension[] combine(final Extension[] extensions, final Extension extension) {
+ return new ImmutableList.Builder<Extension>()
+ .addAll(Arrays.asList(extensions))
+ .add(extension)
+ .build()
+ .toArray(new Extension[0]);
+ }
+
+ public ListenableFuture<Void> pushConfiguration(
+ final Conversation conversation, final Map<String, Object> input) {
+ final var address = conversation.getJid().asBareJid();
+ final var configuration = modifyBestInteroperability(input);
+
+ if (configuration.get("muc#roomconfig_whois") instanceof String whois
+ && whois.equals("anyone")) {
+ conversation.setAttribute("accept_non_anonymous", true);
+ getDatabase().updateConversation(conversation);
+ }
+
+ final var future = fetchConfigurationForm(address);
+ return Futures.transformAsync(
+ future,
+ current -> {
+ final var modified = current.submit(configuration);
+ return submitConfigurationForm(address, modified);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Data> fetchConfigurationForm(final Jid address) {
+ final var iq = new Iq(Iq.Type.GET, new MucOwner());
+ iq.setTo(address);
+ Log.d(Config.LOGTAG, "fetching configuration form: " + iq);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ response -> {
+ final var mucOwner = response.getExtension(MucOwner.class);
+ if (mucOwner == null) {
+ throw new IllegalStateException("Missing MucOwner element in response");
+ }
+ return mucOwner.getConfiguration();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> submitConfigurationForm(final Jid address, final Data data) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var mucOwner = iq.addExtension(new MucOwner());
+ mucOwner.addExtension(data);
+ Log.d(Config.LOGTAG, "pushing configuration form: " + iq);
+ return Futures.transform(
+ this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> fetchMembers(final Conversation conversation) {
+ final var futures =
+ Collections2.transform(
+ Arrays.asList(Affiliation.OWNER, Affiliation.ADMIN, Affiliation.MEMBER),
+ a -> fetchAffiliations(conversation, a));
+ ListenableFuture<List<MucOptions.User>> future = FutureMerger.allAsList(futures);
+ return Futures.transform(
+ future,
+ members -> {
+ setMembers(conversation, members);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setMembers(final Conversation conversation, final List<MucOptions.User> users) {
+ for (final var user : users) {
+ if (user.realJidMatchesAccount()) {
+ continue;
+ }
+ boolean isNew = conversation.getMucOptions().updateUser(user);
+ fetchDeviceIdsIfNeeded(isNew, user);
+ }
+ final var mucOptions = conversation.getMucOptions();
+ final var members = mucOptions.getMembers(true);
+ final var cryptoTargets = conversation.getAcceptedCryptoTargets();
+ boolean changed = false;
+ for (final var iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) {
+ final var jid = iterator.next();
+ if (!members.contains(jid) && !members.contains(jid.getDomain())) {
+ iterator.remove();
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": removed "
+ + jid
+ + " from crypto targets of "
+ + conversation.getName());
+ changed = true;
+ }
+ }
+ if (changed) {
+ conversation.setAcceptedCryptoTargets(cryptoTargets);
+ getDatabase().updateConversation(conversation);
+ }
+ // TODO only when room has no avatar
+ this.service.getAvatarService().clear(mucOptions);
+ this.service.updateMucRosterUi();
+ this.service.updateConversationUi();
+ }
+
+ private ListenableFuture<Collection<MucOptions.User>> fetchAffiliations(
+ final Conversation conversation, final Affiliation affiliation) {
+ final var iq = new Iq(Iq.Type.GET);
+ iq.setTo(conversation.getJid().asBareJid());
+ iq.addExtension(new MucAdmin()).addExtension(new Item()).setAffiliation(affiliation);
+ return Futures.transform(
+ this.connection.sendIqPacket(iq),
+ response -> {
+ final var mucAdmin = response.getExtension(MucAdmin.class);
+ if (mucAdmin == null) {
+ throw new IllegalStateException("No query in response");
+ }
+ return Collections2.transform(
+ mucAdmin.getItems(), i -> itemToUser(conversation, i, null));
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> changeUsername(
+ final Conversation conversation, final String username) {
+
+ // TODO when online send normal available presence
+ // TODO when not online do a normal join
+
+ final Bookmark bookmark = conversation.getBookmark();
+ final MucOptions options = conversation.getMucOptions();
+ final Jid joinJid = options.createJoinJid(username);
+ if (joinJid == null) {
+ return Futures.immediateFailedFuture(new IllegalArgumentException());
+ }
+
+ if (options.online()) {
+ final SettableFuture<Void> renameFuture = SettableFuture.create();
+ options.setOnRenameListener(
+ new MucOptions.OnRenameListener() {
+
+ @Override
+ public void onSuccess() {
+ renameFuture.set(null);
+ }
+
+ @Override
+ public void onFailure() {
+ renameFuture.setException(new IllegalStateException());
+ }
+ });
+
+ available(joinJid, options.nonanonymous());
+
+ if (username.equals(MucOptions.defaultNick(getAccount()))
+ && bookmark != null
+ && bookmark.getNick() != null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": removing nick from bookmark for "
+ + bookmark.getJid());
+ bookmark.setNick(null);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ return renameFuture;
+ } else {
+ conversation.setContactJid(joinJid);
+ getDatabase().updateConversation(conversation);
+ if (bookmark != null) {
+ bookmark.setNick(username);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ join(conversation);
+ return Futures.immediateVoidFuture();
+ }
+ }
+
+ public void checkMucRequiresRename(final Conversation conversation) {
+ final var options = conversation.getMucOptions();
+ if (!options.online()) {
+ return;
+ }
+ final String current = options.getActualNick();
+ final String proposed = options.getProposedNickPure();
+ if (current == null || current.equals(proposed)) {
+ return;
+ }
+ final Jid joinJid = options.createJoinJid(proposed);
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: muc rename required %s (was: %s)",
+ getAccount().getJid().asBareJid(), joinJid, current));
+ available(joinJid, options.nonanonymous());
+ }
+
+ public void setPassword(final Conversation conversation, final String password) {
+ final var bookmark = conversation.getBookmark();
+ conversation.getMucOptions().setPassword(password);
+ if (bookmark != null) {
+ bookmark.setAutojoin(true);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ getDatabase().updateConversation(conversation);
+ this.join(conversation);
+ }
+
+ public void pingAndRejoin(final Conversation conversation) {
+ final Account account = getAccount();
+ synchronized (this.inProgressConferenceJoins) {
+ if (this.inProgressConferenceJoins.contains(conversation)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": canceling muc self ping because join is already under way");
+ return;
+ }
+ }
+ synchronized (this.inProgressConferencePings) {
+ if (!this.inProgressConferencePings.add(conversation)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": canceling muc self ping because ping is already under way");
+ return;
+ }
+ }
+ final Jid self = conversation.getMucOptions().getSelf().getFullJid();
+ final var future = getManager(PingManager.class).ping(self);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Iq result) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " came back fine");
+ synchronized (MultiUserChatManager.this.inProgressConferencePings) {
+ MultiUserChatManager.this.inProgressConferencePings.remove(
+ conversation);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ synchronized (MultiUserChatManager.this.inProgressConferencePings) {
+ MultiUserChatManager.this.inProgressConferencePings.remove(
+ conversation);
+ }
+ if (throwable instanceof IqErrorException iqErrorException) {
+ final var condition = iqErrorException.getErrorCondition();
+ if (condition instanceof Condition.ServiceUnavailable
+ || condition instanceof Condition.FeatureNotImplemented
+ || condition instanceof Condition.ItemNotFound) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " came back as ignorable error");
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " failed. attempting rejoin");
+ join(conversation);
+ }
+ }
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> destroy(final Jid address) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var mucOwner = iq.addExtension(new MucOwner());
+ mucOwner.addExtension(new Destroy());
+ return Futures.transform(
+ connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> setAffiliation(
+ final Conversation conversation, final Affiliation affiliation, Jid user) {
+ return setAffiliation(conversation, affiliation, Collections.singleton(user));
+ }
+
+ public ListenableFuture<Void> setAffiliation(
+ final Conversation conversation,
+ final Affiliation affiliation,
+ final Collection<Jid> users) {
+ final var address = conversation.getJid().asBareJid();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var admin = iq.addExtension(new MucAdmin());
+ for (final var user : users) {
+ final var item = admin.addExtension(new Item());
+ item.setJid(user);
+ item.setAffiliation(affiliation);
+ }
+ return Futures.transform(
+ this.connection.sendIqPacket(iq),
+ response -> {
+ // TODO figure out what this was meant to do
+ // is this a work around for some servers not sending notifications when
+ // changing the affiliation of people not in the room? this would explain this
+ // firing only when getRole == None
+ final var mucOptions = conversation.getMucOptions();
+ for (final var user : users) {
+ mucOptions.changeAffiliation(user, affiliation);
+ }
+ service.getAvatarService().clear(mucOptions);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> setRole(final Jid address, final Role role, final String user) {
+ return setRole(address, role, Collections.singleton(user));
+ }
+
+ public ListenableFuture<Void> setRole(
+ final Jid address, final Role role, final Collection<String> users) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var admin = iq.addExtension(new MucAdmin());
+ for (final var user : users) {
+ final var item = admin.addExtension(new Item());
+ item.setNick(user);
+ item.setRole(role);
+ }
+ return Futures.transform(
+ this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor());
+ }
+
+ public void setSubject(final Conversation conversation, final String subject) {
+ final var message = new Message();
+ message.setType(Message.Type.GROUPCHAT);
+ message.setTo(conversation.getJid().asBareJid());
+ message.addExtension(new Subject(subject));
+ connection.sendMessagePacket(message);
+ }
+
+ public void invite(final Conversation conversation, final Jid address) {
+ Log.d(
+ Config.LOGTAG,
+ conversation.getAccount().getJid().asBareJid()
+ + ": inviting "
+ + address
+ + " to "
+ + conversation.getJid().asBareJid());
+ final MucOptions.User user =
+ conversation.getMucOptions().findUserByRealJid(address.asBareJid());
+ if (user == null || user.getAffiliation() == Affiliation.OUTCAST) {
+ this.setAffiliation(conversation, Affiliation.NONE, address);
+ }
+
+ final var packet = new Message();
+ packet.setTo(conversation.getJid().asBareJid());
+ final var x = packet.addExtension(new MucUser());
+ final var invite = x.addExtension(new Invite());
+ invite.setTo(address.asBareJid());
+ connection.sendMessagePacket(packet);
+ }
+
+ public void directInvite(final Conversation conversation, final Jid address) {
+ final var message = new Message();
+ message.setTo(address);
+ final var directInvite = message.addExtension(new DirectInvite());
+ directInvite.setJid(conversation.getJid().asBareJid());
+ final var password = conversation.getMucOptions().getPassword();
+ if (password != null) {
+ directInvite.setPassword(password);
+ }
+ if (address.isFullJid()) {
+ message.addExtension(new NoStore());
+ message.addExtension(new NoCopy());
+ }
+ this.connection.sendMessagePacket(message);
+ }
+
+ public boolean isJoinInProgress(final Conversation conversation) {
+ synchronized (this.inProgressConferenceJoins) {
+ if (conversation.getMode() == Conversational.MODE_MULTI) {
+ final boolean inProgress = this.inProgressConferenceJoins.contains(conversation);
+ if (inProgress) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": holding back message to group. join in progress");
+ }
+ return inProgress;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public void clearInProgress() {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.clear();
+ }
+ synchronized (this.inProgressConferencePings) {
+ this.inProgressConferencePings.clear();
+ }
+ }
+
+ public Jid getService() {
+ return Iterables.getFirst(this.getServices(), null);
+ }
+
+ public List<Jid> getServices() {
+ final var builder = new ImmutableList.Builder<Jid>();
+ for (final var entry : getManager(DiscoManager.class).getServerItems().entrySet()) {
+ final var value = entry.getValue();
+ if (value.getFeatureStrings().contains(Namespace.MUC)
+ && value.hasIdentityWithCategoryAndType("conference", "text")
+ && !value.getFeatureStrings().contains("jabber:iq:gateway")
+ && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
+ builder.add(entry.getKey());
+ }
+ }
+ return builder.build();
+ }
+
+ public static MucOptions.User itemToUser(
+ final Conversation conference,
+ im.conversations.android.xmpp.model.muc.Item item,
+ final Jid from) {
+ final var affiliation = item.getAffiliation();
+ final var role = item.getRole();
+ final var nick = item.getNick();
+ final Jid fullAddress;
+ if (from != null && from.isFullJid()) {
+ fullAddress = from;
+ } else if (Strings.isNullOrEmpty(nick)) {
+ fullAddress = null;
+ } else {
+ fullAddress = ofNick(conference, nick);
+ }
+ final Jid realJid = item.getAttributeAsJid("jid");
+ MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullAddress);
+ if (Jid.Invalid.isValid(realJid)) {
+ user.setRealJid(realJid);
+ }
+ user.setAffiliation(affiliation);
+ user.setRole(role);
+ return user;
+ }
+
+ private static Jid ofNick(final Conversation conversation, final String nick) {
+ try {
+ return conversation.getJid().withResource(nick);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ private static Map<String, Object> modifyBestInteroperability(
+ final Map<String, Object> unmodified) {
+ final var builder = new ImmutableMap.Builder<String, Object>();
+ builder.putAll(unmodified);
+
+ if (unmodified.get("muc#roomconfig_moderatedroom") instanceof Boolean moderated) {
+ builder.put("members_by_default", !moderated);
+ }
+ if (unmodified.get("muc#roomconfig_allowpm") instanceof String allowPm) {
+ // ejabberd :-/
+ final boolean allow = "anyone".equals(allowPm);
+ builder.put("allow_private_messages", allow);
+ builder.put("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
+ }
+
+ if (unmodified.get("muc#roomconfig_allowinvites") instanceof Boolean allowInvites) {
+ // TODO check that this actually does something useful?
+ builder.put(
+ "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", allowInvites);
+ }
+
+ return builder.buildOrThrow();
+ }
+
+ private static Map<String, Object> configWithName(
+ final Map<String, Object> unmodified, final String name) {
+ if (Strings.isNullOrEmpty(name)) {
+ return unmodified;
+ }
+ return new ImmutableMap.Builder<String, Object>()
+ .putAll(unmodified)
+ .put("muc#roomconfig_roomname", name)
+ .buildKeepingLast();
+ }
+
+ public static Map<String, Object> defaultGroupChatConfiguration() {
+ return new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_membersonly", true)
+ .put("muc#roomconfig_publicroom", false)
+ .put("muc#roomconfig_whois", "anyone")
+ .put("muc#roomconfig_changesubject", false)
+ .put("muc#roomconfig_allowinvites", false)
+ .put("muc#roomconfig_enablearchiving", true) // prosody
+ .put("mam", true) // ejabberd community
+ .put("muc#roomconfig_mam", true) // ejabberd saas
+ .buildOrThrow();
+ }
+
+ public static Map<String, Object> defaultChannelConfiguration() {
+ return new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_membersonly", false)
+ .put("muc#roomconfig_publicroom", true)
+ .put("muc#roomconfig_whois", "moderators")
+ .put("muc#roomconfig_changesubject", false)
+ .put("muc#roomconfig_enablearchiving", true) // prosody
+ .put("mam", true) // ejabberd community
+ .put("muc#roomconfig_mam", true) // ejabberd saas
+ .buildOrThrow();
+ }
}
@@ -0,0 +1,172 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+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.NodeConfiguration;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
+import im.conversations.android.xmpp.model.bookmark2.Nick;
+import im.conversations.android.xmpp.model.bookmark2.Password;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.pubsub.event.Retract;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public class NativeBookmarkManager extends AbstractBookmarkManager {
+
+ public NativeBookmarkManager(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ }
+
+ public void fetch() {
+ final var future = getManager(PepManager.class).fetchItems(Conference.class);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Map<String, Conference> bookmarks) {
+ final var builder = new ImmutableMap.Builder<Jid, Bookmark>();
+ for (final var entry : bookmarks.entrySet()) {
+ final Bookmark bookmark =
+ itemToBookmark(entry.getKey(), entry.getValue(), getAccount());
+ if (bookmark == null) {
+ continue;
+ }
+ builder.put(bookmark.getJid(), bookmark);
+ }
+ processBookmarksInitial(builder.buildKeepingLast(), true);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void handleItems(final Items items) {
+ this.handleItems(items.getItemMap(Conference.class));
+ this.handleRetractions(items.getRetractions());
+ }
+
+ private void handleRetractions(final Collection<Retract> retractions) {
+ final var account = getAccount();
+ for (final var retract : retractions) {
+ final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
+ if (id != null) {
+ account.removeBookmark(id);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
+ processDeletedBookmark(id);
+ service.updateConversationUi();
+ }
+ }
+ }
+
+ private void handleItems(final Map<String, Conference> items) {
+ final var account = getAccount();
+ for (final var item : items.entrySet()) {
+ final Bookmark bookmark = itemToBookmark(item.getKey(), item.getValue(), account);
+ if (bookmark == null) {
+ continue;
+ }
+ account.putBookmark(bookmark);
+ getManager(BookmarkManager.class).processModifiedBookmark(bookmark, true);
+ service.updateConversationUi();
+ }
+ }
+
+ public ListenableFuture<Void> publish(final Bookmark bookmark) {
+ final var address = bookmark.getJid();
+ final var name = bookmark.getBookmarkName();
+ final var nick = bookmark.getNick();
+ final String password = bookmark.getPassword();
+ final var itemId = address.toString();
+ final var conference = new Conference();
+ conference.setAutoJoin(bookmark.autojoin());
+ if (nick != null) {
+ conference.addExtension(new Nick()).setContent(nick);
+ }
+ if (name != null) {
+ conference.setConferenceName(name);
+ }
+ if (password != null) {
+ conference.addExtension(new Password()).setContent(password);
+ }
+ conference.addExtension(bookmark.getExtensions());
+ return Futures.transform(
+ getManager(PepManager.class)
+ .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS),
+ result -> null,
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> retract(final Jid address) {
+ final var itemId = address.toString();
+ return Futures.transform(
+ getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2),
+ result -> null,
+ MoreExecutors.directExecutor());
+ }
+
+ private void deleteAllItems() {
+ final var account = getAccount();
+ final var previous = account.getBookmarkedJids();
+ account.setBookmarks(Collections.emptyMap());
+ processDeletedBookmarks(previous);
+ }
+
+ public void handleDelete() {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": deleted bookmarks node");
+ this.deleteAllItems();
+ }
+
+ public void handlePurge() {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": purged bookmarks");
+ this.deleteAllItems();
+ }
+
+ public boolean hasFeature() {
+ final var pep = getManager(PepManager.class);
+ final var disco = getManager(DiscoManager.class);
+ return pep.hasPublishOptions()
+ && pep.hasConfigNodeMax()
+ && disco.hasAccountFeature(Namespace.BOOKMARKS2_COMPAT);
+ }
+
+ private static Bookmark itemToBookmark(
+ final String id, final Conference conference, final Account account) {
+ if (id == null || conference == null) {
+ return null;
+ }
+ final var jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
+ if (jid == null || jid.isFullJid()) {
+ return null;
+ }
+ final Bookmark bookmark = new Bookmark(account, jid);
+
+ // TODO use proper API
+
+ bookmark.setBookmarkName(conference.getAttribute("name"));
+ bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
+ bookmark.setNick(conference.findChildContent("nick"));
+ bookmark.setPassword(conference.findChildContent("password"));
+ final var extensions = conference.getExtensions();
+ if (extensions != null) {
+ bookmark.setExtensions(conference.getExtensions());
+ }
+ return bookmark;
+ }
+}
@@ -4,7 +4,9 @@ import android.content.Context;
import androidx.annotation.NonNull;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.ping.Ping;
import im.conversations.android.xmpp.model.stanza.Iq;
@@ -24,6 +26,12 @@ public class PingManager extends AbstractManager {
}
}
+ public ListenableFuture<Iq> ping(final Jid address) {
+ final var iq = new Iq(Iq.Type.GET, new Ping());
+ iq.setTo(address);
+ return this.connection.sendIqPacket(iq);
+ }
+
public void ping(final Runnable runnable) {
final var pingFuture = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping()));
Futures.addCallback(
@@ -105,9 +105,7 @@ public class PresenceManager extends AbstractManager {
presence.setAvailability(availability);
presence.setStatus(message);
if (pgpSignature != null) {
- final var signed = new Signed();
- signed.setContent(pgpSignature);
- presence.addExtension(signed);
+ presence.addExtension(new Signed(pgpSignature));
}
final var lastActivity = service.getLastActivity();
@@ -127,8 +125,13 @@ public class PresenceManager extends AbstractManager {
}
public void available(final Jid to, final Extension... extensions) {
+ available(to, null, extensions);
+ }
+
+ public void available(final Jid to, final String message, final Extension... extensions) {
final var presence = new Presence();
presence.setTo(to);
+ presence.setStatus(message);
for (final var extension : extensions) {
presence.addExtension(extension);
}
@@ -176,7 +176,7 @@ public class PubSubManager extends AbstractManager {
final var isFromBare = from == null || from.isBareJid();
final var node = items.getNode();
if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
- getManager(BookmarkManager.class).handleItems(items);
+ getManager(NativeBookmarkManager.class).handleItems(items);
return;
}
if (connection.fromAccount(message) && Namespace.BOOKMARKS.equals(node)) {
@@ -205,7 +205,7 @@ public class PubSubManager extends AbstractManager {
final var isFromBare = from == null || from.isBareJid();
final var node = purge.getNode();
if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
- getManager(BookmarkManager.class).handlePurge();
+ getManager(NativeBookmarkManager.class).handlePurge();
}
if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) {
// purge (delete all items in a node) is functionally equivalent to delete
@@ -218,7 +218,7 @@ public class PubSubManager extends AbstractManager {
final var isFromBare = from == null || from.isBareJid();
final var node = delete.getNode();
if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
- getManager(BookmarkManager.class).handleDelete();
+ getManager(NativeBookmarkManager.class).handleDelete();
return;
}
if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) {
@@ -38,6 +38,9 @@ public class VCardManager extends AbstractManager {
}
public ListenableFuture<byte[]> retrievePhoto(final Jid address) {
+
+ // TODO add a caching variant
+
final var vCardFuture = retrieve(address);
return Futures.transform(
vCardFuture,
@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.conference;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x")
+public class DirectInvite extends Extension {
+
+ public DirectInvite() {
+ super(DirectInvite.class);
+ }
+
+ public void setJid(final Jid jid) {
+ this.setAttribute("jid", jid);
+ }
+
+ public void setPassword(final String password) {
+ this.setAttribute("password", password);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.DIRECT_MUC_INVITATIONS)
+package im.conversations.android.xmpp.model.conference;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,8 +1,10 @@
package im.conversations.android.xmpp.model.data;
+import android.util.Log;
import com.google.common.base.CaseFormat;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
+import eu.siacs.conversations.Config;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@@ -52,11 +54,14 @@ public class Data extends Extension {
if (type != null) {
field.setType(type);
}
- if (value instanceof Collection) {
- for (final Object subValue : (Collection<?>) value) {
- if (subValue instanceof String) {
+ if (value instanceof Collection<?> collection) {
+ Log.d(Config.LOGTAG, "submitting collection: " + collection);
+ for (final Object subValue : collection) {
+ if (subValue == null) {
+ Log.d(Config.LOGTAG, "null value in the values for " + name);
+ } else if (subValue instanceof String s) {
final var valueExtension = field.addExtension(new Value());
- valueExtension.setContent((String) subValue);
+ valueExtension.setContent(s);
} else {
throw new IllegalArgumentException(
String.format(
@@ -66,15 +71,15 @@ public class Data extends Extension {
}
} else {
final var valueExtension = field.addExtension(new Value());
- if (value instanceof String) {
- valueExtension.setContent((String) value);
+ if (value instanceof String s) {
+ valueExtension.setContent(s);
} 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) {
- valueExtension.setContent(Boolean.TRUE.equals(value) ? "1" : "0");
+ } else if (value instanceof Integer i) {
+ valueExtension.setContent(String.valueOf(i));
+ } else if (value instanceof Boolean b) {
+ valueExtension.setContent(Boolean.TRUE.equals(b) ? "1" : "0");
} else {
throw new IllegalArgumentException(
String.format(
@@ -109,9 +114,11 @@ public class Data extends Extension {
final var fieldName = existingField.getFieldName();
final Object submittedValue = values.get(fieldName);
if (submittedValue != null) {
+ Log.d(Config.LOGTAG, "submitting value " + fieldName + ": " + submittedValue);
submit.addField(fieldName, submittedValue);
} else {
- submit.addField(fieldName, existingField.getValues());
+ Log.d(Config.LOGTAG, "staying with default for: " + fieldName);
+ submit.addExtension(existingField);
}
}
return submit;
@@ -19,6 +19,7 @@ public class Field extends Extension {
}
public Collection<String> getValues() {
+ // TODO filter null
return Collections2.transform(getExtensions(Value.class), Element::getContent);
}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.hints;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class NoCopy extends Extension {
+ public NoCopy() {
+ super(NoCopy.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.hints;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class NoStore extends Extension {
+
+ public NoStore() {
+ super(NoStore.class);
+ }
+}
@@ -9,4 +9,9 @@ public class Subject extends Extension {
public Subject() {
super(Subject.class);
}
+
+ public Subject(final String subject) {
+ this();
+ this.setContent(subject);
+ }
}
@@ -1,5 +1,6 @@
package im.conversations.android.xmpp.model.muc;
+import eu.siacs.conversations.generator.AbstractGenerator;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@@ -17,4 +18,8 @@ public class History extends Extension {
public void setMaxStanzas(final int maxStanzas) {
this.setAttribute("maxstanzas", maxStanzas);
}
+
+ public void setSince(long timestamp) {
+ this.setAttribute("since", AbstractGenerator.getTimestamp(timestamp));
+ }
}
@@ -0,0 +1,54 @@
+package im.conversations.android.xmpp.model.muc;
+
+import android.util.Log;
+import com.google.common.base.Strings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Locale;
+
+public abstract class Item extends Extension {
+
+ public Item(final Class<? extends Item> clazz) {
+ super(clazz);
+ }
+
+ public Affiliation getAffiliation() {
+ return affiliationOrNone(this.getAttribute("affiliation"));
+ }
+
+ public static Affiliation affiliationOrNone(final String affiliation) {
+ if (Strings.isNullOrEmpty(affiliation)) {
+ return Affiliation.NONE;
+ }
+ try {
+ return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT));
+ } catch (final IllegalArgumentException e) {
+ return Affiliation.NONE;
+ }
+ }
+
+ public Role getRole() {
+ return roleOrNone(this.getAttribute("role"));
+ }
+
+ public static Role roleOrNone(final String role) {
+ if (Strings.isNullOrEmpty(role)) {
+ return Role.NONE;
+ }
+ try {
+ return Role.valueOf(role.toUpperCase(Locale.ROOT));
+ } catch (final IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "could not parse role " + role);
+ return Role.NONE;
+ }
+ }
+
+ public String getNick() {
+ return this.getAttribute("nick");
+ }
+
+ public Jid getJid() {
+ return this.getAttributeAsJid("jid");
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Password extends Extension {
+
+ public Password() {
+ super(Password.class);
+ }
+
+ public Password(final String password) {
+ this();
+ this.setContent(password);
+ }
+}
@@ -0,0 +1,30 @@
+package im.conversations.android.xmpp.model.muc.admin;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
+
+@XmlElement
+public class Item extends im.conversations.android.xmpp.model.muc.Item {
+
+ public Item() {
+ super(Item.class);
+ }
+
+ public void setAffiliation(final Affiliation affiliation) {
+ this.setAttribute("affiliation", affiliation);
+ }
+
+ public void setRole(final Role role) {
+ this.setAttribute("role", role);
+ }
+
+ public void setJid(final Jid jid) {
+ this.setAttribute("jid", jid);
+ }
+
+ public void setNick(String user) {
+ this.setAttribute("nick", user);
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc.admin;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+
+@XmlElement(name = "query")
+public class MucAdmin extends Extension {
+
+ public MucAdmin() {
+ super(MucAdmin.class);
+ }
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MUC_ADMIN)
+package im.conversations.android.xmpp.model.muc.admin;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.muc.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Destroy extends Extension {
+
+ public Destroy() {
+ super(Destroy.class);
+ }
+}
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.muc.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+
+@XmlElement(name = "query")
+public class MucOwner extends Extension {
+ public MucOwner() {
+ super(MucOwner.class);
+ }
+
+ public Data getConfiguration() {
+ return this.getExtension(Data.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MUC_OWNER)
+package im.conversations.android.xmpp.model.muc.owner;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc.user;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Invite extends Extension {
+
+ public Invite() {
+ super(Invite.class);
+ }
+
+ public void setTo(final Jid to) {
+ this.setAttribute("to", to);
+ }
+}
@@ -1,58 +1,11 @@
package im.conversations.android.xmpp.model.muc.user;
-import android.util.Log;
-
-import com.google.common.base.Strings;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.Jid;
-
import im.conversations.android.annotation.XmlElement;
-import im.conversations.android.xmpp.model.Extension;
-import im.conversations.android.xmpp.model.muc.Affiliation;
-import im.conversations.android.xmpp.model.muc.Role;
-
-import java.util.Locale;
@XmlElement
-public class Item extends Extension {
-
+public class Item extends im.conversations.android.xmpp.model.muc.Item {
public Item() {
super(Item.class);
}
-
- public Affiliation getAffiliation() {
- final var affiliation = this.getAttribute("affiliation");
- if (Strings.isNullOrEmpty(affiliation)) {
- return Affiliation.NONE;
- }
- try {
- return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT));
- } catch (final IllegalArgumentException e) {
- Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation);
- return Affiliation.NONE;
- }
- }
-
- public Role getRole() {
- final var role = this.getAttribute("role");
- if (Strings.isNullOrEmpty(role)) {
- return Role.NONE;
- }
- try {
- return Role.valueOf(role.toUpperCase(Locale.ROOT));
- } catch (final IllegalArgumentException e) {
- Log.d(Config.LOGTAG,"could not parse role "+ role);
- return Role.NONE;
- }
- }
-
- public String getNick() {
- return this.getAttribute("nick");
- }
-
- public Jid getJid() {
- return this.getAttributeAsJid("jid");
- }
}
@@ -1,15 +1,18 @@
package im.conversations.android.xmpp.model.pgp;
import eu.siacs.conversations.xml.Namespace;
-
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
-@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED)
+@XmlElement(name = "x", namespace = Namespace.PGP_SIGNED)
public class Signed extends Extension {
-
public Signed() {
super(Signed.class);
}
+
+ public Signed(final String signature) {
+ this();
+ this.setContent(signature);
+ }
}
@@ -1,7 +1,6 @@
package im.conversations.android.xmpp.model.stanza;
import com.google.common.base.Strings;
-import eu.siacs.conversations.xml.Element;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.error.Error;
@@ -50,22 +49,6 @@ public class Iq extends Stanza {
return super.isInvalid();
}
- // Legacy methods that need to be refactored:
-
- public Element query() {
- final Element query = findChild("query");
- if (query != null) {
- return query;
- }
- return addChild("query");
- }
-
- public Element query(final String xmlns) {
- final Element query = query();
- query.setAttribute("xmlns", xmlns);
- return query();
- }
-
public Iq generateResponse(final Iq.Type type) {
final var packet = new Iq(type);
packet.setTo(this.getFrom());
@@ -5,12 +5,10 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.http.ServiceOutageStatus;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
-import java.util.ArrayList;
-import java.util.List;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -48,7 +46,7 @@ public class AccountStateProcessor extends XmppConnection.Delegate
this.service.databaseBackend.updateAccount(account);
}
this.service.getMessageArchiveService().executePendingQueries(account);
- if (connection != null && connection.getFeatures().csi()) {
+ if (this.connection.getFeatures().csi()) {
if (this.service.checkListeners()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive");
connection.sendInactive();
@@ -57,36 +55,14 @@ public class AccountStateProcessor extends XmppConnection.Delegate
connection.sendActive();
}
}
- List<Conversation> conversations = this.service.getConversations();
- for (Conversation conversation : conversations) {
- final boolean inProgressJoin;
- synchronized (account.inProgressConferenceJoins) {
- inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
- }
- final boolean pendingJoin;
- synchronized (account.pendingConferenceJoins) {
- pendingJoin = account.pendingConferenceJoins.contains(conversation);
- }
- if (conversation.getAccount() == account && !pendingJoin && !inProgressJoin) {
+ final var mucManager = getManager(MultiUserChatManager.class);
+ final var conversations = this.service.getConversations();
+ for (final var conversation : conversations) {
+ final boolean inProgressJoin = mucManager.isJoinInProgress(conversation);
+ if (conversation.getAccount() == account && !inProgressJoin) {
this.service.sendUnsentMessages(conversation);
}
}
- final List<Conversation> pendingLeaves;
- synchronized (account.pendingConferenceLeaves) {
- pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
- account.pendingConferenceLeaves.clear();
- }
- for (Conversation conversation : pendingLeaves) {
- this.service.leaveMuc(conversation);
- }
- final List<Conversation> pendingJoins;
- synchronized (account.pendingConferenceJoins) {
- pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
- account.pendingConferenceJoins.clear();
- }
- for (Conversation conversation : pendingJoins) {
- this.service.joinMuc(conversation);
- }
this.service.scheduleWakeUpCall(
Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE
@@ -12,12 +12,11 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
-import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.NickManager;
import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
-import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
import eu.siacs.conversations.xmpp.manager.RosterManager;
public class BindProcessor extends XmppConnection.Delegate implements Runnable {
@@ -63,26 +62,12 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
}
getManager(RosterManager.class).clearPresences();
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.clear();
- }
- synchronized (account.inProgressConferencePings) {
- account.inProgressConferencePings.clear();
- }
+ getManager(MultiUserChatManager.class).clearInProgress();
service.getJingleConnectionManager().notifyRebound(account);
service.getQuickConversationsService().considerSyncBackground(false);
getManager(RosterManager.class).request();
-
- if (getManager(BookmarkManager.class).hasFeature()) {
- getManager(BookmarkManager.class).fetch();
- } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
- Log.d(
- Config.LOGTAG,
- account.getJid() + ": not fetching bookmarks. waiting for server to push");
- } else {
- getManager(PrivateStorageManager.class).fetchBookmarks();
- }
+ getManager(BookmarkManager.class).request();
if (features.mds()) {
getManager(MessageDisplayedSynchronizationManager.class).fetch();
@@ -93,7 +93,7 @@
<string name="send_message_to_x">Send message to %s</string>
<string name="send_encrypted_message">Send encrypted message</string>
<string name="send_omemo_x509_message">Send v\\OMEMO encrypted message</string>
- <string name="your_nick_has_been_changed">New nickname in use</string>
+ <string name="your_nick_has_been_changed">Your nickname has been changed</string>
<string name="send_unencrypted">Send clear text</string>
<string name="decryption_failed">Decryption failed. Maybe you donβt have the proper private key.</string>
<string name="openkeychain_required">OpenKeychain</string>