From 9712875d971d95dabe0a475a521248d46473f627 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 May 2025 14:12:17 +0200 Subject: [PATCH 01/87] modify domain regex --- .../java/eu/siacs/conversations/xmpp/Jid.java | 2 +- .../eu/siacs/conversations/xmpp/JidTest.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/test/java/eu/siacs/conversations/xmpp/JidTest.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/Jid.java index a83c762872b18f9b6f9a17a2662016ecd64c4297..9faff3a0080cf601d971b397d86143723f593c15 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Jid.java @@ -15,7 +15,7 @@ public abstract class Jid implements Comparable, Serializable, CharSequence private static final Pattern HOSTNAME_PATTERN = Pattern.compile( - "^(?=.{1,253}$)(?=.{1,253}$)(?!-)(?!.*--)(?!.*-$)[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$"); + "^(?=.{1,253}$)(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?(?:\\.(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?)*\\.?$"); public static Jid of( final CharSequence local, final CharSequence domain, final CharSequence resource) { diff --git a/src/test/java/eu/siacs/conversations/xmpp/JidTest.java b/src/test/java/eu/siacs/conversations/xmpp/JidTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8b72a273d6ae85a60b79f2687d628a0ec1de0030 --- /dev/null +++ b/src/test/java/eu/siacs/conversations/xmpp/JidTest.java @@ -0,0 +1,16 @@ +package eu.siacs.conversations.xmpp; + +import org.junit.Test; + +public class JidTest { + + @Test + public void testDoubleDash() { + Jid.ofUserInput("user@a--z.com"); + } + + @Test + public void testUnicode() { + Jid.ofUserInput("test@գծոոոց.հայ"); + } +} From 02118f4bc3787d8af6dc9c57bba5e9380010e5cc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 May 2025 21:19:44 +0200 Subject: [PATCH 02/87] rudimentary RosterManager and BlockingManager --- .../siacs/conversations/entities/Account.java | 42 ++- .../conversations/entities/MucOptions.java | 4 +- .../siacs/conversations/entities/Roster.java | 95 +----- .../conversations/generator/IqGenerator.java | 29 -- .../siacs/conversations/parser/IqParser.java | 255 ++++------------ .../conversations/parser/MessageParser.java | 7 +- .../conversations/parser/PresenceParser.java | 15 +- .../persistance/DatabaseBackend.java | 24 +- .../services/UnifiedPushBroker.java | 4 +- .../services/XmppConnectionService.java | 59 ++-- .../utils/ReplacingTaskManager.java | 57 ---- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../eu/siacs/conversations/xmpp/Managers.java | 12 +- .../conversations/xmpp/XmppConnection.java | 46 +-- .../xmpp/jingle/JingleConnectionManager.java | 1 + .../transports/SocksByteStreamsTransport.java | 1 + .../xmpp/manager/AbstractManager.java | 2 +- .../xmpp/manager/BlockingManager.java | 144 +++++++++ .../xmpp/manager/DiscoManager.java | 2 +- .../xmpp/manager/EntityTimeManager.java | 43 +++ .../xmpp/manager/PingManager.java | 4 + .../xmpp/manager/RosterManager.java | 276 ++++++++++++++++++ .../xmpp/manager/UnifiedPushManager.java | 34 +++ .../android/xmpp/model/blocking/Block.java | 5 + .../xmpp/model/blocking/Blocklist.java | 5 + .../android/xmpp/model/blocking/Unblock.java | 5 + .../android/xmpp/model/ibb/Close.java | 11 + .../android/xmpp/model/ibb/Data.java | 12 + .../xmpp/model/ibb/InBandByteStream.java | 14 + .../android/xmpp/model/ibb/Open.java | 11 + .../android/xmpp/model/ibb/package-info.java | 5 + .../android/xmpp/model/roster/Query.java | 5 + .../android/xmpp/model/time/Time.java | 20 ++ .../xmpp/model/time/TimeZoneOffset.java | 12 + .../xmpp/model/time/UniversalTime.java | 12 + .../android/xmpp/model/time/package-info.java | 5 + .../android/xmpp/model/up/Push.java | 13 + .../android/xmpp/model/up/package-info.java | 5 + .../android/xmpp/processor/BindProcessor.java | 5 +- .../services/QuickConversationsService.java | 21 +- .../android/xmpp/EntityCapabilitiesTest.java | 100 ++++++- 41 files changed, 922 insertions(+), 501 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/ibb/Close.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/ibb/Data.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/ibb/Open.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/time/Time.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/time/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/up/Push.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/up/package-info.java diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 5f38e1a51c50f17f24bd1a622660f953ece6e593..b1cd4e395137fb0a34bfc6cf612f5dd452a09b84 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -27,15 +27,17 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; import org.json.JSONException; import org.json.JSONObject; @@ -77,10 +79,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; 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; - private final Roster roster = new Roster(this); - private final Collection blocklist = new CopyOnWriteArraySet<>(); public final Set pendingConferenceJoins = new HashSet<>(); public final Set pendingConferenceLeaves = new HashSet<>(); public final Set inProgressConferenceJoins = new HashSet<>(); @@ -550,11 +549,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public void initAccountServices(final XmppConnectionService context) { + this.xmppConnection = new XmppConnection(this, context); this.axolotlService = new AxolotlService(this, context); this.pgpDecryptionService = new PgpDecryptionService(context); - if (xmppConnection != null) { - xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } + this.xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); } public PgpDecryptionService getPgpDecryptionService() { @@ -565,16 +563,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return this.xmppConnection; } - public void setXmppConnection(final XmppConnection connection) { - this.xmppConnection = connection; - } - public String getRosterVersion() { - if (this.rosterVersion == null) { - return ""; - } else { - return this.rosterVersion; - } + return Strings.emptyToNull(this.rosterVersion); } public void setRosterVersion(final String version) { @@ -648,7 +638,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public Roster getRoster() { - return this.roster; + if (xmppConnection != null) { + return xmppConnection.getManager(RosterManager.class); + } + // TODO either return stub or always put XmppConnection into Account + return null; } public Collection getBookmarks() { @@ -767,20 +761,22 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public boolean isBlocked(final ListItem contact) { final Jid jid = contact.getJid(); + final var blocklist = getBlocklist(); return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); } public boolean isBlocked(final Jid jid) { + final var blocklist = getBlocklist(); return jid != null && blocklist.contains(jid.asBareJid()); } - public Collection getBlocklist() { - return this.blocklist; - } - - public void clearBlocklist() { - getBlocklist().clear(); + public Set getBlocklist() { + final var connection = this.xmppConnection; + if (connection == null) { + return Collections.emptySet(); + } + return connection.getManager(BlockingManager.class).getBlocklist(); } public boolean isOnlineAndConnected() { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 2d939d153d600958bc5ecb9769132f0859062281..2dbed2c1470e2713377aa973a9204ef1096d29fc 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -883,7 +883,9 @@ public class MucOptions { public Contact getContact() { if (fullJid != null) { - return getAccount().getRoster().getContactFromContactList(realJid); + return realJid == null + ? null + : getAccount().getRoster().getContactFromContactList(realJid); } else if (realJid != null) { return getAccount().getRoster().getContact(realJid); } else { diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java index bc4c6b9747c05c8d21c45a4f4623210a991a38b1..c1dc4bfe5a28d375b989b04e56b0c765c1f959c7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -1,98 +1,17 @@ package eu.siacs.conversations.entities; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - +import androidx.annotation.NonNull; import eu.siacs.conversations.android.AbstractPhoneContact; import eu.siacs.conversations.xmpp.Jid; +import java.util.List; +public interface Roster { -public class Roster { - private final Account account; - private final HashMap contacts = new HashMap<>(); - private String version = null; - - public Roster(Account account) { - this.account = account; - } - - public Contact getContactFromContactList(Jid jid) { - if (jid == null) { - return null; - } - synchronized (this.contacts) { - Contact contact = contacts.get(jid.asBareJid()); - if (contact != null && contact.showInContactList()) { - return contact; - } else { - return null; - } - } - } - - public Contact getContact(final Jid jid) { - synchronized (this.contacts) { - if (!contacts.containsKey(jid.asBareJid())) { - Contact contact = new Contact(jid.asBareJid()); - contact.setAccount(account); - contacts.put(contact.getJid().asBareJid(), contact); - return contact; - } - return contacts.get(jid.asBareJid()); - } - } - - public void clearPresences() { - for (Contact contact : getContacts()) { - contact.clearPresences(); - } - } - - public void markAllAsNotInRoster() { - for (Contact contact : getContacts()) { - contact.resetOption(Contact.Options.IN_ROSTER); - } - } - - public List getWithSystemAccounts(Class clazz) { - int option = Contact.getOption(clazz); - List with = getContacts(); - for(Iterator iterator = with.iterator(); iterator.hasNext();) { - Contact contact = iterator.next(); - if (!contact.getOption(option)) { - iterator.remove(); - } - } - return with; - } - - public List getContacts() { - synchronized (this.contacts) { - return new ArrayList<>(this.contacts.values()); - } - } - - public void initContact(final Contact contact) { - if (contact == null) { - return; - } - contact.setAccount(account); - synchronized (this.contacts) { - contacts.put(contact.getJid().asBareJid(), contact); - } - } + List getContacts(); - public void setVersion(String version) { - this.version = version; - } + List getWithSystemAccounts(Class clazz); - public String getVersion() { - return this.version; - } + Contact getContact(@NonNull final Jid jid); - public Account getAccount() { - return this.account; - } + Contact getContactFromContactList(@NonNull final Jid jid); } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5f3173f87ea2b7813e43a9ce017bf2f215e9659e..163250e4cf7ece3676c2771981798b891eb9fccb 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -23,9 +23,7 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Set; -import java.util.TimeZone; import java.util.UUID; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -38,26 +36,6 @@ public class IqGenerator extends AbstractGenerator { super(service); } - public Iq entityTimeResponse(final Iq request) { - final Iq packet = request.generateResponse(Iq.Type.RESULT); - Element time = packet.addChild("time", "urn:xmpp:time"); - final long now = System.currentTimeMillis(); - time.addChild("utc").setContent(getTimestamp(now)); - TimeZone ourTimezone = TimeZone.getDefault(); - long offsetSeconds = ourTimezone.getOffset(now) / 1000; - long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); - long offsetHours = offsetSeconds / 3600; - String hours; - if (offsetHours < 0) { - hours = String.format(Locale.US, "%03d", offsetHours); - } else { - hours = String.format(Locale.US, "%02d", offsetHours); - } - String minutes = String.format(Locale.US, "%02d", offsetMinutes); - time.addChild("tzo").setContent(hours + ":" + minutes); - return packet; - } - public static Iq purgeOfflineMessages() { final Iq packet = new Iq(Iq.Type.SET); packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); @@ -338,13 +316,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq generateGetBlockList() { - final Iq iq = new Iq(Iq.Type.GET); - iq.addChild("blocklist", Namespace.BLOCKING); - - return iq; - } - public Iq generateSetBlockRequest( final Jid jid, final boolean reportSpam, final String serverMsgId) { final Iq iq = new Iq(Iq.Type.SET); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 7f2d0d94fd378819522f0d7b7e1290b3173b1d03..2277d9d3ec10b576c648e1fd500fbce83936c3da 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -7,24 +7,33 @@ import com.google.common.base.CharMatcher; import com.google.common.io.BaseEncoding; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.EntityTimeManager; +import eu.siacs.conversations.xmpp.manager.PingManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; +import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; +import im.conversations.android.xmpp.model.blocking.Block; +import im.conversations.android.xmpp.model.blocking.Unblock; import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.ibb.InBandByteStream; +import im.conversations.android.xmpp.model.ping.Ping; +import im.conversations.android.xmpp.model.roster.Query; import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.time.Time; +import im.conversations.android.xmpp.model.up.Push; import im.conversations.android.xmpp.model.version.Version; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -43,76 +52,6 @@ public class IqParser extends AbstractParser implements Consumer { super(service, connection); } - public static List items(final Iq packet) { - ArrayList items = new ArrayList<>(); - final Element query = packet.findChild("query", Namespace.DISCO_ITEMS); - if (query == null) { - return items; - } - for (Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - Jid jid = child.getAttributeAsJid("jid"); - if (jid != null) { - items.add(jid); - } - } - } - return items; - } - - private void rosterItems(final Account account, final Element query) { - final String version = query.getAttribute("ver"); - if (version != null) { - account.getRoster().setVersion(version); - } - for (final Element item : query.getChildren()) { - if (item.getName().equals("item")) { - final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid == null) { - continue; - } - final String name = item.getAttribute("name"); - final String subscription = item.getAttribute("subscription"); - final Contact contact = account.getRoster().getContact(jid); - boolean bothPre = - contact.getOption(Contact.Options.TO) - && contact.getOption(Contact.Options.FROM); - if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { - contact.setServerName(name); - contact.parseGroupsFromElement(item); - } - if ("remove".equals(subscription)) { - contact.resetOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - } else { - contact.setOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.parseSubscriptionFromElement(item); - } - boolean both = - contact.getOption(Contact.Options.TO) - && contact.getOption(Contact.Options.FROM); - if ((both != bothPre) && both) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": gained mutual presence subscription with " - + contact.getJid()); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); - } - } - mXmppConnectionService.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - mXmppConnectionService.getShortcutService().refresh(); - mXmppConnectionService.syncRoster(account); - } - public static String avatarData(final Iq packet) { final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); if (pubsub == null) { @@ -381,139 +320,47 @@ public class IqParser extends AbstractParser implements Consumer { @Override public void accept(final Iq packet) { - final var account = getAccount(); - final boolean isGet = packet.getType() == Iq.Type.GET; - if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) { - return; + final var type = packet.getType(); + switch (type) { + case SET -> acceptPush(packet); + case GET -> acceptRequest(packet); + default -> + throw new AssertionError( + "IQ results and errors should are handled in callbacks"); } - if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { - final Element query = packet.findChild("query"); - // If this is in response to a query for the whole roster: - if (packet.getType() == Iq.Type.RESULT) { - account.getRoster().markAllAsNotInRoster(); - } - this.rosterItems(account, query); - } else if ((packet.hasChild("block", Namespace.BLOCKING) - || packet.hasChild("blocklist", Namespace.BLOCKING)) - && packet.fromServer(account)) { - // Block list or block push. - Log.d(Config.LOGTAG, "Received blocklist update from server"); - final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING); - final Element block = packet.findChild("block", Namespace.BLOCKING); - final Collection items = - blocklist != null - ? blocklist.getChildren() - : (block != null ? block.getChildren() : null); - // If this is a response to a blocklist query, clear the block list and replace with the - // new one. - // Otherwise, just update the existing blocklist. - if (packet.getType() == Iq.Type.RESULT) { - account.clearBlocklist(); - connection.getFeatures().setBlockListRequested(true); - } - if (items != null) { - final Collection jids = new ArrayList<>(items.size()); - // Create a collection of Jids from the packet - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = - Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().addAll(jids); - if (packet.getType() == Iq.Type.SET) { - boolean removed = false; - for (Jid jid : jids) { - removed |= mXmppConnectionService.removeBlockedConversations(account, jid); - } - if (removed) { - mXmppConnectionService.updateConversationUi(); - } - } - } - // Update the UI - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - if (packet.getType() == Iq.Type.SET) { - final Iq response = packet.generateResponse(Iq.Type.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } - } else if (packet.hasChild("unblock", Namespace.BLOCKING) - && packet.fromServer(account) - && packet.getType() == Iq.Type.SET) { - Log.d(Config.LOGTAG, "Received unblock update from server"); - final Collection items = - packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.isEmpty()) { - // No children to unblock == unblock all - account.getBlocklist().clear(); - } else { - final Collection jids = new ArrayList<>(items.size()); - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = - Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().removeAll(jids); - } - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - final Iq response = packet.generateResponse(Iq.Type.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") - || packet.hasChild("data", "http://jabber.org/protocol/ibb") - || packet.hasChild("close", "http://jabber.org/protocol/ibb")) { - mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet); - } else if (packet.hasExtension(InfoQuery.class) && isGet) { + } + + private void acceptPush(final Iq packet) { + if (packet.hasExtension(Query.class)) { + this.getManager(RosterManager.class).push(packet); + } else if (packet.hasExtension(Block.class)) { + this.getManager(BlockingManager.class).pushBlock(packet); + } else if (packet.hasExtension(Unblock.class)) { + this.getManager(BlockingManager.class).pushUnblock(packet); + } else if (packet.hasExtension(InBandByteStream.class)) { + mXmppConnectionService + .getJingleConnectionManager() + .deliverIbbPacket(getAccount(), packet); + } else if (packet.hasExtension(Push.class)) { + this.getManager(UnifiedPushManager.class).push(packet); + } else { + this.connection.sendErrorFor( + packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented()); + } + } + + private void acceptRequest(final Iq packet) { + if (packet.hasExtension(InfoQuery.class)) { this.getManager(DiscoManager.class).handleInfoQuery(packet); - } else if (packet.hasExtension(Version.class) && isGet) { + } else if (packet.hasExtension(Version.class)) { this.getManager(DiscoManager.class).handleVersionRequest(packet); - } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { - final Iq response = packet.generateResponse(Iq.Type.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) { - final Iq response; - if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { - response = packet.generateResponse(Iq.Type.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas"); - } else { - response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); - } - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) - && packet.getType() == Iq.Type.SET) { - final Jid transport = packet.getFrom(); - final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH); - final boolean success = - push != null - && mXmppConnectionService.processUnifiedPushMessage( - account, transport, push); - final Iq response; - if (success) { - response = packet.generateResponse(Iq.Type.RESULT); - } else { - response = packet.generateResponse(Iq.Type.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.setAttribute("code", "404"); - error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); - } - mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasExtension(Time.class)) { + this.getManager(EntityTimeManager.class).request(packet); + } else if (packet.hasExtension(Ping.class)) { + this.getManager(PingManager.class).pong(packet); } else { - if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) { - final Iq response = packet.generateResponse(Iq.Type.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); - connection.sendIqPacket(response, null); - } + this.connection.sendErrorFor( + packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented()); } } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c287a927cd08c1419bf923320a5ea79e75e8a42f..35725518c712f3f97ef0ed4f206e65f9cba67f74 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -37,6 +37,7 @@ 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.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.avatar.Metadata; @@ -276,7 +277,7 @@ public class MessageParser extends AbstractParser } else { final Contact contact = account.getRoster().getContact(from); if (contact.setAvatar(avatar)) { - mXmppConnectionService.syncRoster(account); + connection.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateRosterUi(); @@ -410,7 +411,7 @@ public class MessageParser extends AbstractParser } else { Contact contact = account.getRoster().getContact(user); if (contact.setPresenceName(nick)) { - mXmppConnectionService.syncRoster(account); + connection.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); } } @@ -1435,7 +1436,7 @@ public class MessageParser extends AbstractParser } final Contact contact = account.getRoster().getContact(from); if (contact.setPresenceName(nick)) { - mXmppConnectionService.syncRoster(account); + connection.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); } } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index c56262ed1a475c8423e2522b2635bcdb3e7afbc0..3cdd86b600c44c668869dba930ef07f694a5bb62 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -24,6 +24,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.occupant.OccupantId; @@ -172,8 +173,9 @@ public class PresenceParser extends AbstractParser .getRoster() .getContact(user.getRealJid()); if (c.setAvatar(avatar)) { - mXmppConnectionService.syncRoster( - conversation.getAccount()); + connection + .getManager(RosterManager.class) + .writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(c); } mXmppConnectionService.updateRosterUi(); @@ -351,7 +353,7 @@ public class PresenceParser extends AbstractParser mXmppConnectionService.updateAccountUi(); } else { if (contact.setAvatar(avatar)) { - mXmppConnectionService.syncRoster(account); + connection.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateRosterUi(); @@ -371,8 +373,7 @@ public class PresenceParser extends AbstractParser contact.updatePresence(resource, packet); final var nodeHash = packet.getCapabilities(); - final var connection = account.getXmppConnection(); - if (nodeHash != null && connection != null) { + if (nodeHash != null) { final var discoFuture = this.getManager(DiscoManager.class) .infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash); @@ -410,7 +411,7 @@ public class PresenceParser extends AbstractParser + contact.getJid() + " " + OpenPgpUtils.convertKeyIdToHex(keyId)); - mXmppConnectionService.syncRoster(account); + this.connection.getManager(RosterManager.class).writeToDatabaseAsync(); } } boolean online = sizeBefore < contact.getPresences().size(); @@ -440,7 +441,7 @@ public class PresenceParser extends AbstractParser return; } if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) { - mXmppConnectionService.syncRoster(account); + this.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); } if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 3673e6dcf6585a329b4d66ecfb98d36b469a8f06..9da8e6bc8e5b2853b5eb1b4ae487a787d6ae92df 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -11,6 +11,7 @@ import android.os.SystemClock; import android.util.Base64; import android.util.Log; import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -20,7 +21,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.PresenceTemplate; -import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.ShortcutService; import eu.siacs.conversations.utils.CryptoHelper; @@ -1809,23 +1809,29 @@ public class DatabaseBackend extends SQLiteOpenHelper { return rows == 1; } - public void readRoster(Roster roster) { + public List readRoster(final Account account) { + final var builder = new ImmutableList.Builder(); final SQLiteDatabase db = this.getReadableDatabase(); - final String[] args = {roster.getAccount().getUuid()}; + final String[] args = {account.getUuid()}; try (final Cursor cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) { while (cursor.moveToNext()) { - roster.initContact(Contact.fromCursor(cursor)); + final var contact = Contact.fromCursor(cursor); + if (contact != null) { + contact.setAccount(account); + builder.add(contact); + } } } + return builder.build(); } - public void writeRoster(final Roster roster) { - long start = SystemClock.elapsedRealtime(); - final Account account = roster.getAccount(); + public void writeRoster( + final Account account, final String version, final List contacts) { + final long start = SystemClock.elapsedRealtime(); final SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction(); - for (Contact contact : roster.getContacts()) { + for (final Contact contact : contacts) { if (contact.getOption(Contact.Options.IN_ROSTER) || contact.hasAvatarOrPresenceName() || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { @@ -1838,7 +1844,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } db.setTransactionSuccessful(); db.endTransaction(); - account.setRosterVersion(roster.getVersion()); + account.setRosterVersion(version); updateAccount(account); long duration = SystemClock.elapsedRealtime() - start; Log.d( diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 24aaf1f4279b04abd32fbf3da615283c0dda3a32..5f5de553ddbad9f24306f35fb933a5df59223100 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -31,6 +31,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Presence; +import im.conversations.android.xmpp.model.up.Push; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.List; @@ -320,8 +321,7 @@ public class UnifiedPushBroker { service.sendBroadcast(intent); } - public boolean processPushMessage( - final Account account, final Jid transport, final Element push) { + public boolean processPushMessage(final Account account, final Jid transport, final Push push) { final String instance = push.getAttribute("instance"); final String application = push.getAttribute("application"); if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b090d622fd97e79bb347da656e16903a5af80105..00e353bc802ee07f508c1595d24763b2c8befdce 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -112,7 +112,6 @@ import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; -import eu.siacs.conversations.utils.ReplacingTaskManager; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.StringUtils; @@ -139,6 +138,7 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.Entity; @@ -149,6 +149,7 @@ import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.storage.PrivateStorage; +import im.conversations.android.xmpp.model.up.Push; import java.io.File; import java.security.Security; import java.security.cert.CertificateException; @@ -228,7 +229,6 @@ public class XmppConnectionService extends Service { new SerialSingleThreadExecutor("DatabaseReader"); private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor"); - private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager(); private final IBinder mBinder = new XmppConnectionBinder(); private final List conversations = new CopyOnWriteArrayList<>(); private final IqGenerator mIqGenerator = new IqGenerator(this); @@ -1173,7 +1173,7 @@ public class XmppConnectionService extends Service { } public boolean processUnifiedPushMessage( - final Account account, final Jid transport, final Element push) { + final Account account, final Jid transport, final Push push) { return unifiedPushBroker.processPushMessage(account, transport, push); } @@ -1750,7 +1750,7 @@ public class XmppConnectionService extends Service { int activeAccounts = 0; for (final Account account : accounts) { if (account.isConnectionEnabled()) { - databaseBackend.writeRoster(account.getRoster()); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabase(); activeAccounts++; } if (account.getXmppConnection() != null) { @@ -2529,10 +2529,9 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "restoring roster..."); for (final Account account : accounts) { - databaseBackend.readRoster(account.getRoster()); account.initAccountServices( - XmppConnectionService - .this); // roster needs to be loaded at this stage + this); // roster needs to be loaded at this stage + account.getXmppConnection().getManager(RosterManager.class).restore(); } getBitmapCache().evictAll(); loadPhoneContacts(); @@ -2583,9 +2582,13 @@ public class XmppConnectionService extends Service { () -> { final Map contacts = JabberIdContact.load(this); Log.d(Config.LOGTAG, "start merging phone contacts with roster"); + // TODO if we do this merge this only on enabled accounts we need to trigger + // this upon enable for (final Account account : accounts) { - final List withSystemAccounts = - account.getRoster().getWithSystemAccounts(JabberIdContact.class); + final var remaining = + new ArrayList<>( + account.getRoster() + .getWithSystemAccounts(JabberIdContact.class)); for (final JabberIdContact jidContact : contacts.values()) { final Contact contact = account.getRoster().getContact(jidContact.getJid()); @@ -2593,9 +2596,9 @@ public class XmppConnectionService extends Service { if (needsCacheClean) { getAvatarService().clear(contact); } - withSystemAccounts.remove(contact); + remaining.remove(contact); } - for (final Contact contact : withSystemAccounts) { + for (final Contact contact : remaining) { boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class); if (needsCacheClean) { @@ -2611,11 +2614,6 @@ public class XmppConnectionService extends Service { }); } - public void syncRoster(final Account account) { - mRosterSyncTaskManager.execute( - account, () -> databaseBackend.writeRoster(account.getRoster())); - } - public List getConversations() { return this.conversations; } @@ -3260,7 +3258,6 @@ public class XmppConnectionService extends Service { if (CallIntegration.hasSystemFeature(this)) { CallIntegrationConnectionService.unregisterPhoneAccount(this, account); } - this.mRosterSyncTaskManager.clear(account); updateAccountUi(); mNotificationService.updateErrorNotification(); syncEnabledAccountSetting(); @@ -4693,6 +4690,7 @@ public class XmppConnectionService extends Service { updateConversationUi(); } + // TODO move this to RosterManager public void syncDirtyContacts(Account account) { for (Contact contact : account.getRoster().getContacts()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { @@ -4741,7 +4739,7 @@ public class XmppConnectionService extends Service { account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); } } else { - syncRoster(contact.getAccount()); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); } } @@ -5127,7 +5125,9 @@ public class XmppConnectionService extends Service { final Contact contact = account.getRoster().getContact(avatar.owner); contact.setAvatar(avatar); - syncRoster(account); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); getAvatarService().clear(contact); updateConversationUi(); updateRosterUi(); @@ -5202,7 +5202,9 @@ public class XmppConnectionService extends Service { final Contact contact = account.getRoster().getContact(avatar.owner); contact.setAvatar(avatar, previouslyOmittedPepFetch); - syncRoster(account); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); getAvatarService().clear(contact); updateRosterUi(); } @@ -5227,7 +5229,9 @@ public class XmppConnectionService extends Service { account.getRoster() .getContact(user.getRealJid()); contact.setAvatar(avatar); - syncRoster(account); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); getAvatarService().clear(contact); updateRosterUi(); } @@ -5329,16 +5333,7 @@ public class XmppConnectionService extends Service { private void reconnectAccount( final Account account, final boolean force, final boolean interactive) { synchronized (account) { - final XmppConnection existingConnection = account.getXmppConnection(); - final XmppConnection connection; - if (existingConnection != null) { - connection = existingConnection; - } else if (account.isConnectionEnabled()) { - connection = createConnection(account); - account.setXmppConnection(connection); - } else { - return; - } + final XmppConnection connection = account.getXmppConnection(); final boolean hasInternet = hasInternetConnection(); if (account.isConnectionEnabled() && hasInternet) { if (!force) { @@ -5352,7 +5347,7 @@ public class XmppConnectionService extends Service { scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); } else { disconnect(account, force || account.getTrueStatus().isError() || !hasInternet); - account.getRoster().clearPresences(); + connection.getManager(RosterManager.class).clearPresences(); connection.resetEverything(); final AxolotlService axolotlService = account.getAxolotlService(); if (axolotlService != null) { diff --git a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java b/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java deleted file mode 100644 index 396bfa3ece0259adfba748a3dd3b85e511b1406b..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import java.util.HashMap; - -import eu.siacs.conversations.entities.Account; - -public class ReplacingTaskManager { - - private final HashMap executors = new HashMap<>(); - - public void execute(final Account account, Runnable runnable) { - ReplacingSerialSingleThreadExecutor executor; - synchronized (this.executors) { - executor = this.executors.get(account); - if (executor == null) { - executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName()); - this.executors.put(account, executor); - } - executor.execute(runnable); - } - } - - public void clear(Account account) { - synchronized (this.executors) { - this.executors.remove(account); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 353898c56b72e48c5b80c6b02869afd4e0ee7872..6ee41f5f709049ffb11e66fc1d6b536ccdbfae4b 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -41,6 +41,7 @@ public final class Namespace { public static final String SASL_2 = "urn:xmpp:sasl:2"; public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0"; public static final String FAST = "urn:xmpp:fast:0"; + public static final String TIME = "urn:xmpp:time"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_EVENT = PUBSUB + "#event"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 84e0225e53773b73c08b43fa34e32cd88eebdffb..767234a04b54b7dc078191462ddd16e68faddc03 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -1,13 +1,17 @@ package eu.siacs.conversations.xmpp; -import android.content.Context; import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ImmutableClassToInstanceMap; +import eu.siacs.conversations.services.XmppConnectionService; 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.EntityTimeManager; import eu.siacs.conversations.xmpp.manager.PingManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; +import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; public class Managers { @@ -16,12 +20,16 @@ public class Managers { } public static ClassToInstanceMap get( - final Context context, final XmppConnection connection) { + final XmppConnectionService context, final XmppConnection connection) { return new ImmutableClassToInstanceMap.Builder() + .put(BlockingManager.class, new BlockingManager(context, connection)) .put(CarbonsManager.class, new CarbonsManager(context, connection)) .put(DiscoManager.class, new DiscoManager(context, connection)) + .put(EntityTimeManager.class, new EntityTimeManager(context, connection)) .put(PingManager.class, new PingManager(context, connection)) .put(PresenceManager.class, new PresenceManager(context, connection)) + .put(RosterManager.class, new RosterManager(context, connection)) + .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection)) .build(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d9e79c35f96ac24b69a7acd35c22510a566ad048..3e052ebf8e0643ae0bd335553923ff97efd9c57f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -72,6 +72,7 @@ import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; 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.PingManager; @@ -224,7 +225,7 @@ public class XmppConnection implements Runnable { this.unregisteredIqListener = new IqParser(service, this); this.messageListener = new MessageParser(service, this); this.bindListener = new BindProcessor(service, this); - this.managers = Managers.get(service.getApplicationContext(), this); + this.managers = Managers.get(service, this); } private static void fixResource(final Context context, final Account account) { @@ -2314,6 +2315,7 @@ public class XmppConnection implements Runnable { } private void discoverCommands() { + // TODO move result handling into DiscoManager too final var future = getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain())); Futures.addCallback( @@ -2357,9 +2359,9 @@ public class XmppConnection implements Runnable { } private void enableAdvancedStreamFeatures() { - if (getFeatures().blocking() && !features.blockListRequested) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list"); - this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener); + final var blockingManager = getManager(BlockingManager.class); + if (blockingManager.hasFeature()) { + blockingManager.request(); } for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { @@ -2751,15 +2753,6 @@ public class XmppConnection implements Runnable { return Iterables.getFirst(items, null).getKey(); } - public boolean r() { - if (getFeatures().sm()) { - this.tagWriter.writeStanzaAsync(new Request()); - return true; - } else { - return false; - } - } - public List getMucServersWithholdAccount() { final List servers = getMucServers(); servers.remove(account.getDomain().toString()); @@ -2846,10 +2839,6 @@ public class XmppConnection implements Runnable { this.mInteractive = interactive; } - private IqGenerator getIqGenerator() { - return mXmppConnectionService.getIqGenerator(); - } - public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) { if (trackOfflineMessageRetrieval) { getManager(PingManager.class) @@ -2871,20 +2860,6 @@ public class XmppConnection implements Runnable { return this.offlineMessagesRetrieved; } - public void fetchRoster() { - final Iq iqPacket = new Iq(Iq.Type.GET); - final var version = account.getRosterVersion(); - if (Strings.isNullOrEmpty(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster"); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": fetching roster version " + version); - } - iqPacket.query(Namespace.ROSTER).setAttribute("ver", version); - sendIqPacket(iqPacket, unregisteredIqListener); - } - public void triggerConnectionTimeout() { final var duration = getConnectionDuration(); Log.d( @@ -2909,6 +2884,15 @@ public class XmppConnection implements Runnable { return this.features; } + public boolean fromServer(final Stanza stanza) { + final var account = getAccount().getJid(); + final Jid from = stanza.getFrom(); + return from == null + || from.equals(account.getDomain()) + || from.equals(account.asBareJid()) + || from.equals(account); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d01880ab8c67be468d956f85cae7d948aa11ed8b..9d2ff4434a513d6a9013c61733a4ee1fdf230dd7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -898,6 +898,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void deliverIbbPacket(final Account account, final Iq packet) { + // TODO use extensions final String sid; final Element payload; final InbandBytestreamsTransport.PacketType packetType; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java index 8cebf080fd14835dde8d5a36002b929a63514f98..a0e778d5f93839c8848a00af8d57e5c52b3e73cc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -313,6 +313,7 @@ public class SocksByteStreamsTransport implements Transport { } final Iq iqRequest = new Iq(Iq.Type.GET); iqRequest.setTo(streamer); + // TODO urgent refactor to extension iqRequest.query(Namespace.BYTE_STREAMS); final SettableFuture candidateFuture = SettableFuture.create(); xmppConnection.sendIqPacket( diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java index b5c129e36dcb79aacb246ceffaeeadbaf5d0f924..c7c12d55ad41db26cba3a84080db7c5d794e969d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java @@ -6,6 +6,6 @@ import eu.siacs.conversations.xmpp.XmppConnection; public abstract class AbstractManager extends XmppConnection.Delegate { protected AbstractManager(final Context context, final XmppConnection connection) { - super(context, connection); + super(context.getApplicationContext(), connection); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..89d21602fec1084581b455c0781a85a7e3bc3295 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java @@ -0,0 +1,144 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.blocking.Block; +import im.conversations.android.xmpp.model.blocking.Blocklist; +import im.conversations.android.xmpp.model.blocking.Item; +import im.conversations.android.xmpp.model.blocking.Unblock; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class BlockingManager extends AbstractManager { + + private final XmppConnectionService service; + + private final HashSet blocklist = new HashSet<>(); + + public BlockingManager(final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + // TODO find a way to get rid of XmppConnectionService and use context instead + this.service = service; + } + + public void request() { + final var future = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Blocklist())); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final Iq result) { + final var blocklist = result.getExtension(Blocklist.class); + if (blocklist == null) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": invalid blocklist response"); + return; + } + final var addresses = itemsAsAddresses(blocklist.getItems()); + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": discovered blocklist with " + + addresses.size() + + " items"); + setBlocklist(addresses); + removeBlockedConversations(addresses); + service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.w( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not retrieve blocklist", + throwable); + } + }, + MoreExecutors.directExecutor()); + } + + public void pushBlock(final Iq request) { + if (connection.fromServer(request)) { + final var block = request.getExtension(Block.class); + final var addresses = itemsAsAddresses(block.getItems()); + synchronized (this.blocklist) { + this.blocklist.addAll(addresses); + } + this.removeBlockedConversations(addresses); + this.service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + this.connection.sendResultFor(request); + } else { + this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden()); + } + } + + public void pushUnblock(final Iq request) { + if (connection.fromServer(request)) { + final var unblock = request.getExtension(Unblock.class); + final var address = itemsAsAddresses(unblock.getItems()); + synchronized (this.blocklist) { + this.blocklist.removeAll(address); + } + this.service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + this.connection.sendResultFor(request); + } else { + this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden()); + } + } + + private void removeBlockedConversations(final Collection addresses) { + var removed = false; + for (final Jid address : addresses) { + removed |= service.removeBlockedConversations(getAccount(), address); + } + if (removed) { + service.updateConversationUi(); + } + } + + public ImmutableSet getBlocklist() { + synchronized (this.blocklist) { + return ImmutableSet.copyOf(this.blocklist); + } + } + + private void setBlocklist(final Collection addresses) { + synchronized (this.blocklist) { + this.blocklist.clear(); + this.blocklist.addAll(addresses); + } + } + + public boolean hasFeature() { + return getManager(DiscoManager.class).hasServerFeature(Namespace.BLOCKING); + } + + private static Set itemsAsAddresses(final Collection items) { + final var builder = new ImmutableSet.Builder(); + for (final var item : items) { + final var jid = Jid.Invalid.getNullForInvalid(item.getJid()); + if (jid == null) { + continue; + } + builder.add(jid); + } + return builder.build(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index 3c7c6215e5d949b9a1e246f540f7c1ee6ce09fef..cb143bb7b96c585dabb5126f2a3c54097645696d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -71,7 +71,7 @@ public class DiscoManager extends AbstractManager { Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION); private final List PRIVACY_SENSITIVE = Collections.singletonList( - "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone + Namespace.TIME // XEP-0202: Entity Time leaks time zone ); private final List VOIP_NAMESPACES = Arrays.asList( diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f180c485ee546b919869d446fdd825b1fa783d3b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java @@ -0,0 +1,43 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.generator.AbstractGenerator; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.time.Time; +import java.util.Locale; +import java.util.TimeZone; + +public class EntityTimeManager extends AbstractManager { + + public EntityTimeManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void request(final Iq request) { + final var appSettings = new AppSettings(this.context); + if (appSettings.isUseTor() || getAccount().isOnion()) { + this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden()); + return; + } + final var time = new Time(); + final long now = System.currentTimeMillis(); + time.setUniversalTime(AbstractGenerator.getTimestamp(now)); + final TimeZone ourTimezone = TimeZone.getDefault(); + final long offsetSeconds = ourTimezone.getOffset(now) / 1000; + final long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); + final long offsetHours = offsetSeconds / 3600; + final String hours; + if (offsetHours < 0) { + hours = String.format(Locale.US, "%03d", offsetHours); + } else { + hours = String.format(Locale.US, "%02d", offsetHours); + } + String minutes = String.format(Locale.US, "%02d", offsetMinutes); + time.setTimeZoneOffset(hours + ":" + minutes); + this.connection.sendResultFor(request, time); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java index f162a04d6797d7adc9d296d5b54877de31d0dd69..d16ccc41c57ec4bd559ec278749ba5976868e01e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java @@ -44,4 +44,8 @@ public class PingManager extends AbstractManager { }, MoreExecutors.directExecutor()); } + + public void pong(final Iq packet) { + this.connection.sendResultFor(packet); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ddf5051c0d32d8a33eaa31a710985eebd2e1a94d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java @@ -0,0 +1,276 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +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.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.android.AbstractPhoneContact; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.roster.Item; +import im.conversations.android.xmpp.model.roster.Query; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class RosterManager extends AbstractManager implements Roster { + + private final ReplacingSerialSingleThreadExecutor dbExecutor = + new ReplacingSerialSingleThreadExecutor(RosterManager.class.getName()); + + private final List contacts = new ArrayList<>(); + private String version; + + private final XmppConnectionService service; + + public RosterManager(final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + this.version = getAccount().getRosterVersion(); + ; + this.service = service; + } + + public void request() { + final var iq = new Iq(Iq.Type.GET); + final var query = iq.addExtension(new Query()); + final var version = this.version; + if (version != null) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": requesting roster version " + version); + query.setVersion(version); + } else { + Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + " requesting roster"); + } + final var future = connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final Iq result) { + final var query = result.getExtension(Query.class); + if (query == null) { + // No query in result means further modifications are sent via pushes + return; + } + final var version = query.getVersion(); + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": received full roster (version=" + + version + + ")"); + final var items = query.getItems(); + // In a roster result (Section 2.1.4), the client MUST ignore values of the + // 'subscription' + // attribute other than "none", "to", "from", or "both". + final var validItems = + Collections2.filter( + items, + i -> + Item.RESULT_SUBSCRIPTIONS.contains( + i.getSubscription()) + && Objects.nonNull(i.getJid())); + + setRosterItems(version, validItems); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": could not fetch roster", + throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private void setRosterItems(final String version, final Collection items) { + synchronized (this.contacts) { + markAllAsNotInRoster(); + for (final var item : items) { + processRosterItem(item); + } + this.version = version; + } + this.triggerUiUpdates(); + this.writeToDatabaseAsync(); + } + + private void modifyRosterItems(final String version, final Collection items) { + synchronized (this.contacts) { + for (final var item : items) { + processRosterItem(item); + } + this.version = version; + } + this.triggerUiUpdates(); + this.writeToDatabaseAsync(); + } + + private void triggerUiUpdates() { + this.service.updateConversationUi(); + this.service.updateRosterUi(); + this.service.getShortcutService().refresh(); + } + + public void push(final Iq packet) { + if (connection.fromServer(packet)) { + final var query = packet.getExtension(Query.class); + final var version = query.getVersion(); + modifyRosterItems(version, query.getItems()); + Log.d( + Config.LOGTAG, + getAccount().getJid() + ": received roster push (version=" + version + ")"); + } else { + connection.sendErrorFor(packet, Error.Type.AUTH, new Condition.Forbidden()); + } + } + + private void processRosterItem(final Item item) { + // this is verbatim the original code from IqParser. + // TODO there are likely better ways to handle roster management + final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid")); + if (jid == null) { + return; + } + final var name = item.getItemName(); + final var subscription = item.getSubscription(); + // getContactInternal is not synchronized because all access to processRosterItem is + final var contact = getContactInternal(jid); + boolean bothPre = + contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + contact.parseGroupsFromElement(item); + } + if (subscription == Item.Subscription.REMOVE) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + // TODO use subscription; and set asking separately + contact.parseSubscriptionFromElement(item); + } + boolean both = + contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); + if ((both != bothPre) && both) { + final var account = getAccount(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": gained mutual presence subscription with " + + contact.getJid()); + final var axolotlService = account.getAxolotlService(); + if (axolotlService != null) { + axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); + } + } + service.getAvatarService().clear(contact); + } + + @Override + @NonNull + public Contact getContact(@NonNull final Jid jid) { + synchronized (this.contacts) { + return this.getContactInternal(jid); + } + } + + @NonNull + public Contact getContactInternal(@NonNull final Jid jid) { + final var existing = + Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null); + if (existing != null) { + return existing; + } + final var contact = new Contact(jid.asBareJid()); + contact.setAccount(getAccount()); + this.contacts.add(contact); + return contact; + } + + @Override + @Nullable + public Contact getContactFromContactList(@NonNull final Jid jid) { + synchronized (this.contacts) { + final var contact = + Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid())); + if (contact != null && contact.showInContactList()) { + return contact; + } else { + return null; + } + } + } + + @Override + public List getContacts() { + synchronized (this.contacts) { + return ImmutableList.copyOf(this.contacts); + } + } + + @Override + public ImmutableList getWithSystemAccounts( + final Class clazz) { + final int option = Contact.getOption(clazz); + synchronized (this.contacts) { + return ImmutableList.copyOf( + Collections2.filter(this.contacts, c -> c.getOption(option))); + } + } + + public void clearPresences() { + synchronized (this.contacts) { + for (final var contact : this.contacts) { + contact.clearPresences(); + } + } + } + + private void markAllAsNotInRoster() { + for (final var contact : this.contacts) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void restore() { + synchronized (this.contacts) { + this.contacts.clear(); + this.contacts.addAll(getDatabase().readRoster(getAccount())); + } + } + + public void writeToDatabaseAsync() { + this.dbExecutor.execute(this::writeToDatabase); + } + + public void writeToDatabase() { + final var account = getAccount(); + final List contacts; + final String version; + synchronized (this.contacts) { + contacts = ImmutableList.copyOf(this.contacts); + version = this.version; + } + getDatabase().writeRoster(account, version, contacts); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java new file mode 100644 index 0000000000000000000000000000000000000000..d9ae28f0e78b1d64bb1913598487d87fb2713d97 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.xmpp.manager; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.up.Push; + +public class UnifiedPushManager extends AbstractManager { + + private final XmppConnectionService service; + + public UnifiedPushManager( + final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + this.service = service; + } + + public void push(final Iq packet) { + final Jid transport = packet.getFrom(); + final var push = packet.getOnlyExtension(Push.class); + if (push == null || transport == null) { + connection.sendErrorFor(packet, Error.Type.MODIFY, new Condition.BadRequest()); + return; + } + if (service.processUnifiedPushMessage(getAccount(), transport, push)) { + connection.sendResultFor(packet); + } else { + connection.sendErrorFor(packet, Error.Type.CANCEL, new Condition.ItemNotFound()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java index 6f5d00b3e566d1651059d75fb350c2a233959439..18f1158e1f8d5ed1d611174f7f651dfe7b9ee03f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java @@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement public class Block extends Extension { @@ -9,4 +10,8 @@ public class Block extends Extension { public Block() { super(Block.class); } + + public Collection getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java index a56662d77152bf2d22bc11d76f8d01cb90f30c52..799ec4df75250d89e60f54cbd9336590d93353b5 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java @@ -2,10 +2,15 @@ package im.conversations.android.xmpp.model.blocking; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement public class Blocklist extends Extension { public Blocklist() { super(Blocklist.class); } + + public Collection getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java index 90cec110c050788661af7afbae475d68a7906d26..e04c00f9ddd4076f0a0852275ea861c41defe340 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java @@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement public class Unblock extends Extension { @@ -9,4 +10,8 @@ public class Unblock extends Extension { public Unblock() { super(Unblock.class); } + + public Collection getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java new file mode 100644 index 0000000000000000000000000000000000000000..f4e63148be66d7e69a52add6e5d8ac9c1e712a02 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.ibb; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Close extends InBandByteStream { + + public Close() { + super(Close.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java new file mode 100644 index 0000000000000000000000000000000000000000..e7998fc5a5cc3db439a8acda09eee7b654ed60c6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.ibb; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; + +@XmlElement +public class Data extends InBandByteStream implements ByteContent { + + public Data() { + super(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java b/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java new file mode 100644 index 0000000000000000000000000000000000000000..e1e73c014e5a592506482baf88b8211a5ae24f40 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.ibb; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class InBandByteStream extends Extension { + + public InBandByteStream(Class clazz) { + super(clazz); + } + + public String getSid() { + return this.getAttribute("sid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java new file mode 100644 index 0000000000000000000000000000000000000000..7759a7ff0f89dfd2916ab1be7d9b8cc1531e0253 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.ibb; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Open extends InBandByteStream { + + public Open() { + super(Open.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java b/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..38affe359a288329b3473597fbccb2bb72e2210b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.IBB) +package im.conversations.android.xmpp.model.ibb; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java index 616f6ae0b68cd6a13fdc9096d18df47f5852a8fb..745905995b0a0f8f7f530dcfb5d4a2c8ee9d27f9 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java @@ -3,6 +3,7 @@ package im.conversations.android.xmpp.model.roster; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement(name = "query", namespace = Namespace.ROSTER) public class Query extends Extension { @@ -18,4 +19,8 @@ public class Query extends Extension { public String getVersion() { return this.getAttribute("ver"); } + + public Collection getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/time/Time.java b/src/main/java/im/conversations/android/xmpp/model/time/Time.java new file mode 100644 index 0000000000000000000000000000000000000000..6066e5d955f31df53bde32923a12dbe7974c7834 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/Time.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.time; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Time extends Extension { + + public Time() { + super(Time.class); + } + + public void setTimeZoneOffset(final String tzo) { + this.addExtension(new TimeZoneOffset()).setContent(tzo); + } + + public void setUniversalTime(final String utc) { + this.addExtension(new UniversalTime()).setContent(utc); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java b/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java new file mode 100644 index 0000000000000000000000000000000000000000..1be62ea9774d7043d3347984993d939fff199ec9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.time; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "tzo") +public class TimeZoneOffset extends Extension { + + public TimeZoneOffset() { + super(TimeZoneOffset.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java b/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java new file mode 100644 index 0000000000000000000000000000000000000000..d1d773a69d4d1884d98598d0035c58773445cbb9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.time; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "utc") +public class UniversalTime extends Extension { + + public UniversalTime() { + super(UniversalTime.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/package-info.java b/src/main/java/im/conversations/android/xmpp/model/time/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d86a2b9940e98832fcad1850a4e8e66ff0e0aa12 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.TIME) +package im.conversations.android.xmpp.model.time; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/up/Push.java b/src/main/java/im/conversations/android/xmpp/model/up/Push.java new file mode 100644 index 0000000000000000000000000000000000000000..ab1365328c5962348f93a18ea4c382d47c3576f4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/up/Push.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.up; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Push extends Extension implements ByteContent { + + public Push() { + super(Push.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/up/package-info.java b/src/main/java/im/conversations/android/xmpp/model/up/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..fd230e3265d800d23ee2c933083f6d8333009c51 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/up/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.UNIFIED_PUSH) +package im.conversations.android.xmpp.model.up; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 093ba8ce0e772901fc2a2be89d75aa76b61683a1..6092b0e8f406a6b979f3b88618efaa0ef114e7c2 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -7,6 +7,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; public class BindProcessor extends XmppConnection.Delegate implements Runnable { @@ -49,7 +50,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { } } - account.getRoster().clearPresences(); + connection.getManager(RosterManager.class).clearPresences(); synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.clear(); } @@ -59,7 +60,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { service.getJingleConnectionManager().notifyRebound(account); service.getQuickConversationsService().considerSyncBackground(false); - connection.fetchRoster(); + getManager(RosterManager.class).request(); if (features.bookmarks2()) { service.fetchBookmarks2(account); diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index 54cfc5ee60f7357956aeb584b9cff6cdd8df4272..c144c628c2a8c8ea0692a93b1a41b54ec1a6bcf6 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.utils.TLSSocketFactory; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; import io.michaelrocks.libphonenumber.android.Phonenumber; import java.io.BufferedWriter; @@ -407,7 +408,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService } refresh(account, contacts.values()); if (!considerSync(account, contacts, forced)) { - service.syncRoster(account); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); } } } @@ -422,7 +423,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService } private void refresh(Account account, Collection contacts) { - for (Contact contact : + for (final var contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) { final Uri uri = contact.getSystemAccount(); if (uri == null) { @@ -498,9 +499,11 @@ public class QuickConversationsService extends AbstractQuickConversationsService final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION); if (phoneBook != null) { - final List withSystemAccounts = - account.getRoster() - .getWithSystemAccounts(PhoneNumberContact.class); + final var remaining = + new ArrayList<>( + account.getRoster() + .getWithSystemAccounts( + PhoneNumberContact.class)); for (Entry entry : Entry.ofPhoneBook(phoneBook)) { final PhoneNumberContact phoneContact = contacts.get(entry.getNumber()); @@ -514,10 +517,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService if (needsCacheClean) { service.getAvatarService().clear(contact); } - withSystemAccounts.remove(contact); + remaining.remove(contact); } } - for (final Contact contact : withSystemAccounts) { + for (final Contact contact : remaining) { final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class); if (needsCacheClean) { @@ -539,7 +542,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService + ": failed to sync contact list with api server"); } mRunningSyncJobs.decrementAndGet(); - service.syncRoster(account); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); service.updateRosterUi(); }); return true; diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java index 021978dfa354e0edb4e3debbda0c89d7978d267e..7ea843ddebce14de07aa642c8571c8911b4b9f31 100644 --- a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -82,7 +82,7 @@ public class EntityCapabilitiesTest { } @Test - public void entityCapsOpenFire() throws IOException { + public void entityCapsOpenFireOrg() throws IOException { final String xml = """ @@ -206,6 +206,104 @@ public class EntityCapabilitiesTest { Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var); } + @Test + public void entityCapsOpenFireTestServer() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://jabber.org/network/serverinfo + + + xmpp:admin@example.org + mailto:admin@example.com + + + + + urn:xmpp:dataforms:softwareinfo + + + Linux + + + 6.8.0-59-generic amd64 - Java 21.0.7 + + + Openfire + + + 5.0.0 Alpha + + + + +"""; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(Iq.class)); + final var iq = (Iq) element; + final InfoQuery info = iq.getExtension(InfoQuery.class); + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("3wkXXN9QL/i/AyVoHaqaiTT8BFA=", var); + } + @Test public void caps2() throws IOException { final String xml = From 0335fb876ade3641bbced56336424f9138a450f8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 16 May 2025 10:39:24 +0200 Subject: [PATCH 03/87] do roster and blocklist modifications via managers --- .../conversations/entities/Blockable.java | 16 ++- .../siacs/conversations/entities/Contact.java | 15 +-- .../conversations/entities/Conversation.java | 1 + .../conversations/entities/RawBlockable.java | 4 +- .../conversations/generator/IqGenerator.java | 25 ---- .../generator/PresenceGenerator.java | 47 ------- .../conversations/parser/PresenceParser.java | 6 +- .../services/XmppConnectionService.java | 126 ++++-------------- .../ui/ContactDetailsActivity.java | 34 ++--- .../ui/ConversationFragment.java | 12 +- .../ui/StartConversationActivity.java | 2 +- .../siacs/conversations/ui/XmppActivity.java | 14 +- .../conversations/xmpp/XmppConnection.java | 2 +- .../xmpp/manager/BlockingManager.java | 85 ++++++++++++ .../xmpp/manager/PresenceManager.java | 41 ++++++ .../xmpp/manager/RosterManager.java | 103 +++++++++++++- .../android/xmpp/model/blocking/Item.java | 4 + .../android/xmpp/model/nick/Nick.java | 5 + .../android/xmpp/model/reporting/Report.java | 16 +++ .../android/xmpp/model/roster/Group.java | 5 + .../android/xmpp/model/roster/Item.java | 25 +++- .../android/xmpp/model/stanza/Presence.java | 39 ++++++ .../android/xmpp/model/unique/StanzaId.java | 5 + .../android/xmpp/processor/BindProcessor.java | 2 +- 24 files changed, 403 insertions(+), 231 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/reporting/Report.java diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java index 0d1ab6361198fad4df90651518fbe6d1119db669..98f53d9dddc407e0d07bbfc69a050ba19ddbd727 100644 --- a/src/main/java/eu/siacs/conversations/entities/Blockable.java +++ b/src/main/java/eu/siacs/conversations/entities/Blockable.java @@ -1,11 +1,17 @@ package eu.siacs.conversations.entities; +import androidx.annotation.NonNull; import eu.siacs.conversations.xmpp.Jid; public interface Blockable { - boolean isBlocked(); - boolean isDomainBlocked(); - Jid getBlockedJid(); - Jid getJid(); - Account getAccount(); + boolean isBlocked(); + + boolean isDomainBlocked(); + + @NonNull + Jid getBlockedJid(); + + Jid getJid(); + + Account getAccount(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index f13ffa973ad35a5b69bcedeeb4fa14aea134c2ec..d9a215f41b5039f173acf4e5d2a8e5af7820cf0c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -316,7 +316,7 @@ public class Contact implements ListItem, Blockable { this.systemAccount = lookupUri; } - private Collection getGroups(final boolean unique) { + public Collection getGroups(final boolean unique) { final Collection groups = unique ? new HashSet<>() : new ArrayList<>(); for (int i = 0; i < this.groups.length(); ++i) { try { @@ -428,18 +428,6 @@ public class Contact implements ListItem, Blockable { } } - public Element asElement() { - final Element item = new Element("item"); - item.setAttribute("jid", this.jid); - if (this.serverName != null) { - item.setAttribute("name", this.serverName); - } - for (String group : getGroups(false)) { - item.addChild("group").setContent(group); - } - return item; - } - @Override public int compareTo(@NonNull final ListItem another) { return this.getDisplayName().compareToIgnoreCase(another.getDisplayName()); @@ -490,6 +478,7 @@ public class Contact implements ListItem, Blockable { } @Override + @NonNull public Jid getBlockedJid() { if (isDomainBlocked()) { return getJid().getDomain(); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 6394ee1a477e936b81a89fe19b0a46e38edcd1b4..e50d908b59e25d0eaaed5aacd9ca1a2828651d60 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -533,6 +533,7 @@ public class Conversation extends AbstractEntity } @Override + @NonNull public Jid getBlockedJid() { return getContact().getBlockedJid(); } diff --git a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java index 97f63d99cfe6eb4a13bb224687a28f0b3309dda4..24117a168956b4531aaf9720b3ccaa6dfde205fa 100644 --- a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java +++ b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.Context; import android.text.TextUtils; +import androidx.annotation.NonNull; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import java.util.Collections; @@ -13,7 +14,7 @@ public class RawBlockable implements ListItem, Blockable { private final Account account; private final Jid jid; - public RawBlockable(Account account, Jid jid) { + public RawBlockable(@NonNull Account account, @NonNull Jid jid) { this.account = account; this.jid = jid; } @@ -29,6 +30,7 @@ public class RawBlockable implements ListItem, Blockable { } @Override + @NonNull public Jid getBlockedJid() { return this.jid; } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 163250e4cf7ece3676c2771981798b891eb9fccb..fcb8273cda6dc4fda32cb0e957151421cccf8cdf 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -316,31 +316,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq generateSetBlockRequest( - final Jid jid, final boolean reportSpam, final String serverMsgId) { - final Iq iq = new Iq(Iq.Type.SET); - final Element block = iq.addChild("block", Namespace.BLOCKING); - final Element item = block.addChild("item").setAttribute("jid", jid); - if (reportSpam) { - final Element report = item.addChild("report", Namespace.REPORTING); - report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM); - if (serverMsgId != null) { - final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS); - stanzaId.setAttribute("by", jid); - stanzaId.setAttribute("id", serverMsgId); - } - } - Log.d(Config.LOGTAG, iq.toString()); - return iq; - } - - public Iq generateSetUnblockRequest(final Jid jid) { - final Iq iq = new Iq(Iq.Type.SET); - final Element block = iq.addChild("unblock", Namespace.BLOCKING); - block.addChild("item").setAttribute("jid", jid); - return iq; - } - public Iq generateSetPassword(final Account account, final String newPassword) { final Iq packet = new Iq(Iq.Type.SET); packet.setTo(account.getDomain()); diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 60fa01872680da3e9e2248e6b9056887dbc9a1e3..21301e6a7658f25a27c8ac0a2763ed59c1af02b4 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -1,11 +1,8 @@ package eu.siacs.conversations.generator; -import android.text.TextUtils; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.model.stanza.Presence; @@ -15,50 +12,6 @@ public class PresenceGenerator extends AbstractGenerator { super(service); } - private im.conversations.android.xmpp.model.stanza.Presence subscription( - String type, Contact contact) { - im.conversations.android.xmpp.model.stanza.Presence packet = - new im.conversations.android.xmpp.model.stanza.Presence(); - packet.setAttribute("type", type); - packet.setTo(contact.getJid()); - packet.setFrom(contact.getAccount().getJid().asBareJid()); - return packet; - } - - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( - final Contact contact) { - return requestPresenceUpdatesFrom(contact, null); - } - - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( - final Contact contact, final String preAuth) { - im.conversations.android.xmpp.model.stanza.Presence packet = - subscription("subscribe", contact); - String displayName = contact.getAccount().getDisplayName(); - if (!TextUtils.isEmpty(displayName)) { - packet.addChild("nick", Namespace.NICK).setContent(displayName); - } - if (preAuth != null) { - packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - } - return packet; - } - - public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom( - Contact contact) { - return subscription("unsubscribe", contact); - } - - public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo( - Contact contact) { - return subscription("unsubscribed", contact); - } - - public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo( - Contact contact) { - return subscription("subscribed", contact); - } - public im.conversations.android.xmpp.model.stanza.Presence selfPresence( Account account, Presence.Availability status) { return selfPresence(account, status, true); diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 3cdd86b600c44c668869dba930ef07f694a5bb62..eb58377ff8e650605eb7587f48be5a4018e46b1d 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -24,6 +24,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.Entity; @@ -445,8 +446,9 @@ public class PresenceParser extends AbstractParser mXmppConnectionService.getAvatarService().clear(contact); } if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - mXmppConnectionService.sendPresencePacket( - account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); + connection + .getManager(PresenceManager.class) + .subscribed(contact.getJid().asBareJid()); } else { contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); final Conversation conversation = diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 00e353bc802ee07f508c1595d24763b2c8befdce..34323ed4c61b7575f45aaf97b39668820c5f8aa8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -137,7 +137,9 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; +import eu.siacs.conversations.xmpp.manager.BlockingManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; @@ -1902,7 +1904,7 @@ public class XmppConnectionService extends Service { + ": adding " + contact.getJid() + " on sending message"); - createContact(contact, true); + createContact(contact); } } @@ -3022,10 +3024,13 @@ public class XmppConnectionService extends Service { } } - public void stopPresenceUpdatesTo(Contact contact) { + public void stopPresenceUpdatesTo(final Contact contact) { Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString()); - sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact)); contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + contact.getAccount() + .getXmppConnection() + .getManager(PresenceManager.class) + .unsubscribed(contact.getJid().asBareJid()); } public void createAccount(final Account account) { @@ -4690,57 +4695,20 @@ public class XmppConnectionService extends Service { updateConversationUi(); } - // TODO move this to RosterManager - public void syncDirtyContacts(Account account) { - for (Contact contact : account.getRoster().getContacts()) { - if (contact.getOption(Contact.Options.DIRTY_PUSH)) { - pushContactToServer(contact); - } - if (contact.getOption(Contact.Options.DIRTY_DELETE)) { - deleteContactOnServer(contact); - } - } + public void createContact(final Contact contact) { + createContact(contact, null); } - public void createContact(final Contact contact, final boolean autoGrant) { - createContact(contact, autoGrant, null); + public void createContact(final Contact contact, final String preAuth) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + final var connection = contact.getAccount().getXmppConnection(); + connection.getManager(RosterManager.class).addRosterItem(contact, preAuth); } - public void createContact( - final Contact contact, final boolean autoGrant, final String preAuth) { - if (autoGrant) { - contact.setOption(Contact.Options.PREEMPTIVE_GRANT); - contact.setOption(Contact.Options.ASKING); - } - pushContactToServer(contact, preAuth); - } - - public void pushContactToServer(final Contact contact) { - pushContactToServer(contact, null); - } - - private void pushContactToServer(final Contact contact, final String preAuth) { - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.setOption(Contact.Options.DIRTY_PUSH); - final Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final boolean ask = contact.getOption(Contact.Options.ASKING); - final boolean sendUpdates = - contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final Iq iq = new Iq(Iq.Type.SET); - iq.query(Namespace.ROSTER).addChild(contact.asElement()); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - if (sendUpdates) { - sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } - if (ask) { - sendPresencePacket( - account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); - } - } else { - account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); - } + public void deleteContactOnServer(final Contact contact) { + final var connection = contact.getAccount().getXmppConnection(); + connection.getManager(RosterManager.class).deleteRosterItem(contact); } public void publishMucAvatar( @@ -5312,20 +5280,6 @@ public class XmppConnectionService extends Service { } } - public void deleteContactOnServer(Contact contact) { - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.setOption(Contact.Options.DIRTY_DELETE); - Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final Iq iq = new Iq(Iq.Type.SET); - Element item = iq.query(Namespace.ROSTER).addChild("item"); - item.setAttribute("jid", contact.getJid()); - item.setAttribute("subscription", "remove"); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - } - } - public void updateConversation(final Conversation conversation) { mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); } @@ -6145,29 +6099,11 @@ public class XmppConnectionService extends Service { public boolean sendBlockRequest( final Blockable blockable, final boolean reportSpam, final String serverMsgId) { - if (blockable != null && blockable.getBlockedJid() != null) { - final var account = blockable.getAccount(); - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket( - account, - getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - account.getBlocklist().add(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } - }); - if (blockable.getBlockedJid().isFullJid()) { - return false; - } else if (removeBlockedConversations(blockable.getAccount(), jid)) { - updateConversationUi(); - return true; - } else { - return false; - } - } else { - return false; - } + final var account = blockable.getAccount(); + final var connection = account.getXmppConnection(); + return connection + .getManager(BlockingManager.class) + .block(blockable, reportSpam, serverMsgId); } public boolean removeBlockedConversations(final Account account, final Jid blockedJid) { @@ -6202,19 +6138,9 @@ public class XmppConnectionService extends Service { } public void sendUnblockRequest(final Blockable blockable) { - if (blockable != null && blockable.getJid() != null) { - final var account = blockable.getAccount(); - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket( - account, - getIqGenerator().generateSetUnblockRequest(jid), - response -> { - if (response.getType() == Iq.Type.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } - }); - } + final var account = blockable.getAccount(); + final var connection = account.getXmppConnection(); + connection.getManager(BlockingManager.class).unblock(blockable); } public void publishDisplayName(final Account account) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 039c26e47fc303e7f4fbea9b7b8e676df7b92261..06a1183d369490c51ddd9040d53cf713c9d74ec7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -70,6 +70,8 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Collection; import java.util.Collections; @@ -109,11 +111,10 @@ public class ContactDetailsActivity extends OmemoActivity } } else { contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - xmppConnectionService.sendPresencePacket( - contact.getAccount(), - xmppConnectionService - .getPresenceGenerator() - .stopPresenceUpdatesTo(contact)); + final var connection = contact.getAccount().getXmppConnection(); + connection + .getManager(PresenceManager.class) + .unsubscribed(contact.getJid().asBareJid()); } } }; @@ -122,18 +123,15 @@ public class ContactDetailsActivity extends OmemoActivity @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + final var connection = contact.getAccount().getXmppConnection(); if (isChecked) { - xmppConnectionService.sendPresencePacket( - contact.getAccount(), - xmppConnectionService - .getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); + connection + .getManager(PresenceManager.class) + .subscribe(contact.getJid().asBareJid()); } else { - xmppConnectionService.sendPresencePacket( - contact.getAccount(), - xmppConnectionService - .getPresenceGenerator() - .stopPresenceUpdatesFrom(contact)); + connection + .getManager(PresenceManager.class) + .unsubscribe(contact.getJid().asBareJid()); } } }; @@ -343,8 +341,10 @@ public class ContactDetailsActivity extends OmemoActivity R.string.contact_name, value -> { contact.setServerName(value); - ContactDetailsActivity.this.xmppConnectionService - .pushContactToServer(contact); + final var connection = contact.getAccount().getXmppConnection(); + connection + .getManager(RosterManager.class) + .addRosterItem(contact, null); populateView(); return null; }, diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index bf87e21271952bc9f7b145836e91f7895e8ad84b..0e5df61721920558628f29547ac9c26d97e8c423 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -127,6 +127,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.Arrays; @@ -446,7 +447,7 @@ public class ConversationFragment extends XmppFragment public void onClick(View v) { final Contact contact = conversation == null ? null : conversation.getContact(); if (contact != null) { - activity.xmppConnectionService.createContact(contact, true); + activity.xmppConnectionService.createContact(contact); activity.switchToContactDetails(contact); } } @@ -458,11 +459,10 @@ public class ConversationFragment extends XmppFragment public void onClick(View v) { final Contact contact = conversation == null ? null : conversation.getContact(); if (contact != null) { - activity.xmppConnectionService.sendPresencePacket( - contact.getAccount(), - activity.xmppConnectionService - .getPresenceGenerator() - .sendPresenceUpdatesTo(contact)); + final var connection = contact.getAccount().getXmppConnection(); + connection + .getManager(PresenceManager.class) + .subscribed(contact.getJid().asBareJid()); hideSnackbar(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 9adb4e7bec4c8fb430916c548344386cd8c4352f..721a85739fc0babc55871a735d206514ad42648e 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -647,7 +647,7 @@ public class StartConversationActivity extends XmppActivity invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH); - xmppConnectionService.createContact(contact, true, preAuth); + xmppConnectionService.createContact(contact, preAuth); if (invite != null && invite.hasFingerprints()) { xmppConnectionService.verifyFingerprints( contact, invite.getFingerprints()); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index d597599bd1c97a52441136275eb3da664479e19e..7d899b4ee7fa923af30e0feb7db9ed4e542147c2 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -85,6 +85,7 @@ import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -894,7 +895,7 @@ public abstract class XmppActivity extends ActionBarActivity { builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton( getString(R.string.add_contact), - (dialog, which) -> xmppConnectionService.createContact(contact, true)); + (dialog, which) -> xmppConnectionService.createContact(contact)); builder.create().show(); } @@ -906,13 +907,10 @@ public abstract class XmppActivity extends ActionBarActivity { builder.setPositiveButton( R.string.request_now, (dialog, which) -> { - if (xmppConnectionServiceBound) { - xmppConnectionService.sendPresencePacket( - contact.getAccount(), - xmppConnectionService - .getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); - } + final var connection = contact.getAccount().getXmppConnection(); + connection + .getManager(PresenceManager.class) + .subscribe(contact.getJid().asBareJid()); }); builder.create().show(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 3e052ebf8e0643ae0bd335553923ff97efd9c57f..dec02a32a3614c0ed97adfee4e51b5d271a90296 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3071,7 +3071,7 @@ public class XmppConnection implements Runnable { } public boolean blocking() { - return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING); + return connection.getManager(BlockingManager.class).hasFeature(); } public boolean spamReporting() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java index 89d21602fec1084581b455c0781a85a7e3bc3295..63bd62af34c465a1be8b88b25f181b91e0dcd925 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java @@ -2,11 +2,13 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; @@ -18,7 +20,9 @@ import im.conversations.android.xmpp.model.blocking.Item; import im.conversations.android.xmpp.model.blocking.Unblock; import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.reporting.Report; import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.unique.StanzaId; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -141,4 +145,85 @@ public class BlockingManager extends AbstractManager { } return builder.build(); } + + public boolean block( + @NonNull final Blockable blockable, + final boolean reportSpam, + @Nullable final String serverMsgId) { + final var address = blockable.getBlockedJid(); + final var iq = new Iq(Iq.Type.SET); + final var block = iq.addExtension(new Block()); + final var item = block.addExtension(new Item()); + item.setJid(address); + if (reportSpam) { + final var report = item.addExtension(new Report()); + report.setReason(Namespace.REPORTING_REASON_SPAM); + if (serverMsgId != null) { + // XEP has a 'by' attribute that is the same as reported jid but that doesn't make + // sense this the 'by' attribute in the stanza-id refers to the arriving entity + // (usually the account or the MUC) + report.addExtension(new StanzaId(serverMsgId)); + } + } + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Iq result) { + synchronized (blocklist) { + blocklist.add(address); + } + service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": could not block " + address, + throwable); + } + }, + MoreExecutors.directExecutor()); + if (address.isFullJid()) { + return false; + } else if (service.removeBlockedConversations(getAccount(), address)) { + service.updateConversationUi(); + return true; + } else { + return false; + } + } + + public void unblock(@NonNull final Blockable blockable) { + final var address = blockable.getBlockedJid(); + final var iq = new Iq(Iq.Type.SET); + final var unblock = iq.addExtension(new Unblock()); + final var item = unblock.addExtension(new Item()); + item.setJid(address); + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Iq result) { + synchronized (blocklist) { + blocklist.remove(address); + } + service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not unblock " + + address, + t); + } + }, + MoreExecutors.directExecutor()); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java index 14de9f576f8590bb886091717411d5005af247b3..21dc794be3e171a7f2b8573c235c3850b95b18a8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java @@ -1,12 +1,16 @@ package eu.siacs.conversations.xmpp.manager; import android.content.Context; +import com.google.common.base.Strings; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.EntityCapabilities2; import im.conversations.android.xmpp.ServiceDescription; import im.conversations.android.xmpp.model.capabilties.Capabilities; import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities; +import im.conversations.android.xmpp.model.nick.Nick; +import im.conversations.android.xmpp.model.pars.PreAuth; import im.conversations.android.xmpp.model.pgp.Signed; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.HashMap; @@ -21,6 +25,43 @@ public class PresenceManager extends AbstractManager { super(context, connection); } + public void subscribe(final Jid address) { + subscribe(address, null); + } + + public void subscribe(final Jid address, final String preAuth) { + + var presence = new Presence(Presence.Type.SUBSCRIBE); + presence.setTo(address); + + final var displayName = getAccount().getDisplayName(); + if (!Strings.isNullOrEmpty(displayName)) { + presence.addExtension(new Nick(displayName)); + } + if (preAuth != null) { + presence.addExtension(new PreAuth()).setToken(preAuth); + } + this.connection.sendPresencePacket(presence); + } + + public void unsubscribe(final Jid address) { + var presence = new Presence(Presence.Type.UNSUBSCRIBE); + presence.setTo(address); + this.connection.sendPresencePacket(presence); + } + + public void unsubscribed(final Jid address) { + var presence = new Presence(Presence.Type.UNSUBSCRIBED); + presence.setTo(address); + this.connection.sendPresencePacket(presence); + } + + public void subscribed(final Jid address) { + var presence = new Presence(Presence.Type.SUBSCRIBED); + presence.setTo(address); + this.connection.sendPresencePacket(presence); + } + public Presence getPresence(final Presence.Availability availability, final boolean personal) { final var account = connection.getAccount(); final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java index ddf5051c0d32d8a33eaa31a710985eebd2e1a94d..ff2d13e50748efb5af9bc3c271882a6e4f5f9ee8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java @@ -212,7 +212,7 @@ public class RosterManager extends AbstractManager implements Roster { public Contact getContactFromContactList(@NonNull final Jid jid) { synchronized (this.contacts) { final var contact = - Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid())); + Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null); if (contact != null && contact.showInContactList()) { return contact; } else { @@ -273,4 +273,105 @@ public class RosterManager extends AbstractManager implements Roster { } getDatabase().writeRoster(account, version, contacts); } + + public void syncDirtyContacts() { + synchronized (this.contacts) { + for (final var contact : this.contacts) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + addRosterItem(contact, null); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + deleteRosterItem(contact); + } + } + } + } + + public void addRosterItem(final Contact contact, final String preAuth) { + final var address = contact.getJid().asBareJid(); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + // sync the 'dirty push' flag to disk in case we are offline + this.writeToDatabaseAsync(); + final boolean ask = contact.getOption(Contact.Options.ASKING); + final boolean sendUpdates = + contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + final Iq iq = new Iq(Iq.Type.SET); + final var query = iq.addExtension(new Query()); + final var item = query.addExtension(new Item()); + item.setJid(address); + final var serverName = contact.getServerName(); + if (serverName != null) { + item.setItemName(serverName); + } + item.setGroups(contact.getGroups(false)); + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Iq result) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": pushed roster item " + + address); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not push roster item " + + address, + t); + } + }, + MoreExecutors.directExecutor()); + if (sendUpdates) { + getManager(PresenceManager.class).subscribed(contact.getJid().asBareJid()); + } + if (ask) { + getManager(PresenceManager.class).subscribe(contact.getJid().asBareJid(), preAuth); + } + } + + public void deleteRosterItem(final Contact contact) { + final var address = contact.getJid().asBareJid(); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.setOption(Contact.Options.DIRTY_DELETE); + this.writeToDatabaseAsync(); + final Iq iq = new Iq(Iq.Type.SET); + final var query = iq.addExtension(new Query()); + final var item = query.addExtension(new Item()); + item.setJid(address); + item.setSubscription(Item.Subscription.REMOVE); + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(final Iq result) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": removed roster item " + + address); + } + + @Override + public void onFailure(final @NonNull Throwable t) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not remove roster item " + + address, + t); + } + }, + MoreExecutors.directExecutor()); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java index 647b0ae9912de215bffb4f5057bbc95dae3e6bfd..e93dbe06b2643146e577e8262f30de8280103661 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java @@ -14,4 +14,8 @@ public class Item extends Extension { public Jid getJid() { return getAttributeAsJid("jid"); } + + public void setJid(final Jid address) { + this.setAttribute("jid", address); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java index e9a98512823681750c9bdd5c79c8507ea2b9ce23..81e55062f8ec470306c419af16063ba1f7aa8bfb 100644 --- a/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java +++ b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java @@ -10,4 +10,9 @@ public class Nick extends Extension { public Nick() { super(Nick.class); } + + public Nick(final String nick) { + this(); + this.setContent(nick); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/reporting/Report.java b/src/main/java/im/conversations/android/xmpp/model/reporting/Report.java new file mode 100644 index 0000000000000000000000000000000000000000..a5a763c2d035e6aa0c758199df3d669a59a1ec12 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reporting/Report.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.reporting; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.REPORTING) +public class Report extends Extension { + public Report() { + super(Report.class); + } + + public void setReason(final String reason) { + this.setAttribute("reason", reason); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Group.java b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java index 9f36efae7c7d6defa57a3b142a55725e2ae386df..355e13c7f8fb22dea3e85473dcb9eedc2e5c719b 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Group.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java @@ -9,4 +9,9 @@ public class Group extends Extension { public Group() { super(Group.class); } + + public Group(final String group) { + this(); + this.setContent(group); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java index 0a2e0ef54add741e8141d333dc9d21e510e309ec..a8bcc3e70433696713a0319a3c423d851c653a01 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java @@ -1,13 +1,10 @@ package im.conversations.android.xmpp.model.roster; import com.google.common.collect.Collections2; - import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; - import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; - import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -28,10 +25,18 @@ public class Item extends Extension { return getAttributeAsJid("jid"); } + public void setJid(final Jid jid) { + this.setAttribute("jid", jid); + } + public String getItemName() { return this.getAttribute("name"); } + public void setItemName(final String serverName) { + this.setAttribute("name", serverName); + } + public boolean isPendingOut() { return "subscribe".equalsIgnoreCase(this.getAttribute("ask")); } @@ -45,12 +50,26 @@ public class Item extends Extension { } } + public void setSubscription(final Subscription subscription) { + if (subscription == null) { + this.removeAttribute("subscription"); + } else { + this.setAttribute("subscription", subscription.toString().toLowerCase(Locale.ROOT)); + } + } + public Collection getGroups() { return Collections2.filter( Collections2.transform(getExtensions(Group.class), Element::getContent), Objects::nonNull); } + public void setGroups(final Collection groups) { + for (final String group : groups) { + this.addExtension(new Group()); + } + } + public enum Subscription { NONE, TO, diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java index 0887fde2c649978fec81669efd7ba2673d519e4f..e4d1747f429acf444d4693172f4571b6751ed155 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -5,6 +5,7 @@ import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; import im.conversations.android.xmpp.model.jabber.Show; import im.conversations.android.xmpp.model.jabber.Status; +import java.util.Locale; @XmlElement public class Presence extends Stanza implements EntityCapabilities { @@ -13,6 +14,11 @@ public class Presence extends Stanza implements EntityCapabilities { super(Presence.class); } + public Presence(final Type type) { + this(); + this.setType(type); + } + public Availability getAvailability() { final var show = getExtension(Show.class); if (show == null) { @@ -28,6 +34,18 @@ public class Presence extends Stanza implements EntityCapabilities { this.addExtension(new Show()).setContent(availability.toShowString()); } + public void setType(final Type type) { + if (type == null) { + this.removeAttribute("type"); + } else { + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + } + + public Type getType() { + return Type.valueOfOrNull(this.getAttribute("type")); + } + public void setStatus(final String status) { if (Strings.isNullOrEmpty(status)) { return; @@ -40,6 +58,27 @@ public class Presence extends Stanza implements EntityCapabilities { return status == null ? null : status.getContent(); } + public enum Type { + ERROR, + PROBE, + SUBSCRIBE, + SUBSCRIBED, + UNAVAILABLE, + UNSUBSCRIBE, + UNSUBSCRIBED; + + public static Type valueOfOrNull(final String type) { + if (Strings.isNullOrEmpty(type)) { + return null; + } + try { + return valueOf(type.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + } + public enum Availability { CHAT, ONLINE, diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java index 0078bcbae2be3558658908c98a48e71d6636ac42..f2d1c506814b6cc3f160fb0866348bb61513546d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -13,6 +13,11 @@ public class StanzaId extends Extension { super(StanzaId.class); } + public StanzaId(final String id) { + this(); + this.setAttribute("id", id); + } + public Jid getBy() { return this.getAttributeAsJid("by"); } diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 6092b0e8f406a6b979f3b88618efaa0ef114e7c2..e42e400dc9be30a3dfe29d1602252dca19d0a2aa 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -98,7 +98,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { service.getPushManagementService().registerPushTokenOnServer(account); } service.connectMultiModeConversations(account); - service.syncDirtyContacts(account); + connection.getManager(RosterManager.class).syncDirtyContacts(); service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account); } From 6ed9ba5e66b1ee9564ef8bdc5bd534f6661b1b4a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 17 May 2025 14:11:53 +0200 Subject: [PATCH 04/87] add stub managers for pub sub related events --- .../siacs/conversations/entities/Account.java | 31 +- .../conversations/entities/Bookmark.java | 25 +- .../conversations/parser/MessageParser.java | 10 +- .../conversations/parser/PresenceParser.java | 2 + .../services/XmppConnectionService.java | 99 +---- .../conversations/xmpp/IqErrorException.java | 35 ++ .../xmpp/IqErrorResponseException.java | 33 -- .../eu/siacs/conversations/xmpp/Managers.java | 16 + .../xmpp/PreconditionNotMetException.java | 16 + .../xmpp/PubSubErrorException.java | 19 + .../conversations/xmpp/XmppConnection.java | 39 +- .../xmpp/manager/AbstractBookmarkManager.java | 62 ++++ .../xmpp/manager/AvatarManager.java | 15 + .../xmpp/manager/AxolotlManager.java | 15 + .../xmpp/manager/BookmarkManager.java | 95 +++++ .../xmpp/manager/LegacyBookmarkManager.java | 15 + .../xmpp/manager/NickManager.java | 31 ++ .../xmpp/manager/PepManager.java | 53 +++ .../xmpp/manager/PrivateStorageManager.java | 55 +++ .../xmpp/manager/PubSubManager.java | 349 ++++++++++++++++++ .../android/xmpp/processor/BindProcessor.java | 9 +- 21 files changed, 856 insertions(+), 168 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index b1cd4e395137fb0a34bfc6cf612f5dd452a09b84..043b88d2005a3624c21b26dc5c475934389295b0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -20,7 +20,6 @@ import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.http.ServiceOutageStatus; import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; @@ -96,8 +95,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable protected boolean online = false; private String rosterVersion; private String displayName = null; - private AxolotlService axolotlService = null; - private PgpDecryptionService pgpDecryptionService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private final Map bookmarks = new HashMap<>(); @@ -233,11 +230,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public boolean hasPendingPgpIntent(Conversation conversation) { - return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation); + return getPgpDecryptionService().hasPendingIntent(conversation); } public boolean isPgpDecryptionServiceConnected() { - return pgpDecryptionService != null && pgpDecryptionService.isConnected(); + return getPgpDecryptionService().isConnected(); } public boolean setShowErrorNotification(boolean newValue) { @@ -285,11 +282,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable final Jid prev = this.jid != null ? this.jid.asBareJid() : null; final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid())); if (changed) { - final AxolotlService oldAxolotlService = this.axolotlService; + final AxolotlService oldAxolotlService = xmppConnection.getAxolotlService(); + // TODO check that changing JID and recreating the AxolotlService still works if (oldAxolotlService != null) { oldAxolotlService.destroy(); this.jid = next; - this.axolotlService = oldAxolotlService.makeNew(); + xmppConnection.setAxolotlService(oldAxolotlService.makeNew()); } } this.jid = next; @@ -545,18 +543,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public AxolotlService getAxolotlService() { - return axolotlService; - } - - public void initAccountServices(final XmppConnectionService context) { - this.xmppConnection = new XmppConnection(this, context); - this.axolotlService = new AxolotlService(this, context); - this.pgpDecryptionService = new PgpDecryptionService(context); - this.xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + return this.xmppConnection.getAxolotlService(); } public PgpDecryptionService getPgpDecryptionService() { - return this.pgpDecryptionService; + return this.xmppConnection.getPgpDecryptionService(); } public XmppConnection getXmppConnection() { @@ -739,9 +730,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private List getFingerprints() { ArrayList fingerprints = new ArrayList<>(); - if (axolotlService == null) { - return fingerprints; - } + final var axolotlService = getAxolotlService(); fingerprints.add( new XmppUri.Fingerprint( XmppUri.FingerprintType.OMEMO, @@ -811,6 +800,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return false; } + public void setXmppConnection(final XmppConnection connection) { + this.xmppConnection = connection; + } + public enum State { DISABLED(false, false), LOGGED_OUT(false, false), diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 41bd8c978a85bd32a32546f55f88a67d26310ae7..3245da705a2beb0b8f3e1b5a52f86d963ddef8e2 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -12,7 +12,6 @@ import eu.siacs.conversations.xml.Namespace; 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.pubsub.PubSub; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; @@ -41,6 +40,7 @@ public class Bookmark extends Element implements ListItem { public static Map parseFromStorage( final Storage storage, final Account account) { + // TODO refactor to use extensions. get rid of the 'old' handling if (storage == null) { return Collections.emptyMap(); } @@ -61,26 +61,6 @@ public class Bookmark extends Element implements ListItem { return bookmarks; } - public static Map parseFromPubSub(final PubSub pubSub, final Account account) { - if (pubSub == null) { - return Collections.emptyMap(); - } - final var items = pubSub.getItems(); - if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) { - return Collections.emptyMap(); - } - final Map bookmarks = new HashMap<>(); - for (final var item : items.getItemMap(Conference.class).entrySet()) { - final Bookmark bookmark = - Bookmark.parseFromItem(item.getKey(), item.getValue(), account); - if (bookmark == null) { - continue; - } - bookmarks.put(bookmark.jid, bookmark); - } - return bookmarks; - } - public static Bookmark parse(Element element, Account account) { Bookmark bookmark = new Bookmark(account); bookmark.setAttributes(element.getAttributes()); @@ -103,6 +83,9 @@ public class Bookmark extends Element implements ListItem { 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")); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 35725518c712f3f97ef0ed4f206e65f9cba67f74..77da8fd1eac11d6ba3d4aaf44e061dcf4f9475e9 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -37,6 +37,7 @@ 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.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; @@ -321,7 +322,7 @@ public class MessageParser extends AbstractParser } final var storage = items.getFirstItem(Storage.class); final Map bookmarks = Bookmark.parseFromStorage(storage, account); - mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); + // mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event"); @@ -351,7 +352,7 @@ public class MessageParser extends AbstractParser Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); - mXmppConnectionService.processDeletedBookmark(account, id); + // mXmppConnectionService.processDeletedBookmark(account, id); mXmppConnectionService.updateConversationUi(); } } @@ -398,7 +399,7 @@ public class MessageParser extends AbstractParser private void deleteAllBookmarks(final Account account) { final var previous = account.getBookmarkedJids(); account.setBookmarks(Collections.emptyMap()); - mXmppConnectionService.processDeletedBookmarks(account, previous); + // mXmppConnectionService.processDeletedBookmarks(account, previous); } private void setNick(final Account account, final Jid user, final String nick) { @@ -1410,6 +1411,9 @@ public class MessageParser extends AbstractParser // end no body } + if (original.hasExtension(Event.class)) { + getManager(PubSubManager.class).handleEvent(original); + } final var event = original.getExtension(Event.class); if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { final var action = event.getAction(); diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index eb58377ff8e650605eb7587f48be5a4018e46b1d..95f232d2f244ef0aa1305665de484fc58b210543 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -50,6 +50,7 @@ public class PresenceParser extends AbstractParser ? null : mXmppConnectionService.find(account, packet.getFrom().asBareJid()); if (conversation == null) { + Log.d(Config.LOGTAG, "conversation not found for parsing conference presence"); return; } final MucOptions mucOptions = conversation.getMucOptions(); @@ -490,6 +491,7 @@ public class PresenceParser extends AbstractParser @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { + // Log.d(Config.LOGTAG,"<--"+packet); if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 34323ed4c61b7575f45aaf97b39668820c5f8aa8..0a37de478760c9524c0985e914cc18bb6c50a9e6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -121,7 +121,6 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.IqErrorResponseException; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -144,13 +143,12 @@ import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.avatar.Metadata; -import im.conversations.android.xmpp.model.bookmark.Storage; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; -import im.conversations.android.xmpp.model.storage.PrivateStorage; import im.conversations.android.xmpp.model.up.Push; import java.io.File; import java.security.Security; @@ -366,6 +364,7 @@ public class XmppConnectionService extends Service { @Override public void onStatusChanged(final Account account) { + Log.d(Config.LOGTAG, "begin onStatusChanged()"); final var status = account.getStatus(); if (ServiceOutageStatus.isPossibleOutage(status)) { fetchServiceOutageStatus(account); @@ -499,6 +498,7 @@ public class XmppConnectionService extends Service { } } getNotificationService().updateErrorNotification(); + Log.d(Config.LOGTAG, "end onStatusChanged()"); } }; @@ -1843,15 +1843,13 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); + // TODO move status listener into final variable in XmppConnection connection.setOnStatusChangedListener(this.statusListener); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); + // TODO move MessageAck into final Processor into XmppConnection connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } return connection; } @@ -2137,44 +2135,6 @@ public class XmppConnectionService extends Service { }); } - public void fetchBookmarks(final Account account) { - final Iq iqPacket = new Iq(Iq.Type.GET); - iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage()); - final Consumer callback = - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final var privateStorage = response.getExtension(PrivateStorage.class); - if (privateStorage == null) { - return; - } - final var bookmarkStorage = privateStorage.getExtension(Storage.class); - Map bookmarks = - Bookmark.parseFromStorage(bookmarkStorage, account); - processBookmarksInitial(account, bookmarks, false); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not fetch bookmarks"); - } - }; - sendIqPacket(account, iqPacket, callback); - } - - public void fetchBookmarks2(final Account account) { - final Iq retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket( - account, - retrieve, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final var pubsub = response.getExtension(PubSub.class); - final Map bookmarks = - Bookmark.parseFromPubSub(pubsub, account); - processBookmarksInitial(account, bookmarks, true); - } - }); - } - public void fetchMessageDisplayedSynchronization(final Account account) { Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); final var retrieve = mIqGenerator.retrieveMds(); @@ -2252,43 +2212,7 @@ public class XmppConnectionService extends Service { return true; } - public void processBookmarksInitial( - final Account account, final Map bookmarks, final boolean pep) { - final Set previousBookmarks = account.getBookmarkedJids(); - for (final Bookmark bookmark : bookmarks.values()) { - previousBookmarks.remove(bookmark.getJid().asBareJid()); - processModifiedBookmark(bookmark, pep); - } - if (pep) { - processDeletedBookmarks(account, previousBookmarks); - } - account.setBookmarks(bookmarks); - } - - public void processDeletedBookmarks(final Account account, final Collection bookmarks) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": " - + bookmarks.size() - + " bookmarks have been removed"); - for (final Jid bookmark : bookmarks) { - processDeletedBookmark(account, bookmark); - } - } - - public void processDeletedBookmark(final Account account, final Jid jid) { - final Conversation conversation = find(account, jid); - if (conversation == null) { - return; - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); - archiveConversation(conversation, false); - } - - private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { + public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) { final Account account = bookmark.getAccount(); Conversation conversation = find(bookmark); if (conversation != null) { @@ -2407,6 +2331,7 @@ public class XmppConnectionService extends Service { private void pushBookmarksPrivateXml(Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); final Iq iqPacket = new Iq(Iq.Type.SET); + // TODO we have extensions for that Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); for (final Bookmark bookmark : account.getBookmarks()) { @@ -2531,8 +2456,7 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "restoring roster..."); for (final Account account : accounts) { - account.initAccountServices( - this); // roster needs to be loaded at this stage + account.setXmppConnection(createConnection(account)); account.getXmppConnection().getManager(RosterManager.class).restore(); } getBitmapCache().evictAll(); @@ -2989,7 +2913,7 @@ public class XmppConnectionService extends Service { archiveConversation(conversation, true); } - private void archiveConversation( + public void archiveConversation( Conversation conversation, final boolean maySynchronizeWithBookmarks) { getNotificationService().clear(conversation); conversation.setStatus(Conversation.STATUS_ARCHIVED); @@ -3034,7 +2958,7 @@ public class XmppConnectionService extends Service { } public void createAccount(final Account account) { - account.initAccountServices(this); + account.setXmppConnection(createConnection(account)); databaseBackend.createAccount(account); if (CallIntegration.hasSystemFeature(this)) { CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); @@ -4450,8 +4374,7 @@ public class XmppConnectionService extends Service { account.getJid().asBareJid() + ": received timeout waiting for conference" + " configuration fetch"); - } else if (throwable - instanceof IqErrorResponseException errorResponseException) { + } else if (throwable instanceof IqErrorException errorResponseException) { if (callback != null) { callback.onFetchFailed( conversation, diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..7959c8ec4b985bf73cbaaa168143f4fed4c0c99d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Strings; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.error.Error; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class IqErrorException extends Exception { + + private final Iq response; + + public IqErrorException(Iq response) { + super(getErrorText(response)); + this.response = response; + } + + public Error getError() { + return this.response.getError(); + } + + private static String getErrorText(final Iq response) { + final var error = response.getError(); + final var text = error == null ? null : error.getText(); + final var textContent = text == null ? null : text.getContent(); + if (Strings.isNullOrEmpty(textContent)) { + final var condition = error == null ? null : error.getExtension(Condition.class); + return condition == null ? null : condition.getName(); + } + return textContent; + } + + public Iq getResponse() { + return this.response; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java deleted file mode 100644 index fb8d8e730cd9d4e4b37d3321a084e9d4abc25f93..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import im.conversations.android.xmpp.model.stanza.Iq; - -public class IqErrorResponseException extends Exception { - - private final Iq response; - - public IqErrorResponseException(final Iq response) { - super(message(response)); - this.response = response; - } - - public Iq getResponse() { - return this.response; - } - - public static String message(final Iq iq) { - final var error = iq.getError(); - if (error == null) { - return "missing error element in response"; - } - final var text = error.getTextAsString(); - if (text != null) { - return text; - } - final var condition = error.getCondition(); - if (condition != null) { - return condition.getName(); - } - return "no condition attached to error"; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 767234a04b54b7dc078191462ddd16e68faddc03..1c709aa2d2914b5b4dd92be2df706757334c5326 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -4,12 +4,20 @@ import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ImmutableClassToInstanceMap; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.manager.AbstractManager; +import eu.siacs.conversations.xmpp.manager.AvatarManager; +import eu.siacs.conversations.xmpp.manager.AxolotlManager; import eu.siacs.conversations.xmpp.manager.BlockingManager; +import eu.siacs.conversations.xmpp.manager.BookmarkManager; import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.EntityTimeManager; +import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; +import eu.siacs.conversations.xmpp.manager.NickManager; +import eu.siacs.conversations.xmpp.manager.PepManager; import eu.siacs.conversations.xmpp.manager.PingManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; +import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; @@ -22,12 +30,20 @@ public class Managers { public static ClassToInstanceMap get( final XmppConnectionService context, final XmppConnection connection) { return new ImmutableClassToInstanceMap.Builder() + .put(AvatarManager.class, new AvatarManager(context, connection)) + .put(AxolotlManager.class, new AxolotlManager(context, connection)) .put(BlockingManager.class, new BlockingManager(context, connection)) + .put(BookmarkManager.class, new BookmarkManager(context, connection)) .put(CarbonsManager.class, new CarbonsManager(context, connection)) .put(DiscoManager.class, new DiscoManager(context, connection)) .put(EntityTimeManager.class, new EntityTimeManager(context, connection)) + .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection)) + .put(NickManager.class, new NickManager(context, connection)) + .put(PepManager.class, new PepManager(context, connection)) .put(PingManager.class, new PingManager(context, connection)) .put(PresenceManager.class, new PresenceManager(context, connection)) + .put(PrivateStorageManager.class, new PrivateStorageManager(context, connection)) + .put(PubSubManager.class, new PubSubManager(context, connection)) .put(RosterManager.class, new RosterManager(context, connection)) .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection)) .build(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java new file mode 100644 index 0000000000000000000000000000000000000000..c2777667734c76e46af46c9eccedc334957a7c9f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp; + +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class PreconditionNotMetException extends PubSubErrorException { + + public PreconditionNotMetException(final Iq response) { + super(response); + if (this.pubSubError instanceof PubSubError.PreconditionNotMet) { + return; + } + throw new AssertionError( + "This exception should only be constructed for PreconditionNotMet errors"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java new file mode 100644 index 0000000000000000000000000000000000000000..3b6e079214c672913a1aa0c47d7d5e7102159aad --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp; + +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class PubSubErrorException extends IqErrorException { + + protected final PubSubError pubSubError; + + public PubSubErrorException(Iq response) { + super(response); + final var error = response.getError(); + final var pubSubError = error == null ? null : error.getExtension(PubSubError.class); + if (pubSubError == null) { + throw new AssertionError("This exception should only be constructed for PubSubErrors"); + } + this.pubSubError = pubSubError; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index dec02a32a3614c0ed97adfee4e51b5d271a90296..988dc6f890256f2824f25ade832e085647feceb0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -33,6 +33,7 @@ import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; @@ -77,6 +78,7 @@ import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.PingManager; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; import im.conversations.android.xmpp.model.AuthenticationStreamFeature; @@ -200,6 +202,8 @@ public class XmppConnection implements Runnable { private final Consumer presenceListener; private final Consumer unregisteredIqListener; private final Consumer messageListener; + private AxolotlService axolotlService; + private final PgpDecryptionService pgpDecryptionService; private OnStatusChanged statusListener = null; private final Runnable bindListener; private OnMessageAcknowledged acknowledgedListener = null; @@ -226,6 +230,8 @@ public class XmppConnection implements Runnable { this.messageListener = new MessageParser(service, this); this.bindListener = new BindProcessor(service, this); this.managers = Managers.get(service, this); + this.setAxolotlService(new AxolotlService(account, service)); + this.pgpDecryptionService = new PgpDecryptionService(service); } private static void fixResource(final Context context, final Account account) { @@ -277,7 +283,13 @@ public class XmppConnection implements Runnable { } } if (statusListener != null) { - statusListener.onStatusChanged(account); + try { + statusListener.onStatusChanged(account); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "error executing shit", e); + } + } else { + Log.d(Config.LOGTAG, "status changed listener was null"); } } @@ -2520,7 +2532,7 @@ public class XmppConnection implements Runnable { switch (type) { case RESULT -> settable.set(response); case TIMEOUT -> settable.setException(new TimeoutException()); - default -> settable.setException(new IqErrorResponseException(response)); + default -> settable.setException(new IqErrorException(response)); } }); return settable; @@ -2893,6 +2905,29 @@ public class XmppConnection implements Runnable { || from.equals(account); } + public boolean fromAccount(final Stanza stanza) { + final var account = getAccount().getJid(); + final Jid from = stanza.getFrom(); + return from == null || from.asBareJid().equals(account.asBareJid()); + } + + public AxolotlService getAxolotlService() { + return this.axolotlService; + } + + public PgpDecryptionService getPgpDecryptionService() { + return this.pgpDecryptionService; + } + + public void setAxolotlService(AxolotlService axolotlService) { + final var current = this.axolotlService; + if (current != null) { + this.advancedStreamFeaturesLoadedListeners.remove(current); + } + this.axolotlService = axolotlService; + this.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6ecf0edf874d0a07abdeff3feac4551b785e326a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java @@ -0,0 +1,62 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +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.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class AbstractBookmarkManager extends AbstractManager { + + private final XmppConnectionService service; + + protected AbstractBookmarkManager( + final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + this.service = service; + } + + // TODO rename to setBookmarks? + public void processBookmarksInitial(final Map bookmarks, final boolean pep) { + final var account = getAccount(); + // TODO we can internalize this getBookmarkedJid + final Set previousBookmarks = account.getBookmarkedJids(); + for (final Bookmark bookmark : bookmarks.values()) { + previousBookmarks.remove(bookmark.getJid().asBareJid()); + service.processModifiedBookmark(bookmark, pep); + } + if (pep) { + this.processDeletedBookmarks(account, previousBookmarks); + } + account.setBookmarks(bookmarks); + } + + public void processDeletedBookmarks(final Account account, final Collection bookmarks) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + bookmarks.size() + + " bookmarks have been removed"); + for (final Jid bookmark : bookmarks) { + processDeletedBookmark(account, bookmark); + } + } + + public void processDeletedBookmark(final Account account, final Jid jid) { + final Conversation conversation = service.find(account, jid); + if (conversation == null) { + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); + this.service.archiveConversation(conversation, false); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java new file mode 100644 index 0000000000000000000000000000000000000000..977627bc6b7643c62afe0e57fc683dec3d85ccb4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class AvatarManager extends AbstractManager { + + public AvatarManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(Jid from, final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java new file mode 100644 index 0000000000000000000000000000000000000000..889054032f2c982861163a523a797982e089f4ea --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class AxolotlManager extends AbstractManager { + + public AxolotlManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(Jid from, final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4bfb5d080d9685ad2b95735e169c311aaa8a006f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java @@ -0,0 +1,95 @@ +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.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.pubsub.Items; +import java.util.Map; + +public class BookmarkManager extends AbstractBookmarkManager { + + public BookmarkManager(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 bookmarks) { + final var builder = new ImmutableMap.Builder(); + 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); + } + + @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) { + final var retractions = items.getRetractions(); + final var itemMap = items.getItemMap(Conference.class); + if (!retractions.isEmpty()) { + // deleteItems(retractions); + } + if (!itemMap.isEmpty()) { + // updateItems(itemMap); + } + } + + public ListenableFuture publishBookmark(final Jid address, final boolean autoJoin) { + return publishBookmark(address, autoJoin, null); + } + + public ListenableFuture publishBookmark( + final Jid address, final boolean autoJoin, final String nick) { + final var itemId = address.toString(); + final var conference = new Conference(); + conference.setAutoJoin(autoJoin); + if (nick != null) { + conference.addExtension(new Nick()).setContent(nick); + } + return Futures.transform( + getManager(PepManager.class) + .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS), + result -> null, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retractBookmark(final Jid address) { + final var itemId = address.toString(); + return Futures.transform( + getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2), + result -> null, + MoreExecutors.directExecutor()); + } + + public void deleteAllItems() {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..71817f42e829de5c5964ed99ba344081de1fa246 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.xmpp.manager; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class LegacyBookmarkManager extends AbstractBookmarkManager { + + public LegacyBookmarkManager( + final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); + } + + public void handleItems(final Items items) {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java new file mode 100644 index 0000000000000000000000000000000000000000..56f8766adbbe456b768e5148f57a135a6a1eb54b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java @@ -0,0 +1,31 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ListenableFuture; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.nick.Nick; +import im.conversations.android.xmpp.model.pubsub.Items; + +public class NickManager extends AbstractManager { + + public NickManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleItems(final Jid from, Items items) { + final var item = items.getFirstItem(Nick.class); + final var nick = item == null ? null : item.getContent(); + if (from == null || Strings.isNullOrEmpty(nick)) { + return; + } + } + + public ListenableFuture publishNick(final String name) { + final Nick nick = new Nick(); + nick.setContent(name); + return getManager(PepManager.class).publishSingleton(nick, NodeConfiguration.PRESENCE); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba8534588cd6fcff3c0dced7b5bde417a014ff4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java @@ -0,0 +1,53 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import com.google.common.util.concurrent.ListenableFuture; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.Map; + +public class PepManager extends AbstractManager { + + public PepManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public ListenableFuture> fetchItems(final Class clazz) { + return pubSubManager().fetchItems(pepService(), clazz); + } + + public ListenableFuture fetchMostRecentItem( + final String node, final Class clazz) { + return pubSubManager().fetchMostRecentItem(pepService(), node, clazz); + } + + public ListenableFuture publish( + Extension item, final String itemId, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publish(pepService(), item, itemId, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + Extension item, final String node, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publishSingleton(pepService(), item, node, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + final Extension item, final NodeConfiguration nodeConfiguration) { + return pubSubManager().publishSingleton(pepService(), item, nodeConfiguration); + } + + public ListenableFuture retract(final String itemId, final String node) { + return pubSubManager().retract(pepService(), itemId, node); + } + + private PubSubManager pubSubManager() { + return getManager(PubSubManager.class); + } + + private Jid pepService() { + return connection.getAccount().getJid().asBareJid(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java new file mode 100644 index 0000000000000000000000000000000000000000..1a5bd56c62939c39bedfe18871b16ae02f85edbf --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java @@ -0,0 +1,55 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.storage.PrivateStorage; +import java.util.Map; + +public class PrivateStorageManager extends AbstractBookmarkManager { + + public PrivateStorageManager(final XmppConnectionService service, XmppConnection connection) { + super(service, connection); + } + + public void fetchBookmarks() { + final var iq = new Iq(Iq.Type.GET); + final var privateStorage = iq.addExtension(new PrivateStorage()); + privateStorage.addExtension(new Storage()); + final var future = this.connection.sendIqPacket(iq); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Iq result) { + final var privateStorage = result.getExtension(PrivateStorage.class); + if (privateStorage == null) { + return; + } + final var bookmarkStorage = privateStorage.getExtension(Storage.class); + final Map bookmarks = + Bookmark.parseFromStorage(bookmarkStorage, getAccount()); + processBookmarksInitial(bookmarks, false); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not fetch bookmark from private storage", + t); + } + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java new file mode 100644 index 0000000000000000000000000000000000000000..47856e26729ce138814061b80f87f659fbe1329d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -0,0 +1,349 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.util.concurrent.AsyncFunction; +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.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.ExtensionFactory; +import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.PreconditionNotMetException; +import im.conversations.android.xmpp.PubSubErrorException; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.pubsub.PubSub; +import im.conversations.android.xmpp.model.pubsub.Publish; +import im.conversations.android.xmpp.model.pubsub.PublishOptions; +import im.conversations.android.xmpp.model.pubsub.Retract; +import im.conversations.android.xmpp.model.pubsub.error.PubSubError; +import im.conversations.android.xmpp.model.pubsub.event.Delete; +import im.conversations.android.xmpp.model.pubsub.event.Event; +import im.conversations.android.xmpp.model.pubsub.event.Purge; +import im.conversations.android.xmpp.model.pubsub.owner.Configure; +import im.conversations.android.xmpp.model.pubsub.owner.PubSubOwner; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Message; +import java.util.Map; + +public class PubSubManager extends AbstractManager { + + private static final String SINGLETON_ITEM_ID = "current"; + + public PubSubManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public void handleEvent(final Message message) { + final var event = message.getExtension(Event.class); + final var action = event.getAction(); + final var from = message.getFrom(); + + if (from instanceof Jid.Invalid) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": ignoring event from invalid jid"); + return; + } + + if (action instanceof Purge purge) { + // purge is a deletion of all items in a node + handlePurge(message, purge); + } else if (action instanceof Items items) { + // the items wrapper contains, new and updated items as well as retractions which are + // deletions of individual items in a node + handleItems(message, items); + } else if (action instanceof Delete delete) { + // delete is the deletion of the node itself + handleDelete(message, delete); + } + } + + public ListenableFuture> fetchItems( + final Jid address, final Class clazz) { + final var id = ExtensionFactory.id(clazz); + if (id == null) { + return Futures.immediateFailedFuture( + new IllegalArgumentException( + String.format("%s is not a registered extension", clazz.getName()))); + } + return fetchItems(address, id.namespace, clazz); + } + + public ListenableFuture> fetchItems( + final Jid address, final String node, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getItemMap(clazz); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchItem( + final Jid address, final String itemId, final Class clazz) { + final var id = ExtensionFactory.id(clazz); + if (id == null) { + return Futures.immediateFailedFuture( + new IllegalArgumentException( + String.format("%s is not a registered extension", clazz.getName()))); + } + return fetchItem(address, id.namespace, itemId, clazz); + } + + public ListenableFuture fetchItem( + final Jid address, final String node, final String itemId, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + final var item = itemsWrapper.addExtension(new PubSub.Item()); + item.setId(itemId); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getItemOrThrow(itemId, clazz); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchMostRecentItem( + final Jid address, final String node, final Class clazz) { + final Iq request = new Iq(Iq.Type.GET); + request.setTo(address); + final var pubSub = request.addExtension(new PubSub()); + final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper()); + itemsWrapper.setNode(node); + itemsWrapper.setMaxItems(1); + return Futures.transform( + connection.sendIqPacket(request), + response -> { + final var pubSubResponse = response.getExtension(PubSub.class); + if (pubSubResponse == null) { + throw new IllegalStateException(); + } + final var items = pubSubResponse.getItems(); + if (items == null) { + throw new IllegalStateException(); + } + return items.getOnlyItem(clazz); + }, + MoreExecutors.directExecutor()); + } + + private void handleItems(final Message message, final Items items) { + final var from = message.getFrom(); + 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); + return; + } + if (connection.fromAccount(message) && Namespace.BOOKMARKS.equals(node)) { + getManager(LegacyBookmarkManager.class).handleItems(items); + return; + } + if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { + getManager(AvatarManager.class).handleItems(from, items); + return; + } + if (isFromBare && Namespace.NICK.equals(node)) { + getManager(NickManager.class).handleItems(from, items); + return; + } + if (isFromBare && Namespace.AXOLOTL_DEVICE_LIST.equals(node)) { + getManager(AxolotlManager.class).handleItems(from, items); + } + } + + private void handlePurge(final Message message, final Purge purge) { + final var from = message.getFrom(); + final var node = purge.getNode(); + if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { + getManager(BookmarkManager.class).deleteAllItems(); + } + } + + private void handleDelete(final Message message, final Delete delete) {} + + public ListenableFuture publishSingleton( + Jid address, Extension item, final NodeConfiguration nodeConfiguration) { + final var id = ExtensionFactory.id(item.getClass()); + return publish(address, item, SINGLETON_ITEM_ID, id.namespace, nodeConfiguration); + } + + public ListenableFuture publishSingleton( + Jid address, + Extension item, + final String node, + final NodeConfiguration nodeConfiguration) { + return publish(address, item, SINGLETON_ITEM_ID, node, nodeConfiguration); + } + + public ListenableFuture publish( + Jid address, + Extension item, + final String itemId, + final NodeConfiguration nodeConfiguration) { + final var id = ExtensionFactory.id(item.getClass()); + return publish(address, item, itemId, id.namespace, nodeConfiguration); + } + + public ListenableFuture publish( + final Jid address, + final Extension itemPayload, + final String itemId, + final String node, + final NodeConfiguration nodeConfiguration) { + final var future = publishNoRetry(address, itemPayload, itemId, node, nodeConfiguration); + return Futures.catchingAsync( + future, + PreconditionNotMetException.class, + ex -> { + Log.d( + Config.LOGTAG, + "Node " + node + " on " + address + " requires reconfiguration"); + final var reconfigurationFuture = + reconfigureNode(address, node, nodeConfiguration); + return Futures.transformAsync( + reconfigurationFuture, + ignored -> + publishNoRetry( + address, itemPayload, itemId, node, nodeConfiguration), + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture publishNoRetry( + final Jid address, + final Extension itemPayload, + final String itemId, + final String node, + final NodeConfiguration nodeConfiguration) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSub()); + final var publish = pubSub.addExtension(new Publish()); + publish.setNode(node); + final var item = publish.addExtension(new PubSub.Item()); + item.setId(itemId); + item.addExtension(itemPayload); + pubSub.addExtension(PublishOptions.of(nodeConfiguration)); + final ListenableFuture iqFuture = + Futures.transform( + connection.sendIqPacket(iq), + result -> null, + MoreExecutors.directExecutor()); + return Futures.catchingAsync( + iqFuture, + IqErrorException.class, + new PubSubExceptionTransformer<>(), + MoreExecutors.directExecutor()); + } + + private ListenableFuture reconfigureNode( + final Jid address, final String node, final NodeConfiguration nodeConfiguration) { + final Iq iq = new Iq(Iq.Type.GET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSubOwner()); + final var configure = pubSub.addExtension(new Configure()); + configure.setNode(node); + return Futures.transformAsync( + connection.sendIqPacket(iq), + result -> { + final var pubSubOwnerResult = result.getExtension(PubSubOwner.class); + final Configure configureResult = + pubSubOwnerResult == null + ? null + : pubSubOwnerResult.getExtension(Configure.class); + if (configureResult == null) { + throw new IllegalStateException( + "No configuration found in configuration request result"); + } + final var data = configureResult.getData(); + return setNodeConfiguration(address, node, data.submit(nodeConfiguration)); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture setNodeConfiguration( + final Jid address, final String node, final Data data) { + final Iq iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSubOwner()); + final var configure = pubSub.addExtension(new Configure()); + configure.setNode(node); + configure.addExtension(data); + return Futures.transform( + connection.sendIqPacket(iq), + result -> { + Log.d(Config.LOGTAG, "Modified node configuration " + node + " on " + address); + return null; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retract(final Jid address, final String itemId, final String node) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSub()); + final var retract = pubSub.addExtension(new Retract()); + retract.setNode(node); + retract.setNotify(true); + final var item = retract.addExtension(new PubSub.Item()); + item.setId(itemId); + return connection.sendIqPacket(iq); + } + + private static class PubSubExceptionTransformer + implements AsyncFunction { + + @Override + @NonNull + public ListenableFuture apply(@NonNull IqErrorException ex) { + final var error = ex.getError(); + if (error == null) { + return Futures.immediateFailedFuture(ex); + } + final PubSubError pubSubError = error.getExtension(PubSubError.class); + if (pubSubError instanceof PubSubError.PreconditionNotMet) { + return Futures.immediateFailedFuture( + new PreconditionNotMetException(ex.getResponse())); + } else if (pubSubError != null) { + return Futures.immediateFailedFuture(new PubSubErrorException(ex.getResponse())); + } else { + return Futures.immediateFailedFuture(ex); + } + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index e42e400dc9be30a3dfe29d1602252dca19d0a2aa..dc8e1dd5ff95dd545011292b53c232c84744bb55 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -7,6 +7,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.generator.IqGenerator; 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.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; @@ -21,6 +23,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { @Override public void run() { + Log.d(Config.LOGTAG, "begin onBind()"); final var account = connection.getAccount(); final var features = connection.getFeatures(); service.cancelAvatarFetches(account); @@ -63,9 +66,10 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { getManager(RosterManager.class).request(); if (features.bookmarks2()) { - service.fetchBookmarks2(account); + connection.getManager(BookmarkManager.class).fetch(); + // log that we use bookmarks 1 and wait for +notify } else if (!features.bookmarksConversion()) { - service.fetchBookmarks(account); + connection.getManager(PrivateStorageManager.class).fetchBookmarks(); } if (features.mds()) { @@ -101,5 +105,6 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { connection.getManager(RosterManager.class).syncDirtyContacts(); service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account); + Log.d(Config.LOGTAG, "end onBind()"); } } From 24d3f9e403aad3865121d6edc151692c9b835334 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 19 May 2025 09:40:32 +0200 Subject: [PATCH 05/87] move AccountStateProcessor into its own class --- .../siacs/conversations/entities/Account.java | 6 +- .../services/MessageArchiveService.java | 2 +- .../services/NotificationService.java | 2 +- .../services/XmppConnectionService.java | 180 ++---------------- .../conversations/xmpp/XmppConnection.java | 29 ++- .../xmpp/processor/AccountStateProcessor.java | 152 +++++++++++++++ 6 files changed, 185 insertions(+), 186 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 043b88d2005a3624c21b26dc5c475934389295b0..f3e235573d4971934999f7a6529b88ddd3f9ff6b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -629,11 +629,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public Roster getRoster() { - if (xmppConnection != null) { - return xmppConnection.getManager(RosterManager.class); - } - // TODO either return stub or always put XmppConnection into Account - return null; + return xmppConnection.getManager(RosterManager.class); } public Collection getBookmarks() { diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 502b51ef89b55bedd2386954cda5f4530f908ad4..09af262147a0756bb1043aa763de9119e0d06151 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -221,7 +221,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - void executePendingQueries(final Account account) { + public void executePendingQueries(final Account account) { final List pending = new ArrayList<>(); synchronized (this.pendingQueries) { for (Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) { diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 277db9037d6bbf4e1fff4241247a957aad08fed8..2db57aaa69f9d471438b2438cde3a10d520f2aa8 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1866,7 +1866,7 @@ public class NotificationService { } } - void updateErrorNotification() { + public void updateErrorNotification() { if (Config.SUPPRESS_ERROR_NOTIFICATION) { cancel(ERROR_NOTIFICATION_ID); return; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0a37de478760c9524c0985e914cc18bb6c50a9e6..77e729f251c84f20e8f5cc6d8dbe0fb3988f6edd 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.services; import static eu.siacs.conversations.utils.Compatibility.s; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.Manifest; import android.annotation.SuppressLint; @@ -125,7 +124,6 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; -import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; @@ -219,7 +217,7 @@ public class XmppConnectionService extends Service { private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); - private final ScheduledExecutorService internalPingExecutor = + public final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor(); private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression"); @@ -234,7 +232,7 @@ public class XmppConnectionService extends Service { private final IqGenerator mIqGenerator = new IqGenerator(this); private final Set mInProgressAvatarFetches = new HashSet<>(); private final Set mOmittedPepAvatarFetches = new HashSet<>(); - private final HashSet mLowPingTimeoutMode = new HashSet<>(); + public final HashSet mLowPingTimeoutMode = new HashSet<>(); private final Consumer mDefaultIqHandler = (packet) -> { if (packet.getType() != Iq.Type.RESULT) { @@ -359,148 +357,6 @@ public class XmppConnectionService extends Service { public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private final OnStatusChanged statusListener = - new OnStatusChanged() { - - @Override - public void onStatusChanged(final Account account) { - Log.d(Config.LOGTAG, "begin onStatusChanged()"); - final var status = account.getStatus(); - if (ServiceOutageStatus.isPossibleOutage(status)) { - fetchServiceOutageStatus(account); - } - XmppConnection connection = account.getXmppConnection(); - updateAccountUi(); - - if (account.getStatus() == Account.State.ONLINE - || account.getStatus().isError()) { - mQuickConversationsService.signalAccountStateChange(); - } - - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": leaving low ping timeout mode"); - } - } - if (account.setShowErrorNotification(true)) { - databaseBackend.updateAccount(account); - } - mMessageArchiveService.executePendingQueries(account); - if (connection != null && connection.getFeatures().csi()) { - if (checkListeners()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + " sending csi//inactive"); - connection.sendInactive(); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + " sending csi//active"); - connection.sendActive(); - } - } - List conversations = 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) { - sendUnsentMessages(conversation); - } - } - final List pendingLeaves; - synchronized (account.pendingConferenceLeaves) { - pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); - account.pendingConferenceLeaves.clear(); - } - for (Conversation conversation : pendingLeaves) { - leaveMuc(conversation); - } - final List pendingJoins; - synchronized (account.pendingConferenceJoins) { - pendingJoins = new ArrayList<>(account.pendingConferenceJoins); - account.pendingConferenceJoins.clear(); - } - for (Conversation conversation : pendingJoins) { - joinMuc(conversation); - } - scheduleWakeUpCall( - Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE - || account.getStatus() == Account.State.DISABLED - || account.getStatus() == Account.State.LOGGED_OUT) { - resetSendingToWaiting(account); - if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": went into offline state during low ping mode." - + " reconnecting now"); - reconnectAccount(account, true, false); - } else { - final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; - scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); - } - } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { - databaseBackend.updateAccount(account); - reconnectAccount(account, true, false); - } else if (account.getStatus() != Account.State.CONNECTING - && account.getStatus() != Account.State.NO_INTERNET) { - resetSendingToWaiting(account); - if (connection != null && account.getStatus().isAttemptReconnect()) { - final boolean aggressive = - account.getStatus() == Account.State.SEE_OTHER_HOST - || hasJingleRtpConnection(account); - final int next = connection.getTimeToNextAttempt(aggressive); - final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); - if (next <= 0) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error connecting account. reconnecting now." - + " lowPingTimeout=" - + lowPingTimeoutMode); - reconnectAccount(account, true, false); - } else { - final int attempt = connection.getAttempt() + 1; - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error connecting account. try again in " - + next - + "s for the " - + attempt - + " time. lowPingTimeout=" - + lowPingTimeoutMode - + ", aggressive=" - + aggressive); - scheduleWakeUpCall(next, account.getUuid().hashCode()); - if (aggressive) { - internalPingExecutor.schedule( - XmppConnectionService.this - ::manageAccountConnectionStatesInternal, - (next * 1000L) + 50, - TimeUnit.MILLISECONDS); - } - } - } - } - getNotificationService().updateErrorNotification(); - Log.d(Config.LOGTAG, "end onStatusChanged()"); - } - }; private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; @@ -515,7 +371,7 @@ public class XmppConnectionService extends Service { return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum; } - private boolean isInLowPingTimeoutMode(Account account) { + public boolean isInLowPingTimeoutMode(Account account) { synchronized (mLowPingTimeoutMode) { return mLowPingTimeoutMode.contains(account.getJid().asBareJid()); } @@ -987,7 +843,7 @@ public class XmppConnectionService extends Service { updateConversationUi(); } - private void manageAccountConnectionStatesInternal() { + public void manageAccountConnectionStatesInternal() { manageAccountConnectionStates(ACTION_INTERNAL_PING, null); } @@ -1050,17 +906,16 @@ public class XmppConnectionService extends Service { final boolean isUiAction, final boolean isAccountPushed, final HashSet pingCandidates) { + final var connection = account.getXmppConnection(); if (!account.getStatus().isAttemptReconnect()) { return false; } final var requestCode = account.getUuid().hashCode(); if (!hasInternetConnection()) { - account.setStatus(Account.State.NO_INTERNET); - statusListener.onStatusChanged(account); + connection.setStatusAndTriggerProcessor(Account.State.NO_INTERNET); } else { if (account.getStatus() == Account.State.NO_INTERNET) { - account.setStatus(Account.State.OFFLINE); - statusListener.onStatusChanged(account); + connection.setStatusAndTriggerProcessor(Account.State.OFFLINE); } if (account.getStatus() == Account.State.ONLINE) { synchronized (mLowPingTimeoutMode) { @@ -1111,7 +966,6 @@ public class XmppConnectionService extends Service { } else if (account.getStatus() == Account.State.OFFLINE) { reconnectAccount(account, true, interactive); } else if (account.getStatus() == Account.State.CONNECTING) { - final var connection = account.getXmppConnection(); final var connectionDuration = connection.getConnectionDuration(); final var discoDuration = connection.getDiscoDuration(); final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration; @@ -1128,7 +982,7 @@ public class XmppConnectionService extends Service { final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST || hasJingleRtpConnection(account); - if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) { + if (connection.getTimeToNextAttempt(aggressive) <= 0) { reconnectAccount(account, true, interactive); } } @@ -1146,7 +1000,7 @@ public class XmppConnectionService extends Service { } } - private void fetchServiceOutageStatus(final Account account) { + public void fetchServiceOutageStatus(final Account account) { final var sosUrl = account.getKey(Account.KEY_SOS_URL); if (Strings.isNullOrEmpty(sosUrl)) { return; @@ -1436,6 +1290,9 @@ public class XmppConnectionService extends Service { this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); Log.d(Config.LOGTAG, "restoring accounts..."); this.accounts = databaseBackend.getAccounts(); + for (final var account : this.accounts) { + account.setXmppConnection(createConnection(account)); + } final SharedPreferences.Editor editor = getPreferences().edit(); final boolean hasEnabledAccounts = hasEnabledAccounts(); editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); @@ -1843,8 +1700,6 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); - // TODO move status listener into final variable in XmppConnection - connection.setOnStatusChangedListener(this.statusListener); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); // TODO move MessageAck into final Processor into XmppConnection connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); @@ -2077,7 +1932,7 @@ public class XmppConnectionService extends Service { } } - private void sendUnsentMessages(final Conversation conversation) { + public void sendUnsentMessages(final Conversation conversation) { conversation.findWaitingMessages(message -> resendMessage(message, true)); } @@ -2456,7 +2311,6 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "restoring roster..."); for (final Account account : accounts) { - account.setXmppConnection(createConnection(account)); account.getXmppConnection().getManager(RosterManager.class).restore(); } getBitmapCache().evictAll(); @@ -3100,7 +2954,8 @@ public class XmppConnectionService extends Service { public boolean updateAccount(final Account account) { if (databaseBackend.updateAccount(account)) { account.setShowErrorNotification(true); - this.statusListener.onStatusChanged(account); + // TODO what was the purpose of that? will likely be triggered by reconnect anyway? + // this.statusListener.onStatusChanged(account); databaseBackend.updateAccount(account); reconnectAccountInBackground(account); updateAccountUi(); @@ -5207,7 +5062,7 @@ public class XmppConnectionService extends Service { mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); } - private void reconnectAccount( + public void reconnectAccount( final Account account, final boolean force, final boolean interactive) { synchronized (account) { final XmppConnection connection = account.getXmppConnection(); @@ -5231,6 +5086,7 @@ public class XmppConnectionService extends Service { axolotlService.resetBrokenness(); } if (!hasInternet) { + // TODO should this go via XmppConnection.setStatusAndTriggerProcessor()? account.setStatus(Account.State.NO_INTERNET); } } @@ -5942,7 +5798,7 @@ public class XmppConnectionService extends Service { return this.mJingleConnectionManager; } - private boolean hasJingleRtpConnection(final Account account) { + public boolean hasJingleRtpConnection(final Account account) { return this.mJingleConnectionManager.hasJingleRtpConnection(account); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 988dc6f890256f2824f25ade832e085647feceb0..2bd84684515889eb0c69cfc3ed1e2f10dd348284 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -117,6 +117,7 @@ import im.conversations.android.xmpp.model.stanza.Stanza; import im.conversations.android.xmpp.model.streams.StreamError; import im.conversations.android.xmpp.model.tls.Proceed; import im.conversations.android.xmpp.model.tls.StartTls; +import im.conversations.android.xmpp.processor.AccountStateProcessor; import im.conversations.android.xmpp.processor.BindProcessor; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -202,10 +203,10 @@ public class XmppConnection implements Runnable { private final Consumer presenceListener; private final Consumer unregisteredIqListener; private final Consumer messageListener; + private final Consumer accountStateProcessor; private AxolotlService axolotlService; private final PgpDecryptionService pgpDecryptionService; - private OnStatusChanged statusListener = null; - private final Runnable bindListener; + private final Runnable bindProcessor; private OnMessageAcknowledged acknowledgedListener = null; private final PendingItem pendingResumeId = new PendingItem<>(); private LoginInfo loginInfo; @@ -228,7 +229,8 @@ public class XmppConnection implements Runnable { // TODO requires roster and blocking not to be handled by this this.unregisteredIqListener = new IqParser(service, this); this.messageListener = new MessageParser(service, this); - this.bindListener = new BindProcessor(service, this); + this.bindProcessor = new BindProcessor(service, this); + this.accountStateProcessor = new AccountStateProcessor(service, this); this.managers = Managers.get(service, this); this.setAxolotlService(new AxolotlService(account, service)); this.pgpDecryptionService = new PgpDecryptionService(service); @@ -282,15 +284,7 @@ public class XmppConnection implements Runnable { return; } } - if (statusListener != null) { - try { - statusListener.onStatusChanged(account); - } catch (final Exception e) { - Log.d(Config.LOGTAG, "error executing shit", e); - } - } else { - Log.d(Config.LOGTAG, "status changed listener was null"); - } + this.accountStateProcessor.accept(nextStatus); } public Jid getJidForCommand(final String node) { @@ -2366,7 +2360,7 @@ public class XmppConnection implements Runnable { private void finalizeBind() { this.offlineMessagesRetrieved = false; - this.bindListener.run(); + this.bindProcessor.run(); this.changeStatusToOnline(); } @@ -2660,10 +2654,6 @@ public class XmppConnection implements Runnable { this.jingleListener = listener; } - public void setOnStatusChangedListener(final OnStatusChanged listener) { - this.statusListener = listener; - } - public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { this.acknowledgedListener = listener; } @@ -2928,6 +2918,11 @@ public class XmppConnection implements Runnable { this.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); } + public void setStatusAndTriggerProcessor(final Account.State state) { + this.account.setStatus(state); + this.accountStateProcessor.accept(state); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { diff --git a/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..8c3cfb6ba36732bba95ffbd049448b89dda90e45 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java @@ -0,0 +1,152 @@ +package im.conversations.android.xmpp.processor; + +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 java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class AccountStateProcessor extends XmppConnection.Delegate + implements Consumer { + + private final XmppConnectionService service; + + public AccountStateProcessor(final XmppConnectionService service, XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; + } + + @Override + public void accept(final Account.State status) { + final var account = getAccount(); + if (ServiceOutageStatus.isPossibleOutage(status)) { + this.service.fetchServiceOutageStatus(account); + } + this.service.updateAccountUi(); + + if (account.getStatus() == Account.State.ONLINE || account.getStatus().isError()) { + this.service.getQuickConversationsService().signalAccountStateChange(); + } + + if (account.getStatus() == Account.State.ONLINE) { + synchronized (this.service.mLowPingTimeoutMode) { + if (this.service.mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": leaving low ping timeout mode"); + } + } + if (account.setShowErrorNotification(true)) { + this.service.databaseBackend.updateAccount(account); + } + this.service.getMessageArchiveService().executePendingQueries(account); + if (connection != null && connection.getFeatures().csi()) { + if (this.service.checkListeners()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//active"); + connection.sendActive(); + } + } + List 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) { + this.service.sendUnsentMessages(conversation); + } + } + final List pendingLeaves; + synchronized (account.pendingConferenceLeaves) { + pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); + account.pendingConferenceLeaves.clear(); + } + for (Conversation conversation : pendingLeaves) { + this.service.leaveMuc(conversation); + } + final List 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 + || account.getStatus() == Account.State.DISABLED + || account.getStatus() == Account.State.LOGGED_OUT) { + this.service.resetSendingToWaiting(account); + if (account.isConnectionEnabled() && this.service.isInLowPingTimeoutMode(account)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": went into offline state during low ping mode." + + " reconnecting now"); + this.service.reconnectAccount(account, true, false); + } else { + final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; + this.service.scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); + } + } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { + this.service.databaseBackend.updateAccount(account); + this.service.reconnectAccount(account, true, false); + } else if (account.getStatus() != Account.State.CONNECTING + && account.getStatus() != Account.State.NO_INTERNET) { + this.service.resetSendingToWaiting(account); + if (connection != null && account.getStatus().isAttemptReconnect()) { + final boolean aggressive = + account.getStatus() == Account.State.SEE_OTHER_HOST + || this.service.hasJingleRtpConnection(account); + final int next = connection.getTimeToNextAttempt(aggressive); + final boolean lowPingTimeoutMode = this.service.isInLowPingTimeoutMode(account); + if (next <= 0) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error connecting account. reconnecting now." + + " lowPingTimeout=" + + lowPingTimeoutMode); + this.service.reconnectAccount(account, true, false); + } else { + final int attempt = connection.getAttempt() + 1; + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error connecting account. try again in " + + next + + "s for the " + + attempt + + " time. lowPingTimeout=" + + lowPingTimeoutMode + + ", aggressive=" + + aggressive); + this.service.scheduleWakeUpCall(next, account.getUuid().hashCode()); + if (aggressive) { + this.service.internalPingExecutor.schedule( + service::manageAccountConnectionStatesInternal, + (next * 1000L) + 50, + TimeUnit.MILLISECONDS); + } + } + } + } + this.service.getNotificationService().updateErrorNotification(); + } +} From c183b8e5995c1fff6b4c3a52a3cfb607bb622054 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 22 May 2025 15:11:51 +0200 Subject: [PATCH 06/87] move message acking into its own processor class --- .../services/XmppConnectionService.java | 50 +---------------- .../conversations/xmpp/XmppConnection.java | 14 +++-- .../MessageAcknowledgedProcessor.java | 53 +++++++++++++++++++ 3 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgedProcessor.java diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 77e729f251c84f20e8f5cc6d8dbe0fb3988f6edd..1fc8667cf74efd068e28816858807a2cdf29ba87 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -123,14 +123,12 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import eu.siacs.conversations.xmpp.OnMessageAcknowledged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; @@ -291,43 +289,6 @@ public class XmppConnectionService extends Service { markFileDeleted(file); } }; - private final OnMessageAcknowledged mOnMessageAcknowledgedListener = - new OnMessageAcknowledged() { - - @Override - public boolean onMessageAcknowledged( - final Account account, final Jid to, final String id) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { - final String sessionId = - id.substring( - JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX - .length()); - mJingleConnectionManager.updateProposedSessionDiscovered( - account, - to, - sessionId, - JingleConnectionManager.DeviceDiscoveryState - .SEARCHING_ACKNOWLEDGED); - } - - final Jid bare = to.asBareJid(); - - for (final Conversation conversation : getConversations()) { - if (conversation.getAccount() == account - && conversation.getJid().asBareJid().equals(bare)) { - final Message message = conversation.findUnsentMessageWithUuid(id); - if (message != null) { - message.setStatus(Message.STATUS_SEND); - message.setErrorMessage(null); - databaseBackend.updateMessage(message, false); - return true; - } - } - } - return false; - } - }; - private boolean destroyed = false; private int unreadCount = -1; @@ -1642,13 +1603,8 @@ public class XmppConnectionService extends Service { ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : PendingIntent.FLAG_UPDATE_CURRENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setAndAllowWhileIdle( - AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); - } else { - alarmManager.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); - } + alarmManager.setAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); } catch (RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e); } @@ -1701,8 +1657,6 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); - // TODO move MessageAck into final Processor into XmppConnection - connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); return connection; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 2bd84684515889eb0c69cfc3ed1e2f10dd348284..b5db98f80ab0f6c95582090b3554abe93d830545 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -119,6 +119,7 @@ import im.conversations.android.xmpp.model.tls.Proceed; import im.conversations.android.xmpp.model.tls.StartTls; import im.conversations.android.xmpp.processor.AccountStateProcessor; import im.conversations.android.xmpp.processor.BindProcessor; +import im.conversations.android.xmpp.processor.MessageAcknowledgedProcessor; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -150,6 +151,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.regex.Matcher; import javax.net.ssl.KeyManager; @@ -204,10 +206,10 @@ public class XmppConnection implements Runnable { private final Consumer unregisteredIqListener; private final Consumer messageListener; private final Consumer accountStateProcessor; + private final BiFunction messageAcknowledgedProcessor; private AxolotlService axolotlService; private final PgpDecryptionService pgpDecryptionService; private final Runnable bindProcessor; - private OnMessageAcknowledged acknowledgedListener = null; private final PendingItem pendingResumeId = new PendingItem<>(); private LoginInfo loginInfo; private HashedToken.Mechanism hashTokenRequest; @@ -231,6 +233,7 @@ public class XmppConnection implements Runnable { this.messageListener = new MessageParser(service, this); this.bindProcessor = new BindProcessor(service, this); this.accountStateProcessor = new AccountStateProcessor(service, this); + this.messageAcknowledgedProcessor = new MessageAcknowledgedProcessor(service, this); this.managers = Managers.get(service, this); this.setAxolotlService(new AxolotlService(account, service)); this.pgpDecryptionService = new PgpDecryptionService(service); @@ -1249,12 +1252,11 @@ public class XmppConnection implements Runnable { } final Stanza stanza = mStanzaQueue.valueAt(i); if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet - && acknowledgedListener != null) { + && messageAcknowledgedProcessor != null) { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { - acknowledgedMessages |= - acknowledgedListener.onMessageAcknowledged(account, to, id); + acknowledgedMessages |= messageAcknowledgedProcessor.apply(to, id); } } mStanzaQueue.removeAt(i); @@ -2654,10 +2656,6 @@ public class XmppConnection implements Runnable { this.jingleListener = listener; } - public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { - this.acknowledgedListener = listener; - } - public void addOnAdvancedStreamFeaturesAvailableListener( final OnAdvancedStreamFeaturesLoaded listener) { this.advancedStreamFeaturesLoadedListeners.add(listener); diff --git a/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgedProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgedProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..5be91b6b44b99dc22948de70ff963d251ff2cdc3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgedProcessor.java @@ -0,0 +1,53 @@ +package im.conversations.android.xmpp.processor; + +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import java.util.function.BiFunction; + +public class MessageAcknowledgedProcessor extends XmppConnection.Delegate + implements BiFunction { + + private final XmppConnectionService service; + + public MessageAcknowledgedProcessor( + final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; + } + + @Override + public Boolean apply(final Jid to, final String id) { + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { + final String sessionId = + id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); + this.service + .getJingleConnectionManager() + .updateProposedSessionDiscovered( + getAccount(), + to, + sessionId, + JingleConnectionManager.DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED); + } + + final Jid bare = to.asBareJid(); + + for (final Conversation conversation : service.getConversations()) { + if (conversation.getAccount() == getAccount() + && conversation.getJid().asBareJid().equals(bare)) { + final Message message = conversation.findUnsentMessageWithUuid(id); + if (message != null) { + message.setStatus(Message.STATUS_SEND); + message.setErrorMessage(null); + getDatabase().updateMessage(message, false); + return true; + } + } + } + return false; + } +} From b81c29bcf346934f093db4ad76ddaa4b84b615b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 22 May 2025 18:18:10 +0200 Subject: [PATCH 07/87] dispatch PEP events via manager --- .../conversations/parser/MessageParser.java | 195 ------------------ .../services/XmppConnectionService.java | 46 ----- .../eu/siacs/conversations/xmpp/Managers.java | 4 + .../xmpp/manager/AbstractBookmarkManager.java | 19 +- .../xmpp/manager/AvatarManager.java | 57 ++++- .../xmpp/manager/AxolotlManager.java | 27 ++- .../xmpp/manager/BookmarkManager.java | 54 ++++- .../xmpp/manager/LegacyBookmarkManager.java | 30 ++- ...essageDisplayedSynchronizationManager.java | 70 +++++++ .../xmpp/manager/NickManager.java | 37 +++- .../xmpp/manager/PubSubManager.java | 24 ++- .../android/xmpp/processor/BindProcessor.java | 3 +- 12 files changed, 294 insertions(+), 272 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 77da8fd1eac11d6ba3d4aaf44e061dcf4f9475e9..35c1f4b3b6d604808f5be8b98497a0e1b31dc63a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; -import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.AppSettings; @@ -14,7 +13,6 @@ import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException; import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; @@ -26,7 +24,6 @@ import eu.siacs.conversations.entities.ReceiptRequest; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; @@ -39,25 +36,16 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; -import im.conversations.android.xmpp.model.avatar.Metadata; -import im.conversations.android.xmpp.model.axolotl.DeviceList; import im.conversations.android.xmpp.model.axolotl.Encrypted; -import im.conversations.android.xmpp.model.bookmark.Storage; -import im.conversations.android.xmpp.model.bookmark2.Conference; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; 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.nick.Nick; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.oob.OutOfBandData; -import im.conversations.android.xmpp.model.pubsub.Items; -import im.conversations.android.xmpp.model.pubsub.event.Delete; import im.conversations.android.xmpp.model.pubsub.event.Event; -import im.conversations.android.xmpp.model.pubsub.event.Purge; import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.receipts.Request; import im.conversations.android.xmpp.model.unique.StanzaId; @@ -65,10 +53,8 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; @@ -257,169 +243,6 @@ public class MessageParser extends AbstractParser return null; } - private void parseEvent(final Items items, final Jid from, final Account account) { - final String node = items.getNode(); - if ("urn:xmpp:avatar:metadata".equals(node)) { - // TODO support retract - final var entry = items.getFirstItemWithId(Metadata.class); - final var avatar = - entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); - if (avatar != null) { - avatar.owner = from.asBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (account.getJid().asBareJid().equals(from)) { - if (account.setAvatar(avatar.getFilename())) { - mXmppConnectionService.databaseBackend.updateAccount(account); - mXmppConnectionService.notifyAccountAvatarHasChanged(account); - } - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } else { - final Contact contact = account.getRoster().getContact(from); - if (contact.setAvatar(avatar)) { - connection.getManager(RosterManager.class).writeToDatabaseAsync(); - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(account, avatar); - } - } - } else if (Namespace.NICK.equals(node)) { - final var nickItem = items.getFirstItem(Nick.class); - final String nick = nickItem == null ? null : nickItem.getContent(); - if (nick != null) { - setNick(account, from, nick); - } - } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - final var deviceList = items.getFirstItem(DeviceList.class); - if (deviceList != null) { - final Set deviceIds = deviceList.getDeviceIds(); - Log.d( - Config.LOGTAG, - AxolotlService.getLogprefix(account) - + "Received PEP device list " - + deviceIds - + " update from " - + from - + ", processing... "); - final AxolotlService axolotlService = account.getAxolotlService(); - axolotlService.registerDevices(from, new HashSet<>(deviceIds)); - } - - } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { - final var connection = account.getXmppConnection(); - if (connection.getFeatures().bookmarksConversion()) { - if (connection.getFeatures().bookmarks2()) { - Log.w( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received storage:bookmark notification even though we" - + " opted into bookmarks:1"); - } - final var storage = items.getFirstItem(Storage.class); - final Map bookmarks = Bookmark.parseFromStorage(storage, account); - // mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": processing bookmark PEP event"); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": ignoring bookmark PEP event because bookmark conversion was" - + " not detected"); - } - } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - final var retractions = items.getRetractions(); - for (final var item : items.getItemMap(Conference.class).entrySet()) { - final Bookmark bookmark = - Bookmark.parseFromItem(item.getKey(), item.getValue(), account); - if (bookmark == null) { - continue; - } - account.putBookmark(bookmark); - mXmppConnectionService.processModifiedBookmark(bookmark); - mXmppConnectionService.updateConversationUi(); - } - 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); - // mXmppConnectionService.processDeletedBookmark(account, id); - mXmppConnectionService.updateConversationUi(); - } - } - } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION - && Namespace.MDS_DISPLAYED.equals(node) - && account.getJid().asBareJid().equals(from)) { - for (final var item : - items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class) - .entrySet()) { - mXmppConnectionService.processMdsItem(account, item); - } - } - } - - private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) { - final String node = delete.getNode(); - if (Namespace.NICK.equals(node)) { - setNick(account, from, null); - } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); - deleteAllBookmarks(account); - } else if (Namespace.AVATAR_METADATA.equals(node)) { - final boolean isAccount = account.getJid().asBareJid().equals(from); - if (isAccount) { - account.setAvatar(null); - mXmppConnectionService.databaseBackend.updateAccount(account); - mXmppConnectionService.getAvatarService().clear(account); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": deleted avatar metadata node"); - } - } - } - - private void parsePurgeEvent( - @NonNull final Purge purge, final Jid from, final Account account) { - final String node = purge.getNode(); - if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks"); - deleteAllBookmarks(account); - } - } - - private void deleteAllBookmarks(final Account account) { - final var previous = account.getBookmarkedJids(); - account.setBookmarks(Collections.emptyMap()); - // mXmppConnectionService.processDeletedBookmarks(account, previous); - } - - private void setNick(final Account account, final Jid user, final String nick) { - if (user.asBareJid().equals(account.getJid().asBareJid())) { - account.setDisplayName(nick); - if (QuickConversationsService.isQuicksy()) { - mXmppConnectionService.getAvatarService().clear(account); - } - mXmppConnectionService.checkMucRequiresRename(); - } else { - Contact contact = account.getRoster().getContact(user); - if (contact.setPresenceName(nick)) { - connection.getManager(RosterManager.class).writeToDatabaseAsync(); - mXmppConnectionService.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } - private boolean handleErrorMessage( final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) { @@ -1414,24 +1237,6 @@ public class MessageParser extends AbstractParser if (original.hasExtension(Event.class)) { getManager(PubSubManager.class).handleEvent(original); } - final var event = original.getExtension(Event.class); - if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { - final var action = event.getAction(); - final var node = action == null ? null : action.getNode(); - if (node == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": no node found in PubSub event from " - + original.getFrom()); - } else if (action instanceof Items items) { - parseEvent(items, original.getFrom(), account); - } else if (action instanceof Purge purge) { - parsePurgeEvent(purge, original.getFrom(), account); - } else if (action instanceof Delete delete) { - parseDeleteEvent(delete, from, account); - } - } final String nick = packet.findChildContent("nick", Namespace.NICK); if (nick != null && Jid.Invalid.hasValidFrom(original)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1fc8667cf74efd068e28816858807a2cdf29ba87..c7f9baacdbf0bc51a8040286c99a57c234b9f09d 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -142,7 +142,6 @@ import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.disco.info.InfoQuery; -import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.up.Push; @@ -1944,51 +1943,6 @@ public class XmppConnectionService extends Service { }); } - public void fetchMessageDisplayedSynchronization(final Account account) { - Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); - final var retrieve = mIqGenerator.retrieveMds(); - sendIqPacket( - account, - retrieve, - (response) -> { - if (response.getType() != Iq.Type.RESULT) { - return; - } - final var pubsub = response.getExtension(PubSub.class); - if (pubsub == null) { - return; - } - final var items = pubsub.getItems(); - if (items == null) { - return; - } - if (Namespace.MDS_DISPLAYED.equals(items.getNode())) { - for (final var item : - items.getItemMap( - im.conversations.android.xmpp.model.mds.Displayed - .class) - .entrySet()) { - processMdsItem(account, item); - } - } - }); - } - - public void processMdsItem(final Account account, final Map.Entry item) { - final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey())); - if (jid == null) { - return; - } - final var displayed = item.getValue(); - final var stanzaId = displayed.getStanzaId(); - final String id = stanzaId == null ? null : stanzaId.getId(); - final Conversation conversation = find(account, jid); - if (id != null && conversation != null) { - conversation.setDisplayState(id); - markReadUpToStanzaId(conversation, id); - } - } - public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) { final Message message = conversation.findMessageWithServerMsgId(stanzaId); if (message == null) { // do we want to check if isRead? diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 1c709aa2d2914b5b4dd92be2df706757334c5326..e4761de5fa73ed3332c5e40542708900a2454228 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.EntityTimeManager; import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager; +import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager; import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.PepManager; import eu.siacs.conversations.xmpp.manager.PingManager; @@ -38,6 +39,9 @@ public class Managers { .put(DiscoManager.class, new DiscoManager(context, connection)) .put(EntityTimeManager.class, new EntityTimeManager(context, connection)) .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection)) + .put( + MessageDisplayedSynchronizationManager.class, + new MessageDisplayedSynchronizationManager(context, connection)) .put(NickManager.class, new NickManager(context, connection)) .put(PepManager.class, new PepManager(context, connection)) .put(PingManager.class, new PingManager(context, connection)) diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java index 6ecf0edf874d0a07abdeff3feac4551b785e326a..3039b1020d714a1d75fe0ffc0bd85e1e500fd281 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; 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.services.XmppConnectionService; @@ -14,7 +13,7 @@ import java.util.Set; public class AbstractBookmarkManager extends AbstractManager { - private final XmppConnectionService service; + protected final XmppConnectionService service; protected AbstractBookmarkManager( final XmppConnectionService service, final XmppConnection connection) { @@ -23,7 +22,7 @@ public class AbstractBookmarkManager extends AbstractManager { } // TODO rename to setBookmarks? - public void processBookmarksInitial(final Map bookmarks, final boolean pep) { + protected void processBookmarksInitial(final Map bookmarks, final boolean pep) { final var account = getAccount(); // TODO we can internalize this getBookmarkedJid final Set previousBookmarks = account.getBookmarkedJids(); @@ -32,31 +31,31 @@ public class AbstractBookmarkManager extends AbstractManager { service.processModifiedBookmark(bookmark, pep); } if (pep) { - this.processDeletedBookmarks(account, previousBookmarks); + this.processDeletedBookmarks(previousBookmarks); } account.setBookmarks(bookmarks); } - public void processDeletedBookmarks(final Account account, final Collection bookmarks) { + protected void processDeletedBookmarks(final Collection bookmarks) { Log.d( Config.LOGTAG, - account.getJid().asBareJid() + getAccount().getJid().asBareJid() + ": " + bookmarks.size() + " bookmarks have been removed"); for (final Jid bookmark : bookmarks) { - processDeletedBookmark(account, bookmark); + processDeletedBookmark(bookmark); } } - public void processDeletedBookmark(final Account account, final Jid jid) { - final Conversation conversation = service.find(account, jid); + protected void processDeletedBookmark(final Jid jid) { + final Conversation conversation = service.find(getAccount(), jid); if (conversation == null) { return; } Log.d( Config.LOGTAG, - account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); + getAccount().getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update"); this.service.archiveConversation(conversation, false); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 977627bc6b7643c62afe0e57fc683dec3d85ccb4..e173197fcfc64968af5f95142753a7ec6d74653d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,15 +1,64 @@ package eu.siacs.conversations.xmpp.manager; -import android.content.Context; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.pubsub.Items; public class AvatarManager extends AbstractManager { - public AvatarManager(Context context, XmppConnection connection) { - super(context, connection); + private final XmppConnectionService service; + + public AvatarManager(final XmppConnectionService service, XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; + } + + public void handleItems(final Jid from, final Items items) { + final var account = getAccount(); + // TODO support retract + final var entry = items.getFirstItemWithId(Metadata.class); + final var avatar = + entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); + if (avatar != null) { + avatar.owner = from.asBareJid(); + if (service.getFileBackend().isAvatarCached(avatar)) { + if (account.getJid().asBareJid().equals(from)) { + if (account.setAvatar(avatar.getFilename())) { + service.databaseBackend.updateAccount(account); + service.notifyAccountAvatarHasChanged(account); + } + service.getAvatarService().clear(account); + service.updateConversationUi(); + service.updateAccountUi(); + } else { + final Contact contact = account.getRoster().getContact(from); + if (contact.setAvatar(avatar)) { + connection.getManager(RosterManager.class).writeToDatabaseAsync(); + service.getAvatarService().clear(contact); + service.updateConversationUi(); + service.updateRosterUi(); + } + } + } else if (service.isDataSaverDisabled()) { + service.fetchAvatar(account, avatar); + } + } } - public void handleItems(Jid from, final Items items) {} + public void handleDelete(final Jid from) { + final var account = getAccount(); + final boolean isAccount = account.getJid().asBareJid().equals(from); + if (isAccount) { + account.setAvatar(null); + getDatabase().updateAccount(account); + service.getAvatarService().clear(account); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node"); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java index 889054032f2c982861163a523a797982e089f4ea..72625504732deb19abf2552d070a4b14be2813c4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java @@ -1,15 +1,38 @@ package eu.siacs.conversations.xmpp.manager; import android.content.Context; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.axolotl.DeviceList; import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.HashSet; +import java.util.Set; public class AxolotlManager extends AbstractManager { - public AxolotlManager(Context context, XmppConnection connection) { + public AxolotlManager(final Context context, final XmppConnection connection) { super(context, connection); } - public void handleItems(Jid from, final Items items) {} + public void handleItems(final Jid from, final Items items) { + final var account = getAccount(); + final var deviceList = items.getFirstItem(DeviceList.class); + if (deviceList == null) { + return; + } + final Set deviceIds = deviceList.getDeviceIds(); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received PEP device list " + + deviceIds + + " update from " + + from + + ", processing... "); + final AxolotlService axolotlService = account.getAxolotlService(); + axolotlService.registerDevices(from, new HashSet<>(deviceIds)); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java index 4bfb5d080d9685ad2b95735e169c311aaa8a006f..ab8fffab1e6138c1fa28fa39f6a988b8af49aab8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java @@ -17,6 +17,9 @@ 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.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 { @@ -54,13 +57,35 @@ public class BookmarkManager extends AbstractBookmarkManager { } public void handleItems(final Items items) { - final var retractions = items.getRetractions(); - final var itemMap = items.getItemMap(Conference.class); - if (!retractions.isEmpty()) { - // deleteItems(retractions); + this.handleItems(items.getItemMap(Conference.class)); + this.handleRetractions(items.getRetractions()); + } + + private void handleRetractions(final Collection 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(); + } } - if (!itemMap.isEmpty()) { - // updateItems(itemMap); + } + + private void handleItems(final Map items) { + 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; + } + account.putBookmark(bookmark); + service.processModifiedBookmark(bookmark); + service.updateConversationUi(); } } @@ -91,5 +116,20 @@ public class BookmarkManager extends AbstractBookmarkManager { MoreExecutors.directExecutor()); } - public void deleteAllItems() {} + 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(); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java index 71817f42e829de5c5964ed99ba344081de1fa246..e58f541de66928a4950321f52894e05224f9194a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java @@ -1,8 +1,14 @@ package eu.siacs.conversations.xmpp.manager; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.bookmark.Storage; import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Map; public class LegacyBookmarkManager extends AbstractBookmarkManager { @@ -11,5 +17,27 @@ public class LegacyBookmarkManager extends AbstractBookmarkManager { super(service, connection); } - public void handleItems(final Items items) {} + public void handleItems(final Items items) { + final var account = this.getAccount(); + final var connection = this.connection; + if (connection.getFeatures().bookmarksConversion()) { + if (connection.getFeatures().bookmarks2()) { + Log.w( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received storage:bookmark notification even though we" + + " opted into bookmarks:1"); + } + final var storage = items.getFirstItem(Storage.class); + final Map bookmarks = Bookmark.parseFromStorage(storage, account); + this.processBookmarksInitial(bookmarks, true); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ignoring bookmark PEP event because bookmark conversion was" + + " not detected"); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6c1af04b382548b985b8b1adcdf817dca2ea64ee --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java @@ -0,0 +1,70 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.util.Log; + +import androidx.annotation.NonNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.mds.Displayed; +import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Map; + +public class MessageDisplayedSynchronizationManager extends AbstractManager { + + private final XmppConnectionService service; + + public MessageDisplayedSynchronizationManager( + final XmppConnectionService service, XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; + } + + public void handleItems(final Items items) { + for (final var item : items.getItemMap(Displayed.class).entrySet()) { + this.processMdsItem(item); + } + } + + public void processMdsItem(final Map.Entry item) { + final var account = getAccount(); + final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey())); + if (jid == null) { + return; + } + final var displayed = item.getValue(); + final var stanzaId = displayed.getStanzaId(); + final String id = stanzaId == null ? null : stanzaId.getId(); + final Conversation conversation = this.service.find(account, jid); + if (id != null && conversation != null) { + conversation.setDisplayState(id); + this.service.markReadUpToStanzaId(conversation, id); + } + } + + public void fetch() { + final var future = getManager(PepManager.class).fetchItems(Displayed.class); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Map result) { + for (final var entry : result.entrySet()) { + processMdsItem(entry); + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG,getAccount().getJid().asBareJid()+": could not retrieve MDS items", t); + } + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java index 56f8766adbbe456b768e5148f57a135a6a1eb54b..9beeaf4c4830389614c9537393d17ecea7b51ef9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java @@ -1,8 +1,10 @@ package eu.siacs.conversations.xmpp.manager; -import android.content.Context; import com.google.common.base.Strings; import com.google.common.util.concurrent.ListenableFuture; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.NodeConfiguration; @@ -11,16 +13,39 @@ import im.conversations.android.xmpp.model.pubsub.Items; public class NickManager extends AbstractManager { - public NickManager(Context context, XmppConnection connection) { - super(context, connection); + private final XmppConnectionService service; + + public NickManager(final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; } - public void handleItems(final Jid from, Items items) { + public void handleItems(final Jid from, final Items items) { final var item = items.getFirstItem(Nick.class); final var nick = item == null ? null : item.getContent(); if (from == null || Strings.isNullOrEmpty(nick)) { return; } + setNick(from, nick); + } + + private void setNick(final Jid user, final String nick) { + final var account = getAccount(); + if (user.asBareJid().equals(account.getJid().asBareJid())) { + account.setDisplayName(nick); + if (QuickConversationsService.isQuicksy()) { + service.getAvatarService().clear(account); + } + service.checkMucRequiresRename(); + } else { + final Contact contact = account.getRoster().getContact(user); + if (contact.setPresenceName(nick)) { + connection.getManager(RosterManager.class).writeToDatabaseAsync(); + service.getAvatarService().clear(contact); + } + } + service.updateConversationUi(); + service.updateAccountUi(); } public ListenableFuture publishNick(final String name) { @@ -28,4 +53,8 @@ public class NickManager extends AbstractManager { nick.setContent(name); return getManager(PepManager.class).publishSingleton(nick, NodeConfiguration.PRESENCE); } + + public void handleDelete(final Jid from) { + this.setNick(from, null); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java index 47856e26729ce138814061b80f87f659fbe1329d..2d1132b96f966722f7129d5c381570404c553c38 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -172,6 +172,10 @@ public class PubSubManager extends AbstractManager { getManager(LegacyBookmarkManager.class).handleItems(items); return; } + if (connection.fromAccount(message) && Namespace.MDS_DISPLAYED.equals(node)) { + getManager(MessageDisplayedSynchronizationManager.class).handleItems(items); + return; + } if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { getManager(AvatarManager.class).handleItems(from, items); return; @@ -187,13 +191,29 @@ public class PubSubManager extends AbstractManager { private void handlePurge(final Message message, final Purge purge) { final var from = message.getFrom(); + final var isFromBare = from == null || from.isBareJid(); final var node = purge.getNode(); if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { - getManager(BookmarkManager.class).deleteAllItems(); + getManager(BookmarkManager.class).handlePurge(); } } - private void handleDelete(final Message message, final Delete delete) {} + private void handleDelete(final Message message, final Delete delete) { + final var from = message.getFrom(); + final var isFromBare = from == null || from.isBareJid(); + final var node = delete.getNode(); + if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) { + getManager(BookmarkManager.class).handleDelete(); + return; + } + if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) { + getManager(AvatarManager.class).handleDelete(from); + return; + } + if (isFromBare && Namespace.NICK.equals(node)) { + getManager(NickManager.class).handleDelete(from); + } + } public ListenableFuture publishSingleton( Jid address, Extension item, final NodeConfiguration nodeConfiguration) { diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index dc8e1dd5ff95dd545011292b53c232c84744bb55..96b2be840c80b04aa6659c00a4cafedda148a2b5 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -8,6 +8,7 @@ import eu.siacs.conversations.generator.IqGenerator; 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.MessageDisplayedSynchronizationManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; @@ -73,7 +74,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { } if (features.mds()) { - service.fetchMessageDisplayedSynchronization(account); + connection.getManager(MessageDisplayedSynchronizationManager.class).fetch(); } else { Log.d(Config.LOGTAG, account.getJid() + ": server has no support for mds"); } From 15bfce952cf50897002f8dca06348e38917cd571 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 11:55:56 +0200 Subject: [PATCH 08/87] modify bookmarks vie respective managers --- .../conversations/entities/Bookmark.java | 10 +- .../conversations/generator/IqGenerator.java | 21 --- .../services/XmppConnectionService.java | 175 +++++++++--------- .../conversations/xmpp/XmppConnection.java | 11 -- .../xmpp/manager/AvatarManager.java | 5 + .../xmpp/manager/BookmarkManager.java | 31 +++- .../xmpp/manager/DiscoManager.java | 7 +- .../xmpp/manager/LegacyBookmarkManager.java | 22 ++- ...essageDisplayedSynchronizationManager.java | 8 +- .../xmpp/manager/NickManager.java | 12 +- .../xmpp/manager/PepManager.java | 16 ++ .../xmpp/manager/PrivateStorageManager.java | 13 ++ .../xmpp/manager/PubSubManager.java | 10 + .../android/xmpp/NodeConfiguration.java | 6 + .../xmpp/model/bookmark2/Conference.java | 4 + .../xmpp/model/bookmark2/Password.java | 12 ++ .../xmpp/model/pubsub/owner/Delete.java | 16 ++ .../android/xmpp/processor/BindProcessor.java | 18 +- 18 files changed, 243 insertions(+), 154 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark2/Password.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Delete.java diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 3245da705a2beb0b8f3e1b5a52f86d963ddef8e2..934c10a2d48b73fda974181a46f6db4fe50c00bb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -8,10 +8,10 @@ import com.google.common.collect.ImmutableList; import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; 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; import java.util.HashMap; @@ -24,7 +24,7 @@ public class Bookmark extends Element implements ListItem { private final Account account; private WeakReference conversation; private Jid jid; - protected Element extensions = new Element("extensions", Namespace.BOOKMARKS2); + protected Extensions extensions = new Extensions(); public Bookmark(final Account account, final Jid jid) { super("conference"); @@ -90,14 +90,14 @@ public class Bookmark extends Element implements ListItem { bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); bookmark.setNick(conference.findChildContent("nick")); bookmark.setPassword(conference.findChildContent("password")); - final Element extensions = conference.findChild("extensions", Namespace.BOOKMARKS2); + final var extensions = conference.getExtensions(); if (extensions != null) { - bookmark.extensions = extensions; + bookmark.extensions = conference.getExtensions(); } return bookmark; } - public Element getExtensions() { + public Extensions getExtensions() { return extensions; } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index fcb8273cda6dc4fda32cb0e957151421cccf8cdf..3394f9b4ec309cc9f320400d94206148d8f56fe2 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -6,7 +6,6 @@ import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.services.MessageArchiveService; @@ -193,26 +192,6 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions); } - public Element publishBookmarkItem(final Bookmark bookmark) { - final String name = bookmark.getBookmarkName(); - final String nick = bookmark.getNick(); - final String password = bookmark.getPassword(); - final boolean autojoin = bookmark.autojoin(); - final Element conference = new Element("conference", Namespace.BOOKMARKS2); - if (name != null) { - conference.setAttribute("name", name); - } - if (nick != null) { - conference.addChild("nick").setContent(nick); - } - if (password != null) { - conference.addChild("password").setContent(password); - } - conference.setAttribute("autojoin", String.valueOf(autojoin)); - conference.addChild(bookmark.getExtensions()); - return conference; - } - public Element mdsDisplayed(final String stanzaId, final Conversation conversation) { final Jid by; if (conversation.getMode() == Conversation.MODE_MULTI) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index c7f9baacdbf0bc51a8040286c99a57c234b9f09d..b96cd096adbcae06e2caeaaa4b476285052a2689 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -132,9 +132,14 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; +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.NickManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; +import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; @@ -230,16 +235,6 @@ public class XmppConnectionService extends Service { private final Set mInProgressAvatarFetches = new HashSet<>(); private final Set mOmittedPepAvatarFetches = new HashSet<>(); public final HashSet mLowPingTimeoutMode = new HashSet<>(); - private final Consumer mDefaultIqHandler = - (packet) -> { - if (packet.getType() != Iq.Type.RESULT) { - final var error = packet.getError(); - String text = error != null ? error.findChildContent("text") : null; - if (text != null) { - Log.d(Config.LOGTAG, "received iq error: " + text); - } - } - }; public DatabaseBackend databaseBackend; private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger"); @@ -2040,81 +2035,76 @@ public class XmppConnectionService extends Service { public void createBookmark(final Account account, final Bookmark bookmark) { account.putBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); - if (connection == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": no connection. ignoring bookmark creation"); - } else if (connection.getFeatures().bookmarks2()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2"); - final Element item = mIqGenerator.publishBookmarkItem(bookmark); - pushNodeAndEnforcePublishOptions( - account, - Namespace.BOOKMARKS2, - item, - bookmark.getJid().asBareJid().toString(), - PublishOptions.persistentWhitelistAccessMaxItems()); - } else if (connection.getFeatures().bookmarksConversion()) { - pushBookmarksPep(account); + final ListenableFuture 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 { - pushBookmarksPrivateXml(account); + future = + connection + .getManager(PrivateStorageManager.class) + .publishBookmarks(account.getBookmarks()); } + Futures.addCallback( + future, + new FutureCallback() { + @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()); } public void deleteBookmark(final Account account, final Bookmark bookmark) { account.removeBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); - if (connection.getFeatures().bookmarks2()) { - final Iq request = - mIqGenerator.deleteItem( - Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString()); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2"); - sendIqPacket( - account, - request, - (response) -> { - if (response.getType() == Iq.Type.ERROR) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to delete bookmark " - + response.getErrorCondition()); - } - }); - } else if (connection.getFeatures().bookmarksConversion()) { - pushBookmarksPep(account); + final ListenableFuture 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 { - pushBookmarksPrivateXml(account); - } - } - - private void pushBookmarksPrivateXml(Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); - final Iq iqPacket = new Iq(Iq.Type.SET); - // TODO we have extensions for that - Element query = iqPacket.query("jabber:iq:private"); - Element storage = query.addChild("storage", "storage:bookmarks"); - for (final Bookmark bookmark : account.getBookmarks()) { - storage.addChild(bookmark); + future = + connection + .getManager(PrivateStorageManager.class) + .publishBookmarks(account.getBookmarks()); } - sendIqPacket(account, iqPacket, mDefaultIqHandler); - } + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark"); + } - private void pushBookmarksPep(Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep"); - final Element storage = new Element("storage", "storage:bookmarks"); - for (final Bookmark bookmark : account.getBookmarks()) { - storage.addChild(bookmark); - } - pushNodeAndEnforcePublishOptions( - account, - Namespace.BOOKMARKS, - storage, - "current", - PublishOptions.persistentWhitelistAccess()); + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not delete bookmark", + t); + } + }, + MoreExecutors.directExecutor()); } private void pushNodeAndEnforcePublishOptions( @@ -4942,7 +4932,8 @@ public class XmppConnectionService extends Service { public void notifyAccountAvatarHasChanged(final Account account) { final XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().bookmarksConversion()) { + // this was bookmark conversion for a bit which doesn't make sense + if (connection.getManager(AvatarManager.class).hasPepToVCardConversion()) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -5831,26 +5822,26 @@ public class XmppConnectionService extends Service { } public void publishDisplayName(final Account account) { - String displayName = account.getDisplayName(); - final Iq request; - if (TextUtils.isEmpty(displayName)) { - request = mIqGenerator.deleteNode(Namespace.NICK); - } else { - request = mIqGenerator.publishNick(displayName); - } + final var connection = account.getXmppConnection(); + final String displayName = account.getDisplayName(); mAvatarService.clear(account); - sendIqPacket( - account, - request, - (packet) -> { - if (packet.getType() == Iq.Type.ERROR) { + final var future = connection.getManager(NickManager.class).publish(displayName); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { Log.d( Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to modify nick name " - + packet); + account.getJid().asBareJid() + ": published User Nick"); } - }); + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not publish User Nick", t); + } + }, + MoreExecutors.directExecutor()); } public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b5db98f80ab0f6c95582090b3554abe93d830545..9cd3f96409e521757219dc1462912a6078e8fdb0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3093,11 +3093,6 @@ public class XmppConnection implements Runnable { } } - public boolean bookmarksConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) - && pepPublishOptions(); - } - public boolean blocking() { return connection.getManager(BlockingManager.class).hasFeature(); } @@ -3262,12 +3257,6 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS); } - public boolean bookmarks2() { - return pepPublishOptions() - && pepConfigNodeMax() - && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); - } - public boolean externalServiceDiscovery() { return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index e173197fcfc64968af5f95142753a7ec6d74653d..7eac27a005d219363a747a00cfee1b91993dfcec 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -4,6 +4,7 @@ import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Contact; 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 eu.siacs.conversations.xmpp.pep.Avatar; @@ -61,4 +62,8 @@ public class AvatarManager extends AbstractManager { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node"); } } + + public boolean hasPepToVCardConversion() { + return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java index ab8fffab1e6138c1fa28fa39f6a988b8af49aab8..03a8cc3f3b085221e393214467bf25be2e88ecab 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java @@ -16,6 +16,7 @@ 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; @@ -89,18 +90,24 @@ public class BookmarkManager extends AbstractBookmarkManager { } } - public ListenableFuture publishBookmark(final Jid address, final boolean autoJoin) { - return publishBookmark(address, autoJoin, null); - } - - public ListenableFuture publishBookmark( - final Jid address, final boolean autoJoin, final String nick) { + public ListenableFuture 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(autoJoin); + 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), @@ -108,7 +115,7 @@ public class BookmarkManager extends AbstractBookmarkManager { MoreExecutors.directExecutor()); } - public ListenableFuture retractBookmark(final Jid address) { + public ListenableFuture retract(final Jid address) { final var itemId = address.toString(); return Futures.transform( getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2), @@ -132,4 +139,12 @@ public class BookmarkManager extends AbstractBookmarkManager { 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); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index cb143bb7b96c585dabb5126f2a3c54097645696d..d13856f0be5b0d6551c8066c66107d241e0810f4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -331,7 +331,7 @@ public class DiscoManager extends AbstractManager { if (appSettings.isBroadcastLastActivity()) { features.add(Namespace.IDLE); } - if (connection.getFeatures().bookmarks2()) { + if (getManager(BookmarkManager.class).hasFeature()) { features.add(Namespace.BOOKMARKS2 + "+notify"); } else { features.add(Namespace.BOOKMARKS + "+notify"); @@ -427,6 +427,11 @@ public class DiscoManager extends AbstractManager { return infoQuery != null && infoQuery.hasFeature(feature); } + public boolean hasAccountFeature(final String feature) { + final var infoQuery = this.get(getAccount().getJid().asBareJid()); + return infoQuery != null && infoQuery.hasFeature(feature); + } + private void put(final Jid address, final InfoQuery infoQuery) { synchronized (this.entityInformation) { this.entityInformation.put(address, infoQuery); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java index e58f541de66928a4950321f52894e05224f9194a..0730ab088b671e801b661db20709129e28615e88 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java @@ -1,13 +1,17 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; +import com.google.common.util.concurrent.ListenableFuture; import eu.siacs.conversations.Config; 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.bookmark.Storage; import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Collection; import java.util.Map; public class LegacyBookmarkManager extends AbstractBookmarkManager { @@ -19,9 +23,8 @@ public class LegacyBookmarkManager extends AbstractBookmarkManager { public void handleItems(final Items items) { final var account = this.getAccount(); - final var connection = this.connection; - if (connection.getFeatures().bookmarksConversion()) { - if (connection.getFeatures().bookmarks2()) { + if (this.hasConversion()) { + if (getManager(BookmarkManager.class).hasFeature()) { Log.w( Config.LOGTAG, account.getJid().asBareJid() @@ -40,4 +43,17 @@ public class LegacyBookmarkManager extends AbstractBookmarkManager { + " not detected"); } } + + public boolean hasConversion() { + return getManager(PepManager.class).hasPublishOptions() + && getManager(DiscoManager.class).hasAccountFeature(Namespace.BOOKMARKS_CONVERSION); + } + + public ListenableFuture publish(final Collection bookmarks) { + final var storage = new Storage(); + for (final var bookmark : bookmarks) { + storage.addChild(bookmark); + } + return getManager(PepManager.class).publishSingleton(storage, NodeConfiguration.WHITELIST); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java index 6c1af04b382548b985b8b1adcdf817dca2ea64ee..8d2c4b65748cd1d9bb8ced1de16f47e1e4e692e9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java @@ -1,12 +1,10 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; - import androidx.annotation.NonNull; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; @@ -62,7 +60,11 @@ public class MessageDisplayedSynchronizationManager extends AbstractManager { @Override public void onFailure(@NonNull Throwable t) { - Log.d(Config.LOGTAG,getAccount().getJid().asBareJid()+": could not retrieve MDS items", t); + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not retrieve MDS items", + t); } }, MoreExecutors.directExecutor()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java index 9beeaf4c4830389614c9537393d17ecea7b51ef9..93a509551245497e40fc3c25a6144a8219f28165 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java @@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.QuickConversationsService; 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; @@ -48,10 +49,13 @@ public class NickManager extends AbstractManager { service.updateAccountUi(); } - public ListenableFuture publishNick(final String name) { - final Nick nick = new Nick(); - nick.setContent(name); - return getManager(PepManager.class).publishSingleton(nick, NodeConfiguration.PRESENCE); + public ListenableFuture publish(final String name) { + if (Strings.isNullOrEmpty(name)) { + return getManager(PepManager.class).delete(Namespace.NICK); + } else { + return getManager(PepManager.class) + .publishSingleton(new Nick(name), NodeConfiguration.PRESENCE); + } } public void handleDelete(final Jid from) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java index 6ba8534588cd6fcff3c0dced7b5bde417a014ff4..fd5bd6f155b9040d2ff37797021914e077486c24 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java @@ -1,7 +1,10 @@ package eu.siacs.conversations.xmpp.manager; import android.content.Context; +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.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.NodeConfiguration; @@ -43,6 +46,19 @@ public class PepManager extends AbstractManager { return pubSubManager().retract(pepService(), itemId, node); } + public ListenableFuture delete(final String node) { + final var future = pubSubManager().delete(pepService(), node); + return Futures.transform(future, iq -> null, MoreExecutors.directExecutor()); + } + + public boolean hasPublishOptions() { + return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_PUBLISH_OPTIONS); + } + + public boolean hasConfigNodeMax() { + return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_CONFIG_NODE_MAX); + } + private PubSubManager pubSubManager() { return getManager(PubSubManager.class); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java index 1a5bd56c62939c39bedfe18871b16ae02f85edbf..b65a4fd56d83a12c4a169fe3cf6641b68bc1d4b7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java @@ -4,6 +4,7 @@ import android.util.Log; 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.Config; import eu.siacs.conversations.entities.Bookmark; @@ -13,6 +14,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.model.bookmark.Storage; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.storage.PrivateStorage; +import java.util.Collection; import java.util.Map; public class PrivateStorageManager extends AbstractBookmarkManager { @@ -52,4 +54,15 @@ public class PrivateStorageManager extends AbstractBookmarkManager { }, MoreExecutors.directExecutor()); } + + public ListenableFuture publishBookmarks(Collection bookmarks) { + final var iq = new Iq(Iq.Type.SET); + final var privateStorage = iq.addExtension(new PrivateStorage()); + final var storage = privateStorage.addExtension(new Storage()); + for (final var bookmark : bookmarks) { + storage.addChild(bookmark); + } + return Futures.transform( + connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java index 2d1132b96f966722f7129d5c381570404c553c38..956049335d9db2f2e724b16807b0cb1f4991afb7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -345,6 +345,16 @@ public class PubSubManager extends AbstractManager { return connection.sendIqPacket(iq); } + public ListenableFuture delete(final Jid address, final String node) { + final var iq = new Iq(Iq.Type.SET); + iq.setTo(address); + final var pubSub = iq.addExtension(new PubSubOwner()); + final var delete = + pubSub.addExtension(new im.conversations.android.xmpp.model.pubsub.owner.Delete()); + delete.setNode(node); + return connection.sendIqPacket(iq); + } + private static class PubSubExceptionTransformer implements AsyncFunction { diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java index 81a55f18c5a80b7acf2a90ccda9661169ff0e05f..e96e453e200547056d0ce45837d63265d3997f97 100644 --- a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java +++ b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java @@ -28,6 +28,12 @@ public class NodeConfiguration implements Map { .put(PERSIST_ITEMS, Boolean.TRUE) .put(ACCESS_MODEL, "presence") .build()); + public static final NodeConfiguration WHITELIST = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "whitelist") + .build()); public static final NodeConfiguration WHITELIST_MAX_ITEMS = new NodeConfiguration( new ImmutableMap.Builder() diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java index 265c80e3fd7db0d2e56572eb700f94592af12a5e..691a80364b1fb8f93977ce536c07ea56762a927d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java @@ -29,4 +29,8 @@ public class Conference extends Extension { public Extensions getExtensions() { return this.getExtension(Extensions.class); } + + public void setConferenceName(String name) { + this.setAttribute("name", name); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Password.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Password.java new file mode 100644 index 0000000000000000000000000000000000000000..85204a5e786c7425ce357c8ebacd45d7dd8c9ee5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Password.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Password extends Extension { + + public Password() { + super(Password.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Delete.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Delete.java new file mode 100644 index 0000000000000000000000000000000000000000..ca60593aaa6ecf6d123938c6c1df9693cdb6f1d1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Delete.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Delete extends Extension { + + public Delete() { + super(Delete.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 96b2be840c80b04aa6659c00a4cafedda148a2b5..6116649a449eb0187a73b28ef6addccc6a1c0fae 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -1,14 +1,16 @@ package im.conversations.android.xmpp.processor; -import android.text.TextUtils; import android.util.Log; +import com.google.common.base.Strings; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.generator.IqGenerator; 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.LegacyBookmarkManager; import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager; +import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import im.conversations.android.xmpp.model.stanza.Iq; @@ -45,12 +47,13 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { } if (loggedInSuccessfully) { - if (!TextUtils.isEmpty(account.getDisplayName())) { + final String displayName = account.getDisplayName(); + if (!Strings.isNullOrEmpty(displayName)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); - service.publishDisplayName(account); + getManager(NickManager.class).publish(displayName); } } @@ -66,10 +69,13 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable { getManager(RosterManager.class).request(); - if (features.bookmarks2()) { + if (getManager(BookmarkManager.class).hasFeature()) { connection.getManager(BookmarkManager.class).fetch(); - // log that we use bookmarks 1 and wait for +notify - } else if (!features.bookmarksConversion()) { + } else if (getManager(LegacyBookmarkManager.class).hasConversion()) { + Log.d( + Config.LOGTAG, + account.getJid() + ": not fetching bookmarks. waiting for server to push"); + } else { connection.getManager(PrivateStorageManager.class).fetchBookmarks(); } From 89da1a89e21bbee334a1045fec877421c8e720b4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 12:09:49 +0200 Subject: [PATCH 09/87] publish MDS via manager --- .../conversations/generator/IqGenerator.java | 18 ---- .../services/XmppConnectionService.java | 84 ++----------------- ...essageDisplayedSynchronizationManager.java | 21 +++++ .../xmpp/pep/PublishOptions.java | 20 ----- .../android/xmpp/model/unique/StanzaId.java | 4 + 5 files changed, 33 insertions(+), 114 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 3394f9b4ec309cc9f320400d94206148d8f56fe2..61c24b8748e0030518aa986982fe542d400ad315 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -192,24 +192,6 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions); } - public Element mdsDisplayed(final String stanzaId, final Conversation conversation) { - final Jid by; - if (conversation.getMode() == Conversation.MODE_MULTI) { - by = conversation.getJid().asBareJid(); - } else { - by = conversation.getAccount().getJid().asBareJid(); - } - return mdsDisplayed(stanzaId, by); - } - - private Element mdsDisplayed(final String stanzaId, final Jid by) { - final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED); - final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS); - stanzaIdElement.setAttribute("id", stanzaId); - stanzaIdElement.setAttribute("by", by); - return displayed; - } - public Iq publishBundles( final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b96cd096adbcae06e2caeaaa4b476285052a2689..8163581fad99af56fba70a828fd6b154ab769475 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -137,6 +137,7 @@ 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.NickManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; @@ -2107,66 +2108,6 @@ public class XmppConnectionService extends Service { MoreExecutors.directExecutor()); } - private void pushNodeAndEnforcePublishOptions( - final Account account, - final String node, - final Element element, - final String id, - final Bundle options) { - pushNodeAndEnforcePublishOptions(account, node, element, id, options, true); - } - - private void pushNodeAndEnforcePublishOptions( - final Account account, - final String node, - final Element element, - final String id, - final Bundle options, - final boolean retry) { - final Iq packet = mIqGenerator.publishElement(node, element, id, options); - sendIqPacket( - account, - packet, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - return; - } - if (retry && PublishOptions.preconditionNotMet(response)) { - pushNodeConfiguration( - account, - node, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - pushNodeAndEnforcePublishOptions( - account, node, element, id, options, false); - } - - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to push node configuration (" - + node - + ")"); - } - }); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error publishing " - + node - + " (retry=" - + retry - + ") " - + response); - } - }); - } - private void restoreFromDatabase() { synchronized (this.conversations) { final Map accountLookupTable = @@ -5387,7 +5328,8 @@ public class XmppConnectionService extends Service { final String stanzaId = last.getServerMsgId(); if (sendDisplayedMarker && serverAssist) { - final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation); + final var mdsDisplayed = + MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation); final var packet = mMessageGenerator.confirm(last); packet.addChild(mdsDisplayed); if (!last.isPrivateMessage()) { @@ -5434,21 +5376,11 @@ public class XmppConnectionService extends Service { itemId = conversation.getJid().asBareJid(); } Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId); - publishMds(account, itemId, stanzaId, conversation); - } - - private void publishMds( - final Account account, - final Jid itemId, - final String stanzaId, - final Conversation conversation) { - final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation); - pushNodeAndEnforcePublishOptions( - account, - Namespace.MDS_DISPLAYED, - item, - itemId.toString(), - PublishOptions.persistentWhitelistAccessMaxItems()); + final var displayed = + MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation); + connection + .getManager(MessageDisplayedSynchronizationManager.class) + .publish(itemId, displayed); } public boolean sendReactions(final Message message, final Collection reactions) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java index 8d2c4b65748cd1d9bb8ced1de16f47e1e4e692e9..84417d90b160bc70edb7b93298882a82bfeb7192 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java @@ -4,14 +4,17 @@ import android.util.Log; 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.Config; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.NodeConfiguration; import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.unique.StanzaId; import java.util.Map; public class MessageDisplayedSynchronizationManager extends AbstractManager { @@ -69,4 +72,22 @@ public class MessageDisplayedSynchronizationManager extends AbstractManager { }, MoreExecutors.directExecutor()); } + + public static Displayed displayed(final String id, final Conversation conversation) { + final Jid by; + if (conversation.getMode() == Conversation.MODE_MULTI) { + by = conversation.getJid().asBareJid(); + } else { + by = conversation.getAccount().getJid().asBareJid(); + } + final var displayed = new Displayed(); + final var stanzaId = displayed.addExtension(new StanzaId(id)); + stanzaId.setBy(by); + return displayed; + } + + public ListenableFuture publish(final Jid itemId, final Displayed displayed) { + return getManager(PepManager.class) + .publish(displayed, itemId.toString(), NodeConfiguration.WHITELIST_MAX_ITEMS); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index 2dcaf9bac7b07c6cc89d7d7a3b6db18789f59fc7..534f943c03738c2511fb327ad92fd067e10239bb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -23,26 +23,6 @@ public class PublishOptions { return options; } - public static Bundle persistentWhitelistAccess() { - final Bundle options = new Bundle(); - options.putString("pubsub#persist_items", "true"); - options.putString("pubsub#access_model", "whitelist"); - return options; - } - - public static Bundle persistentWhitelistAccessMaxItems() { - final Bundle options = new Bundle(); - options.putString("pubsub#persist_items", "true"); - options.putString("pubsub#access_model", "whitelist"); - options.putString("pubsub#send_last_published_item", "never"); - options.putString("pubsub#max_items", "max"); - options.putString("pubsub#notify_delete", "true"); - options.putString( - "pubsub#notify_retract", "true"); // one could also set notify=true on the retract - - return options; - } - public static boolean preconditionNotMet(Iq response) { final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java index f2d1c506814b6cc3f160fb0866348bb61513546d..bd2319f9307c7c7632469461d336109e69697dee 100644 --- a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -22,6 +22,10 @@ public class StanzaId extends Extension { return this.getAttributeAsJid("by"); } + public void setBy(final Jid by) { + this.setAttribute("by", by); + } + public String getId() { return this.getAttribute("id"); } From 2a9337c5c8c8fcdc94c714725e1500b6f317b23e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 14:08:18 +0200 Subject: [PATCH 10/87] publish avatar via manager --- .../conversations/generator/IqGenerator.java | 54 ---- .../conversations/services/AvatarService.java | 17 +- .../services/XmppConnectionService.java | 245 +++--------------- .../xmpp/manager/AvatarManager.java | 73 ++++-- .../xmpp/manager/DiscoManager.java | 1 + .../android/xmpp/model/avatar/Info.java | 20 ++ 6 files changed, 105 insertions(+), 305 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 61c24b8748e0030518aa986982fe542d400ad315..1ea59ba83c98f0aac81cf46e11766e811e82ab95 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -69,21 +69,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq retrieveBookmarks() { - return retrieve(Namespace.BOOKMARKS2, null); - } - - public Iq retrieveMds() { - return retrieve(Namespace.MDS_DISPLAYED, null); - } - - public Iq publishNick(String nick) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - item.addChild("nick", Namespace.NICK).setContent(nick); - return publish(Namespace.NICK, item); - } - public Iq deleteNode(final String node) { final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); @@ -91,45 +76,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq deleteItem(final String node, final String id) { - final var packet = new Iq(Iq.Type.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); - final Element retract = pubsub.addChild("retract"); - retract.setAttribute("node", node); - retract.setAttribute("notify", "true"); - retract.addChild("item").setAttribute("id", id); - return packet; - } - - public Iq publishAvatar(Avatar avatar, Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", Namespace.AVATAR_DATA); - data.setContent(avatar.image); - return publish(Namespace.AVATAR_DATA, item, options); - } - - public Iq publishElement( - final String namespace, final Element element, String id, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", id); - item.addChild(element); - return publish(namespace, item, options); - } - - public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element metadata = item.addChild("metadata", Namespace.AVATAR_METADATA); - final Element info = metadata.addChild("info"); - info.setAttribute("bytes", avatar.size); - info.setAttribute("id", avatar.sha1sum); - info.setAttribute("height", avatar.height); - info.setAttribute("width", avatar.height); - info.setAttribute("type", avatar.type); - return publish(Namespace.AVATAR_METADATA, item, options); - } - public Iq retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 941914a92ea7b7a6abde01eda8b7615ba5aae6e6..fd2cf0099a2c8ab267713ee4ae8dbb3a4fe0b072 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -14,12 +14,10 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.TextUtils; import android.util.DisplayMetrics; -import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import com.google.common.base.Strings; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; @@ -33,15 +31,13 @@ import eu.siacs.conversations.entities.RawBlockable; import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.XmppConnection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -public class AvatarService implements OnAdvancedStreamFeaturesLoaded { +public class AvatarService { private static final int FG_COLOR = 0xFFFAFAFA; private static final int TRANSPARENT = 0x00000000; @@ -714,17 +710,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { return true; } - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - XmppConnection.Features features = account.getXmppConnection().getFeatures(); - if (features.pep() && !features.pepPersistent()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has pep but is not persistent"); - if (account.getAvatar() != null) { - mXmppConnectionService.republishAvatarIfNeeded(account); - } - } - } - private static String emptyOnNull(@Nullable Jid value) { return value == null ? "" : value.toString(); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8163581fad99af56fba70a828fd6b154ab769475..6898c7dc9b2ee225ade4b40091532cf3feb235ae 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -143,7 +143,6 @@ import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.avatar.Metadata; @@ -1653,7 +1652,6 @@ public class XmppConnectionService extends Service { final XmppConnection connection = new XmppConnection(account, this); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); - connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); return connection; } @@ -3561,6 +3559,7 @@ public class XmppConnectionService extends Service { updateAccountUi(); } }; + // TODO execute this via the respective Managers deleteVcardAvatar(account, onDeleted); deletePepNode(account, Namespace.AVATAR_DATA); deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted); @@ -4368,15 +4367,36 @@ public class XmppConnectionService extends Service { final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final int size = Config.AVATAR_SIZE; final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, "unable to save vcard"); - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - return; - } - publishAvatar(account, avatar, open, callback); - } else { + if (avatar == null) { callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); + return; + } + if (fileBackend.save(avatar)) { + final var connection = account.getXmppConnection(); + final var future = connection.getManager(AvatarManager.class).publish(avatar, open); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { + callback.onAvatarPublicationSucceeded(); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not publish avatar", + t); + callback.onAvatarPublicationFailed( + R.string.error_publish_avatar_server_reject); + } + }, + MoreExecutors.directExecutor()); + + } else { + Log.d(Config.LOGTAG, "could not save avatar"); + callback.onAvatarPublicationFailed(R.string.error_saving_avatar); } } @@ -4430,211 +4450,6 @@ public class XmppConnectionService extends Service { }); } - public void publishAvatar( - final Account account, - final Avatar avatar, - final boolean open, - final OnAvatarPublication callback) { - final Bundle options; - if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess(); - } else { - options = null; - } - publishAvatar(account, avatar, options, true, callback); - } - - public void publishAvatar( - Account account, - final Avatar avatar, - final Bundle options, - final boolean retry, - final OnAvatarPublication callback) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": publishing avatar. options=" + options); - final Iq packet = this.mIqGenerator.publishAvatar(avatar, options); - this.sendIqPacket( - account, - packet, - result -> { - if (result.getType() == Iq.Type.RESULT) { - publishAvatarMetadata(account, avatar, options, true, callback); - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration( - account, - Namespace.AVATAR_DATA, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": changed node configuration for avatar" - + " node"); - publishAvatar(account, avatar, options, false, callback); - } - - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to change node configuration" - + " for avatar node"); - publishAvatar(account, avatar, null, false, callback); - } - }); - } else { - Element error = result.findChild("error"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server rejected avatar " - + (avatar.size / 1024) - + "KiB " - + (error != null ? error.toString() : "")); - if (callback != null) { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - } - }); - } - - public void publishAvatarMetadata( - Account account, - final Avatar avatar, - final Bundle options, - final boolean retry, - final OnAvatarPublication callback) { - final Iq packet = - XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); - sendIqPacket( - account, - packet, - result -> { - if (result.getType() == Iq.Type.RESULT) { - if (account.setAvatar(avatar.getFilename())) { - getAvatarService().clear(account); - databaseBackend.updateAccount(account); - notifyAccountAvatarHasChanged(account); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": published avatar " - + (avatar.size / 1024) - + "KiB"); - if (callback != null) { - callback.onAvatarPublicationSucceeded(); - } - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration( - account, - Namespace.AVATAR_METADATA, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": changed node configuration for avatar" - + " meta data node"); - publishAvatarMetadata( - account, avatar, options, false, callback); - } - - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to change node configuration" - + " for avatar meta data node"); - publishAvatarMetadata( - account, avatar, null, false, callback); - } - }); - } else { - if (callback != null) { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - } - }); - } - - public void republishAvatarIfNeeded(final Account account) { - if (account.getAxolotlService().isPepBroken()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": skipping republication of avatar because pep is broken"); - return; - } - final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket( - account, - packet, - new Consumer() { - - private Avatar parseAvatar(final Iq packet) { - final var pubsub = packet.getExtension(PubSub.class); - if (pubsub == null) { - return null; - } - final var items = pubsub.getItems(); - if (items == null) { - return null; - } - final var item = items.getFirstItemWithId(Metadata.class); - if (item == null) { - return null; - } - return Avatar.parseMetadata(item.getKey(), item.getValue()); - } - - private boolean errorIsItemNotFound(Iq packet) { - Element error = packet.findChild("error"); - return packet.getType() == Iq.Type.ERROR - && error != null - && error.hasChild("item-not-found"); - } - - @Override - public void accept(final Iq packet) { - if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) { - final Avatar serverAvatar = parseAvatar(packet); - if (serverAvatar == null && account.getAvatar() != null) { - final Avatar avatar = - fileBackend.getStoredPepAvatar(account.getAvatar()); - if (avatar != null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": avatar on server was null. republishing"); - // publishing as 'open' - old server (that requires - // republication) likely doesn't support access models anyway - publishAvatar( - account, - fileBackend.getStoredPepAvatar(account.getAvatar()), - true, - null); - } else { - Log.e( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error rereading avatar"); - } - } - } - } - }); - } - public void cancelAvatarFetches(final Account account) { synchronized (mInProgressAvatarFetches) { for (final Iterator iterator = mInProgressAvatarFetches.iterator(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 7eac27a005d219363a747a00cfee1b91993dfcec..7104ead02a1dcc4105af573f35750abf330590a3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,6 +1,9 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; +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.Contact; import eu.siacs.conversations.services.XmppConnectionService; @@ -8,6 +11,9 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.avatar.Data; +import im.conversations.android.xmpp.model.avatar.Info; import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.pubsub.Items; @@ -24,31 +30,35 @@ public class AvatarManager extends AbstractManager { final var account = getAccount(); // TODO support retract final var entry = items.getFirstItemWithId(Metadata.class); + Log.d(Config.LOGTAG, "<-- " + entry + " (" + from + ")"); final var avatar = entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); - if (avatar != null) { - avatar.owner = from.asBareJid(); - if (service.getFileBackend().isAvatarCached(avatar)) { - if (account.getJid().asBareJid().equals(from)) { - if (account.setAvatar(avatar.getFilename())) { - service.databaseBackend.updateAccount(account); - service.notifyAccountAvatarHasChanged(account); - } - service.getAvatarService().clear(account); + if (avatar == null) { + Log.d(Config.LOGTAG, "could not parse avatar metadata from " + from); + return; + } + avatar.owner = from.asBareJid(); + if (service.getFileBackend().isAvatarCached(avatar)) { + if (account.getJid().asBareJid().equals(from)) { + if (account.setAvatar(avatar.getFilename())) { + service.databaseBackend.updateAccount(account); + service.notifyAccountAvatarHasChanged(account); + } + service.getAvatarService().clear(account); + service.updateConversationUi(); + service.updateAccountUi(); + } else { + final Contact contact = account.getRoster().getContact(from); + if (contact.setAvatar(avatar)) { + connection.getManager(RosterManager.class).writeToDatabaseAsync(); + service.getAvatarService().clear(contact); service.updateConversationUi(); - service.updateAccountUi(); - } else { - final Contact contact = account.getRoster().getContact(from); - if (contact.setAvatar(avatar)) { - connection.getManager(RosterManager.class).writeToDatabaseAsync(); - service.getAvatarService().clear(contact); - service.updateConversationUi(); - service.updateRosterUi(); - } + service.updateRosterUi(); } - } else if (service.isDataSaverDisabled()) { - service.fetchAvatar(account, avatar); } + } else if (service.isDataSaverDisabled()) { + // TODO use internal mechanism to fetch PEP avatars + service.fetchAvatar(account, avatar); } } @@ -63,6 +73,29 @@ public class AvatarManager extends AbstractManager { } } + public ListenableFuture publish(final Avatar avatar, final boolean open) { + final NodeConfiguration configuration = + open ? NodeConfiguration.OPEN : NodeConfiguration.PRESENCE; + final var avatarData = new Data(); + avatarData.setContent(avatar.getImageAsBytes()); + final var future = + getManager(PepManager.class).publish(avatarData, avatar.sha1sum, configuration); + return Futures.transformAsync( + future, + v -> { + final var id = avatar.sha1sum; + final var metadata = new Metadata(); + final var info = metadata.addExtension(new Info()); + info.setBytes(avatar.size); + info.setId(avatar.sha1sum); + info.setHeight(avatar.height); + info.setWidth(avatar.width); + info.setType(avatar.type); + return getManager(PepManager.class).publish(metadata, id, configuration); + }, + MoreExecutors.directExecutor()); + } + public boolean hasPepToVCardConversion() { return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index d13856f0be5b0d6551c8066c66107d241e0810f4..526b43b42228af8d60db2612f29967f7250e694f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -311,6 +311,7 @@ public class DiscoManager extends AbstractManager { final var appSettings = new AppSettings(context); final var account = connection.getAccount(); final ImmutableList.Builder features = ImmutableList.builder(); + features.addAll(STATIC_FEATURES); if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) { features.add(Namespace.MDS_DISPLAYED + "+notify"); } diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java index f544af72fc784e0b935bbac0c767bd3504194807..31099ff79e246d884da1b4a2c738571cb359f57a 100644 --- a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java @@ -34,4 +34,24 @@ public class Info extends Extension { public String getId() { return this.getAttribute("id"); } + + public void setBytes(final long size) { + this.setAttribute("bytes", size); + } + + public void setId(final String id) { + this.setAttribute("id", id); + } + + public void setHeight(final long height) { + this.setAttribute("height", height); + } + + public void setWidth(final long width) { + this.setAttribute("width", width); + } + + public void setType(final String type) { + this.setAttribute("type", type); + } } From c8f962b7a5a7fb8385d2657c58bff3c707ee75db Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 18:42:23 +0200 Subject: [PATCH 11/87] retrieve avatar via manager --- .../siacs/conversations/entities/Contact.java | 7 +- .../services/XmppConnectionService.java | 166 ++++++++---------- .../eu/siacs/conversations/xmpp/Managers.java | 2 + .../xmpp/manager/AvatarManager.java | 66 ++++++- .../xmpp/manager/VCardManager.java | 50 ++++++ .../android/xmpp/model/vcard/Photo.java | 4 + .../android/xmpp/model/vcard/VCard.java | 4 + 7 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index d9a215f41b5039f173acf4e5d2a8e5af7820cf0c..8b023b9b46a3bab029dcd80099f6dbdd0a278759 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -438,15 +438,10 @@ public class Contact implements ListItem, Blockable { } public boolean setAvatar(final Avatar avatar) { - return setAvatar(avatar, false); - } - - public boolean setAvatar(final Avatar avatar, final boolean previouslyOmittedPepFetch) { if (this.avatar != null && this.avatar.equals(avatar)) { return false; } - if (!previouslyOmittedPepFetch - && this.avatar != null + if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) { return false; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 6898c7dc9b2ee225ade4b40091532cf3feb235ae..69276d9512e1cf91f2a0ae337e4287f80b423aab 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -57,6 +57,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -142,6 +143,7 @@ import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; 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; @@ -4462,23 +4464,18 @@ public class XmppConnectionService extends Service { } } - public void fetchAvatar(Account account, Avatar avatar) { - fetchAvatar(account, avatar, null); - } - - public void fetchAvatar( - Account account, final Avatar avatar, final UiCallback callback) { + public void fetchAvatar(Account account, final Avatar avatar) { final String KEY = generateFetchKey(account, avatar); synchronized (this.mInProgressAvatarFetches) { if (mInProgressAvatarFetches.add(KEY)) { switch (avatar.origin) { case PEP: this.mInProgressAvatarFetches.add(KEY); - fetchAvatarPep(account, avatar, callback); + fetchAvatarPep(account, avatar, null); break; case VCARD: this.mInProgressAvatarFetches.add(KEY); - fetchAvatarVcard(account, avatar, callback); + fetchAvatarVcard(account, avatar); break; } } else if (avatar.origin == Avatar.Origin.PEP) { @@ -4560,88 +4557,81 @@ public class XmppConnectionService extends Service { }); } - private void fetchAvatarVcard( - final Account account, final Avatar avatar, final UiCallback callback) { - final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket( - account, - packet, - response -> { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (response.getType() == Iq.Type.RESULT) { - Element vCard = response.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " - + avatar.owner - + " omittedPep=" - + previouslyOmittedPepFetch); - if (avatar.owner.isBareJid()) { - if (account.getJid().asBareJid().equals(avatar.owner) - && account.getAvatar() == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": had no avatar. replacing with vcard"); - account.setAvatar(avatar.getFilename()); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } else { - final Contact contact = - account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar, previouslyOmittedPepFetch); - account.getXmppConnection() - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateRosterUi(); - } - updateConversationUi(); - } else { - Conversation conversation = - find(account, avatar.owner.asBareJid()); - if (conversation != null - && conversation.getMode() == Conversation.MODE_MULTI) { - MucOptions.User user = - conversation - .getMucOptions() - .findUserByFullJid(avatar.owner); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - if (user.getRealJid() != null) { - Contact contact = - account.getRoster() - .getContact(user.getRealJid()); - contact.setAvatar(avatar); - account.getXmppConnection() - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateRosterUi(); - } - } - } - } - } + private void fetchAvatarVcard(final Account account, final Avatar avatar) { + final var address = avatar.owner; + final var connection = account.getXmppConnection(); + final var future = connection.getManager(VCardManager.class).retrievePhoto(address); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(byte[] result) { + avatar.image = BaseEncoding.base64().encode(result); + if (fileBackend.save(avatar)) { + setVCardAvatar(account, avatar); } } - }); + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d( + Config.LOGTAG, + "could not retrieve avatar from " + + address + + " (" + + avatar.sha1sum + + ")", + t); + } + }, + MoreExecutors.directExecutor()); + } + + private void setVCardAvatar(final Account account, final Avatar avatar) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully fetched vCard avatar for " + + avatar.owner); + if (avatar.owner.isBareJid()) { + if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); + account.setAvatar(avatar.getFilename()); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); + } else { + final Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); + getAvatarService().clear(contact); + updateRosterUi(); + } + updateConversationUi(); + } else { + Conversation conversation = find(account, avatar.owner.asBareJid()); + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); + if (user != null) { + if (user.setAvatar(avatar)) { + getAvatarService().clear(user); + updateConversationUi(); + updateMucRosterUi(); + } + if (user.getRealJid() != null) { + Contact contact = account.getRoster().getContact(user.getRealJid()); + contact.setAvatar(avatar); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); + getAvatarService().clear(contact); + updateRosterUi(); + } + } + } + } } public void checkForAvatar(final Account account, final UiCallback callback) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index e4761de5fa73ed3332c5e40542708900a2454228..f3c873a90c130d6d51984956dd8e165f6b308bbe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; +import eu.siacs.conversations.xmpp.manager.VCardManager; public class Managers { @@ -50,6 +51,7 @@ public class Managers { .put(PubSubManager.class, new PubSubManager(context, connection)) .put(RosterManager.class, new RosterManager(context, connection)) .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection)) + .put(VCardManager.class, new VCardManager(context, connection)) .build(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 7104ead02a1dcc4105af573f35750abf330590a3..53960dbbd1b7aae1192e6d3eac0a691b70ba8627 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,6 +1,9 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.io.BaseEncoding; +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; @@ -12,6 +15,7 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.ByteContent; import im.conversations.android.xmpp.model.avatar.Data; import im.conversations.android.xmpp.model.avatar.Info; import im.conversations.android.xmpp.model.avatar.Metadata; @@ -26,11 +30,50 @@ public class AvatarManager extends AbstractManager { this.service = service; } + public ListenableFuture fetch(final Jid address, final String itemId) { + final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class); + return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchAndStore(final Avatar avatar) { + final var future = fetch(avatar.owner, avatar.sha1sum); + return Futures.transform( + future, + data -> { + avatar.image = BaseEncoding.base64().encode(data); + if (service.getFileBackend().save(avatar)) { + setPepAvatar(avatar); + return null; + } else { + throw new IllegalStateException("Could not store avatar"); + } + }, + MoreExecutors.directExecutor()); + } + + private void setPepAvatar(final Avatar avatar) { + final var account = getAccount(); + if (account.getJid().asBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + getDatabase().updateAccount(account); + } + this.service.getAvatarService().clear(account); + this.service.updateConversationUi(); + this.service.updateAccountUi(); + } else { + final Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); + this.service.getAvatarService().clear(contact); + this.service.updateConversationUi(); + this.service.updateRosterUi(); + } + } + public void handleItems(final Jid from, final Items items) { final var account = getAccount(); // TODO support retract final var entry = items.getFirstItemWithId(Metadata.class); - Log.d(Config.LOGTAG, "<-- " + entry + " (" + from + ")"); final var avatar = entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); if (avatar == null) { @@ -57,8 +100,25 @@ public class AvatarManager extends AbstractManager { } } } else if (service.isDataSaverDisabled()) { - // TODO use internal mechanism to fetch PEP avatars - service.fetchAvatar(account, avatar); + final var future = this.fetchAndStore(avatar); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully fetched pep avatar for " + + avatar.owner); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not fetch avatar", t); + } + }, + MoreExecutors.directExecutor()); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java new file mode 100644 index 0000000000000000000000000000000000000000..aa2765deb0d29e462ffa013311c94321b3f0ea44 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -0,0 +1,50 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +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.stanza.Iq; +import im.conversations.android.xmpp.model.vcard.VCard; + +public class VCardManager extends AbstractManager { + + public VCardManager(final Context context, final XmppConnection connection) { + super(context, connection); + } + + public ListenableFuture retrieve(final Jid address) { + final var iq = new Iq(Iq.Type.GET, new VCard()); + iq.setTo(address); + return Futures.transform( + this.connection.sendIqPacket(iq), + result -> { + final var vCard = result.getExtension(VCard.class); + if (vCard == null) { + throw new IllegalStateException("Result did not include vCard"); + } + return vCard; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retrievePhoto(final Jid address) { + final var vCardFuture = retrieve(address); + return Futures.transform( + vCardFuture, + vCard -> { + final var photo = vCard.getPhoto(); + if (photo == null) { + throw new IllegalStateException("No photo in vCard"); + } + final var binaryValue = photo.getBinaryValue(); + if (binaryValue == null) { + throw new IllegalStateException("Photo has no binary value"); + } + return binaryValue.asBytes(); + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java index 92adc6831c5346cc15a1d2a5663341e29497e2c5..3ca485341cea54ba7db5d892e3390a6b5d959ce7 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -8,4 +8,8 @@ public class Photo extends Extension { public Photo() { super(Photo.class); } + + public BinaryValue getBinaryValue() { + return this.getExtension(BinaryValue.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java index 20a6949775b4348d1c703d3362e570ff98b4f07c..0851966f022674ab20c23ed879bd298339f71dc2 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -9,4 +9,8 @@ public class VCard extends Extension { public VCard() { super(VCard.class); } + + public Photo getPhoto() { + return this.getExtension(Photo.class); + } } From dbfac018352a63e051b5cf893d4712f0367ac5bb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 19:31:54 +0200 Subject: [PATCH 12/87] fix launcher icon not being centered --- .../res/drawable/ic_launcher_foreground.xml | 16 ++++++------- .../res/drawable/ic_launcher_monochrome.xml | 24 +++++++++---------- .../res/drawable/ic_launcher_foreground.xml | 19 ++++++++------- .../res/drawable/ic_launcher_monochrome.xml | 24 ++++++++++--------- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/conversations/res/drawable/ic_launcher_foreground.xml b/src/conversations/res/drawable/ic_launcher_foreground.xml index 5851e5f2c06f45ed22d3b15f01d9469a05396c1e..75550aecc7f7febdc24514f284a5801ec777e2a7 100644 --- a/src/conversations/res/drawable/ic_launcher_foreground.xml +++ b/src/conversations/res/drawable/ic_launcher_foreground.xml @@ -1,13 +1,13 @@ + android:width="432dp" + android:height="432dp" + android:viewportWidth="732" + android:viewportHeight="732"> + android:translateX="150" + android:translateY="150"> + android:pathData="M216 74.92c-79.3 0.37-144 63.8-144 141.73 0 77.93 64.69 140.8 144 140.43 24.66-0.11 43.7-5.26 65.4-13.25l68.95 27.47c6.87 2.76 14.01-3.47 12.22-10.67l-18.68-75.4c12-21.14 16.11-45.17 16.11-69.92 0-77.93-64.7-140.76-144-140.4Zm-68.25 127.8c8.62-0.04 15.6 6.92 15.6 15.53 0 8.62-6.98 15.64-15.6 15.68-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.64 15.6-15.68Zm68.47-0.31c8.62-0.04 15.6 6.9 15.6 15.53 0 8.61-6.98 15.63-15.6 15.67-8.61 0.04-15.6-6.91-15.6-15.53s6.99-15.64 15.6-15.68Zm68.66-0.32c8.61-0.05 15.6 6.9 15.6 15.53 0 8.61-6.99 15.63-15.6 15.67-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.63 15.6-15.67Z" /> - + \ No newline at end of file diff --git a/src/conversations/res/drawable/ic_launcher_monochrome.xml b/src/conversations/res/drawable/ic_launcher_monochrome.xml index 56895d60519f596e8d611ca7cefe6a2bfbf10f7f..c7927359d29f59dc5c47922862cb3ef35044f486 100644 --- a/src/conversations/res/drawable/ic_launcher_monochrome.xml +++ b/src/conversations/res/drawable/ic_launcher_monochrome.xml @@ -1,13 +1,13 @@ - - - - +android:width="432dp" +android:height="432dp" +android:viewportWidth="732" +android:viewportHeight="732"> + + + + \ No newline at end of file diff --git a/src/quicksy/res/drawable/ic_launcher_foreground.xml b/src/quicksy/res/drawable/ic_launcher_foreground.xml index 276e138e3a52ea0f5db7afff8db9a86b48f4d743..92622e098e178afaee4258b9743a8deddde756dc 100644 --- a/src/quicksy/res/drawable/ic_launcher_foreground.xml +++ b/src/quicksy/res/drawable/ic_launcher_foreground.xml @@ -1,13 +1,16 @@ + android:width="432dp" + android:height="432dp" + android:viewportWidth="732" + android:viewportHeight="732"> + + android:translateX="150" + android:translateY="150"> + android:fillColor="#FFFDFDFD" + android:fillType="evenOdd" + android:pathData="M72 216.65c0-77.93 64.7-141.36 144-141.73 79.3-0.37 144 62.46 144 140.39 0 24.75-4.1 48.78-16.11 69.91l18.68 75.41c1.79 7.2-5.35 13.43-12.22 10.67l-68.95-27.47c-21.7 7.99-40.74 13.14-65.4 13.25-79.31 0.37-144-62.5-144-140.43Zm80.3-27.7c-3.27-3.28-7.7-5.12-12.3-5.12-2.28 0-4.54 0.45-6.65 1.33-2.11 0.88-4.03 2.16-5.64 3.78-1.62 1.62-2.9 3.55-3.77 5.66-0.88 2.12-1.33 4.4-1.33 6.68 0 4.63 1.84 9.07 5.1 12.34 3.26 3.27 7.68 5.11 12.29 5.11 4.6 0 9.03-1.84 12.29-5.11 3.26-3.27 5.1-7.7 5.1-12.34 0-4.63-1.84-9.06-5.1-12.34Zm70.47-3.79c-2.11-0.88-4.37-1.33-6.66-1.33-4.6 0-9.03 1.84-12.29 5.11-3.26 3.28-5.1 7.72-5.1 12.34 0.01 4.63 1.84 9.07 5.1 12.34 3.26 3.27 7.68 5.11 12.3 5.11 4.6 0 9.02-1.84 12.28-5.1 3.26-3.28 5.1-7.72 5.1-12.35 0-2.29-0.45-4.56-1.33-6.68-0.87-2.11-2.15-4.04-3.76-5.66-1.62-1.62-3.53-2.9-5.64-3.78Zm76.42 0c-2.1-0.88-4.37-1.33-6.65-1.33-2.29 0-4.55 0.45-6.66 1.33-2.1 0.88-4.02 2.16-5.64 3.78-1.61 1.62-2.9 3.55-3.76 5.66-0.88 2.12-1.33 4.4-1.33 6.68 0 4.63 1.84 9.07 5.1 12.34 3.26 3.27 7.68 5.11 12.29 5.11 4.6 0 9.03-1.84 12.29-5.1 3.26-3.28 5.1-7.72 5.1-12.35 0-2.29-0.46-4.56-1.33-6.68-0.88-2.11-2.16-4.04-3.77-5.66-1.61-1.62-3.53-2.9-5.64-3.78Zm-151.15 72.78c0-15.6-10.61-28.23-23.7-28.23-7.63 0-21.92 8.06-21.21 26.82 7.07-0.18 15.17 7.74 22.54 14.95 12.58 12.3 22.99 22.48 22.37-13.55Zm181.37-1.41c0.7-18.76-13.59-26.82-21.22-26.82-13.09 0-23.7 12.64-23.7 28.22-0.62 36.03 9.8 25.85 22.38 13.55 7.36-7.2 15.46-15.13 22.54-14.95Zm-98.86-19.82c-4.5-1.9-9.45-2.54-14.34-2.61-4.89-0.08-9.85 0.4-14.41 2.16-4.56 1.75-8.72 4.9-11.05 9.2-1.71 3.2-2.37 6.88-2.37 10.5 0 3.63 0.65 7.22 1.38 10.77l0.44 2.08c0.6 2.85 1.21 5.7 1.55 8.6 0.41 3.6 0.39 7.3-0.57 10.8-1.74 6.34-6.7 11.69-12.88 13.88-6.17 2.19-13.38 1.16-18.7-2.67 15.15 13.18 35.05 20.73 55.1 20.92 20.75 0.2 41.47-7.51 57.07-21.23-5.29 3.96-12.57 5.06-18.8 2.86-6.21-2.21-11.18-7.67-12.82-14.09-0.88-3.41-0.84-7.01-0.4-10.5 0.37-2.86 1-5.66 1.64-8.46l0.43-1.92c0.76-3.44 1.43-6.93 1.5-10.45 0.08-3.53-0.46-7.12-2.03-10.28-2.18-4.39-6.24-7.66-10.74-9.56Z" /> + diff --git a/src/quicksy/res/drawable/ic_launcher_monochrome.xml b/src/quicksy/res/drawable/ic_launcher_monochrome.xml index fefe300d511cfa83af9d7fd28d21a59a80320cda..bb191fb1f687863392ee703a1584542b5d4fa797 100644 --- a/src/quicksy/res/drawable/ic_launcher_monochrome.xml +++ b/src/quicksy/res/drawable/ic_launcher_monochrome.xml @@ -1,13 +1,15 @@ - - - +android:width="432dp" +android:height="432dp" +android:viewportWidth="732" +android:viewportHeight="732"> + + + + From a7df21412db4adbaab7b5687f28d46db315921e8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 May 2025 20:01:17 +0200 Subject: [PATCH 13/87] use fixed drawables in notification icons --- .../res/drawable/ic_app_icon_notification.xml | 10 ++++++---- .../res/drawable/ic_app_icon_notification.xml | 16 +++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/conversations/res/drawable/ic_app_icon_notification.xml b/src/conversations/res/drawable/ic_app_icon_notification.xml index 51ccc1fae5df3e885d759dfcc4c1524610cc20d3..defaebdd58ad9c45df44e6a1fd2a7f7ccf3e4386 100644 --- a/src/conversations/res/drawable/ic_app_icon_notification.xml +++ b/src/conversations/res/drawable/ic_app_icon_notification.xml @@ -1,11 +1,13 @@ - + android:viewportWidth="472" + android:viewportHeight="472"> + + android:pathData="M216 22C106.95 22.53 18 109.75 18 216.9c0 107.15 88.95 193.6 198 193.1 33.91-0.17 60.08-7.25 89.93-18.23l94.8 37.77c9.45 3.79 19.26-4.77 16.81-14.67l-25.7-103.69c16.51-29.05 22.16-62.1 22.16-96.13C414 107.9 325.05 21.5 216 22.01Zm-93.84 175.75c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.84 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Zm94.15-0.44c11.85-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.45 21.55-11.85 0.06-21.46-9.5-21.46-21.35 0-11.85 9.6-21.5 21.46-21.55Zm94.4-0.44c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.85 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Z" /> diff --git a/src/quicksy/res/drawable/ic_app_icon_notification.xml b/src/quicksy/res/drawable/ic_app_icon_notification.xml index d27fa1d7835818412baf5fb08e689a617a6ba1c1..3c71fad17b0831f940b3bff3acec8846363400a9 100644 --- a/src/quicksy/res/drawable/ic_app_icon_notification.xml +++ b/src/quicksy/res/drawable/ic_app_icon_notification.xml @@ -1,11 +1,13 @@ - + android:width="24dp" + android:height="24dp" + android:viewportWidth="472" + android:viewportHeight="472"> + + android:pathData="M216 22c109.05-0.5 198 85.9 198 193.05 0 34.04-5.65 67.08-22.15 96.13l25.69 103.7c2.45 9.89-7.36 18.45-16.81 14.66l-94.8-37.77c-29.85 10.98-56.02 18.06-89.93 18.22-109.05 0.5-198-85.94-198-193.1 0-107.15 88.95-194.37 198-194.88Zm0.29 218.89c-6.72-0.1-13.54 0.55-19.82 2.97-6.27 2.41-11.98 6.72-15.18 12.66-2.36 4.38-3.26 9.44-3.26 14.43 0 4.98 0.88 9.93 1.9 14.8 1 4.88 2.15 9.75 2.72 14.7 0.57 4.95 0.54 10.03-0.78 14.84-2.39 8.71-9.22 16.07-17.7 19.08-8.5 3.01-18.41 1.6-25.73-3.67 20.84 18.13 48.2 28.51 75.78 28.77 28.52 0.27 57.01-10.33 78.47-29.2-7.28 5.45-17.3 6.97-25.85 3.93-8.55-3.03-15.38-10.54-17.63-19.36-1.2-4.7-1.16-9.64-0.55-14.45 0.62-4.81 1.8-9.53 2.84-14.27 1.05-4.73 1.97-9.52 2.08-14.37 0.1-4.85-0.65-9.79-2.8-14.13-3-6.03-8.58-10.53-14.78-13.15-6.2-2.6-13-3.48-19.71-3.58Zm126.47-6.04c-18 0-32.58 17.38-32.58 38.81-1.35 78.53 35.41-2.62 61.75-1.93 0.97-25.8-18.68-36.88-29.17-36.88Zm-252.79 0c-10.5 0-30.14 11.09-29.17 36.88 26.34-0.69 63.1 80.46 61.76 1.93 0-21.43-14.6-38.81-32.59-38.81Zm21.53-63.08c-3.14 0-6.25 0.62-9.15 1.82-2.9 1.21-5.54 2.98-7.76 5.2-2.22 2.24-3.98 4.88-5.18 7.8-1.2 2.9-1.82 6.02-1.82 9.17 0 6.37 2.52 12.47 7 16.97 4.5 4.5 10.57 7.03 16.9 7.03 6.35 0 12.43-2.53 16.9-7.03 4.49-4.5 7-10.6 7-16.97 0.01-6.36-2.5-12.46-6.99-16.96-4.48-4.5-10.56-7.03-16.9-7.03Zm104.66 0c-6.34 0-12.42 2.53-16.9 7.03-4.49 4.5-7 10.6-7 16.96 0 6.37 2.51 12.47 7 16.97 4.48 4.5 10.56 7.03 16.9 7.03 6.33 0 12.41-2.53 16.9-7.03 4.48-4.5 7-10.6 7-16.97 0-3.15-0.62-6.27-1.82-9.18-1.2-2.91-2.96-5.56-5.18-7.78-2.22-2.23-4.86-4-7.76-5.2-2.9-1.21-6-1.83-9.15-1.83Zm105.08 0c-3.14 0-6.25 0.62-9.15 1.82-2.9 1.21-5.53 2.98-7.75 5.2-2.22 2.24-3.98 4.88-5.19 7.8-1.2 2.9-1.81 6.02-1.81 9.17 0 6.37 2.51 12.47 7 16.97 4.48 4.5 10.56 7.03 16.9 7.03 6.34 0 12.42-2.53 16.9-7.03 4.48-4.5 7-10.6 7-16.97 0-3.15-0.62-6.27-1.82-9.18-1.2-2.91-2.96-5.56-5.18-7.78-2.22-2.23-4.85-4-7.75-5.2-2.9-1.21-6.01-1.83-9.15-1.83Z" /> - + \ No newline at end of file From 986e72482dfc60a30177ce0a9262570abac0db3c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 May 2025 10:43:40 +0200 Subject: [PATCH 14/87] move slot requesting to HttpUploadManager --- .../conversations/generator/IqGenerator.java | 33 ----- .../http/HttpUploadConnection.java | 16 ++- .../conversations/http/SlotRequester.java | 126 ------------------ .../eu/siacs/conversations/xmpp/Managers.java | 2 + .../conversations/xmpp/XmppConnection.java | 29 +--- .../transports/SocksByteStreamsTransport.java | 11 +- .../xmpp/manager/DiscoManager.java | 11 ++ .../xmpp/manager/HttpUploadManager.java | 108 +++++++++++++++ .../xmpp/manager/VCardManager.java | 6 +- .../android/xmpp/model/upload/Put.java | 22 +++ 10 files changed, 168 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/http/SlotRequester.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 1ea59ba83c98f0aac81cf46e11766e811e82ab95..f48e2023d436a35b3a05476913bf89b408f85dbe 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -7,7 +7,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; @@ -16,14 +15,11 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.stanza.Iq; -import im.conversations.android.xmpp.model.upload.Request; -import java.nio.ByteBuffer; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.UUID; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; @@ -262,35 +258,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq requestHttpUploadSlot( - final Jid host, final DownloadableFile file, final String mime) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(host); - final var request = packet.addExtension(new Request()); - request.setFilename(convertFilename(file.getName())); - request.setSize(file.getExpectedSize()); - return packet; - } - - private static String convertFilename(String name) { - int pos = name.indexOf('.'); - if (pos != -1) { - try { - UUID uuid = UUID.fromString(name.substring(0, pos)); - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - return Base64.encodeToString( - bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) - + name.substring(pos); - } catch (Exception e) { - return name; - } - } else { - return name; - } - } - public static Iq generateCreateAccountWithCaptcha( final Account account, final String id, final Data data) { final Iq register = new Iq(Iq.Type.SET); diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 9154c2c422783ed65d10cec2451db1c91220d94c..f476636dc25617b5eb4f8b0a5574874dc9166abc 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -17,6 +17,7 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.manager.HttpUploadManager; import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -39,12 +40,12 @@ public class HttpUploadConnection private boolean delayed = false; private DownloadableFile file; private final Message message; - private SlotRequester.Slot slot; + private HttpUploadManager.Slot slot; private byte[] key = null; private long transmitted = 0; private Call mostRecentCall; - private ListenableFuture slotFuture; + private ListenableFuture slotFuture; public HttpUploadConnection( final Message message, final HttpConnectionManager httpConnectionManager) { @@ -78,7 +79,7 @@ public class HttpUploadConnection @Override public void cancel() { - final ListenableFuture slotFuture = this.slotFuture; + final ListenableFuture slotFuture = this.slotFuture; if (slotFuture != null && !slotFuture.isDone()) { if (slotFuture.cancel(true)) { Log.d(Config.LOGTAG, "cancelled slot requester"); @@ -94,7 +95,7 @@ public class HttpUploadConnection private void fail(String errorMessage) { finish(); final Call call = this.mostRecentCall; - final Future slotFuture = this.slotFuture; + final Future slotFuture = this.slotFuture; final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled()); @@ -109,8 +110,9 @@ public class HttpUploadConnection message.setTransferable(null); } - public void init(boolean delay) { + public void init(final boolean delay) { final Account account = message.getConversation().getAccount(); + final var connection = account.getXmppConnection(); this.file = mXmppConnectionService.getFileBackend().getFile(message, false); final String mime; if (message.getEncryption() == Message.ENCRYPTION_PGP @@ -129,12 +131,12 @@ public class HttpUploadConnection } this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); message.resetFileParams(); - this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime); + this.slotFuture = connection.getManager(HttpUploadManager.class).request(file, mime); Futures.addCallback( this.slotFuture, new FutureCallback<>() { @Override - public void onSuccess(@Nullable SlotRequester.Slot result) { + public void onSuccess(@Nullable HttpUploadManager.Slot result) { HttpUploadConnection.this.slot = result; try { HttpUploadConnection.this.upload(); diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java deleted file mode 100644 index 5bb5d7772b253857e7cc8eab81c60c4aedb9d1c2..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.http; - -import android.util.Log; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -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.DownloadableFile; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import im.conversations.android.xmpp.model.stanza.Iq; -import im.conversations.android.xmpp.model.upload.Header; -import im.conversations.android.xmpp.model.upload.Slot; -import java.util.Map; -import okhttp3.Headers; -import okhttp3.HttpUrl; - -public class SlotRequester { - - private final XmppConnectionService service; - - public SlotRequester(XmppConnectionService service) { - this.service = service; - } - - public ListenableFuture request( - final Account account, final DownloadableFile file, final String mime) { - final var result = - account.getXmppConnection() - .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD); - if (result == null) { - return Futures.immediateFailedFuture( - new IllegalStateException("No HTTP upload host found")); - } - return requestHttpUpload(account, result.getKey(), file, mime); - } - - private ListenableFuture requestHttpUpload( - final Account account, final Jid host, final DownloadableFile file, final String mime) { - final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); - final var iqFuture = service.sendIqPacket(account, request); - return Futures.transform( - iqFuture, - response -> { - final var slot = - response.getExtension( - im.conversations.android.xmpp.model.upload.Slot.class); - if (slot == null) { - Log.d(Config.LOGTAG, "-->" + response); - throw new IllegalStateException("Slot not found in IQ response"); - } - final var getUrl = slot.getGetUrl(); - final var put = slot.getPut(); - if (getUrl == null || put == null) { - throw new IllegalStateException("Missing get or put in slot response"); - } - final var putUrl = put.getUrl(); - if (putUrl == null) { - throw new IllegalStateException("Missing put url"); - } - final var headers = new ImmutableMap.Builder(); - for (final Header header : put.getHeaders()) { - final String name = header.getHeaderName(); - final String value = header.getContent(); - if (Strings.isNullOrEmpty(value) || value.contains("\n")) { - continue; - } - headers.put(name, value.trim()); - } - headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); - return new Slot(putUrl, getUrl, headers.buildKeepingLast()); - }, - MoreExecutors.directExecutor()); - } - - public static class Slot { - public final HttpUrl put; - public final HttpUrl get; - public final Headers headers; - - private Slot(HttpUrl put, HttpUrl get, Headers headers) { - this.put = put; - this.get = get; - this.headers = headers; - } - - private Slot(HttpUrl put, HttpUrl getUrl, Map headers) { - this.put = put; - this.get = getUrl; - this.headers = Headers.of(headers); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index f3c873a90c130d6d51984956dd8e165f6b308bbe..116427f371750c5a8909c8dff35e200678c7c1a1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -11,6 +11,7 @@ import eu.siacs.conversations.xmpp.manager.BookmarkManager; import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.EntityTimeManager; +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.NickManager; @@ -39,6 +40,7 @@ public class Managers { .put(CarbonsManager.class, new CarbonsManager(context, connection)) .put(DiscoManager.class, new DiscoManager(context, connection)) .put(EntityTimeManager.class, new EntityTimeManager(context, connection)) + .put(HttpUploadManager.class, new HttpUploadManager(context, connection)) .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection)) .put( MessageDisplayedSynchronizationManager.class, diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 9cd3f96409e521757219dc1462912a6078e8fdb0..d932461384db3339d5ab15c499f1b036a93c0dd3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2730,29 +2730,6 @@ public class XmppConnection implements Runnable { return this.managers.getInstance(clazz); } - private List> findDiscoItemsByFeature(final String feature) { - final List> items = new ArrayList<>(); - for (final Entry cursor : - getManager(DiscoManager.class).getServerItems().entrySet()) { - if (cursor.getValue().getFeatureStrings().contains(feature)) { - items.add(cursor); - } - } - return items; - } - - public Entry getServiceDiscoveryResultByFeature(final String feature) { - return Iterables.getFirst(findDiscoItemsByFeature(feature), null); - } - - public Jid findDiscoItemByFeature(final String feature) { - final var items = findDiscoItemsByFeature(feature); - if (items.isEmpty()) { - return null; - } - return Iterables.getFirst(items, null).getKey(); - } - public List getMucServersWithholdAccount() { final List servers = getMucServers(); servers.remove(account.getDomain().toString()); @@ -3207,7 +3184,8 @@ public class XmppConnection implements Runnable { if (Config.DISABLE_HTTP_UPLOAD) { return false; } - final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD); + final var result = + getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); if (result == null) { return false; } @@ -3238,7 +3216,8 @@ public class XmppConnection implements Runnable { } public long getMaxHttpUploadSize() { - final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD); + final var result = + getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); if (result == null) { return -1; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java index a0e778d5f93839c8848a00af8d57e5c52b3e73cc..ef89de61f18bab68416f45babf2fb44ee421aa43 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -29,6 +29,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils; import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.stanza.Iq; import java.io.IOException; import java.io.InputStream; @@ -306,14 +307,18 @@ public class SocksByteStreamsTransport implements Transport { return Futures.immediateFailedFuture( new IllegalStateException("Proxy look up is disabled")); } - final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS); + final var streamer = + xmppConnection + .getManager(DiscoManager.class) + .findDiscoItemByFeature(Namespace.BYTE_STREAMS); if (streamer == null) { return Futures.immediateFailedFuture( new IllegalStateException("No proxy/streamer found")); } final Iq iqRequest = new Iq(Iq.Type.GET); - iqRequest.setTo(streamer); + iqRequest.setTo(streamer.getKey()); // TODO urgent refactor to extension + // TODO and maybe move to Manager iqRequest.query(Namespace.BYTE_STREAMS); final SettableFuture candidateFuture = SettableFuture.create(); xmppConnection.sendIqPacket( @@ -342,7 +347,7 @@ public class SocksByteStreamsTransport implements Transport { new Candidate( UUID.randomUUID().toString(), host, - streamer, + streamer.getKey(), port, 655360 + (initiator ? 0 : 15), CandidateType.PROXY)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index 526b43b42228af8d60db2612f29967f7250e694f..20583c93cf30d69e919bea1b651e08c310ee71eb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -7,6 +7,8 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -467,6 +469,15 @@ public class DiscoManager extends AbstractManager { } } + public Map findDiscoItemsByFeature(final String feature) { + return Maps.filterValues(getServerItems(), v -> v.hasFeature(feature)); + } + + public Map.Entry findDiscoItemByFeature(final String feature) { + final var items = findDiscoItemsByFeature(feature); + return Iterables.getFirst(items.entrySet(), null); + } + public static final class CapsHashMismatchException extends IllegalStateException { public CapsHashMismatchException(final String message) { super(message); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java new file mode 100644 index 0000000000000000000000000000000000000000..13c26b77a02b88a0020c168d1bb9f0ccd121761d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java @@ -0,0 +1,108 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import android.util.Base64; +import com.google.common.collect.ImmutableMap; +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.entities.DownloadableFile; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.upload.Request; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.UUID; +import okhttp3.Headers; +import okhttp3.HttpUrl; + +public class HttpUploadManager extends AbstractManager { + + public HttpUploadManager(final Context context, final XmppConnection connection) { + super(context, connection); + } + + public ListenableFuture request(final DownloadableFile file, final String mime) { + final var result = + getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); + if (result == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("No HTTP upload host found")); + } + return requestHttpUpload(result.getKey(), file, mime); + } + + private ListenableFuture requestHttpUpload( + final Jid host, final DownloadableFile file, final String mime) { + final Iq iq = new Iq(Iq.Type.GET); + iq.setTo(host); + final var request = iq.addExtension(new Request()); + request.setFilename(convertFilename(file.getName())); + request.setSize(file.getExpectedSize()); + request.setContentType(mime); + final var iqFuture = this.connection.sendIqPacket(iq); + return Futures.transform( + iqFuture, + response -> { + final var slot = + response.getExtension( + im.conversations.android.xmpp.model.upload.Slot.class); + if (slot == null) { + throw new IllegalStateException("Slot not found in IQ response"); + } + final var getUrl = slot.getGetUrl(); + final var put = slot.getPut(); + if (getUrl == null || put == null) { + throw new IllegalStateException("Missing get or put in slot response"); + } + final var putUrl = put.getUrl(); + if (putUrl == null) { + throw new IllegalStateException("Missing put url"); + } + final var contentType = mime == null ? "application/octet-stream" : mime; + final var headers = + new ImmutableMap.Builder() + .putAll(put.getHeadersAllowList()) + .put("Content-Type", contentType) + .buildKeepingLast(); + return new Slot(putUrl, getUrl, headers); + }, + MoreExecutors.directExecutor()); + } + + private static String convertFilename(final String name) { + int pos = name.indexOf('.'); + if (pos < 0) { + return name; + } + try { + UUID uuid = UUID.fromString(name.substring(0, pos)); + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return Base64.encodeToString( + bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) + + name.substring(pos); + } catch (final Exception e) { + return name; + } + } + + public static class Slot { + public final HttpUrl put; + public final HttpUrl get; + public final Headers headers; + + private Slot(final HttpUrl put, final HttpUrl get, final Headers headers) { + this.put = put; + this.get = get; + this.headers = headers; + } + + private Slot(final HttpUrl put, final HttpUrl get, final Map headers) { + this(put, get, Headers.of(headers)); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java index aa2765deb0d29e462ffa013311c94321b3f0ea44..a2d5873664a2d3ca75b386fc262ad2cd1d11e41c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -37,11 +37,13 @@ public class VCardManager extends AbstractManager { vCard -> { final var photo = vCard.getPhoto(); if (photo == null) { - throw new IllegalStateException("No photo in vCard"); + throw new IllegalStateException( + String.format("No photo in vCard of %s", address)); } final var binaryValue = photo.getBinaryValue(); if (binaryValue == null) { - throw new IllegalStateException("Photo has no binary value"); + throw new IllegalStateException( + String.format("Photo has no binary value in vCard of %s", address)); } return binaryValue.asBytes(); }, diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Put.java b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java index 1b52a495c551b4acb34778f09c45aff11999ed12..30913b039a70c5a4ac09b98f22477bf6d5b70891 100644 --- a/src/main/java/im/conversations/android/xmpp/model/upload/Put.java +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java @@ -1,14 +1,21 @@ package im.conversations.android.xmpp.model.upload; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Map; import okhttp3.HttpUrl; @XmlElement public class Put extends Extension { + private static final List HEADER_ALLOW_LIST = + Arrays.asList("Authorization", "Cookie", "Expires"); + public Put() { super(Put.class); } @@ -24,4 +31,19 @@ public class Put extends Extension { public Collection
getHeaders() { return this.getExtensions(Header.class); } + + public Map getHeadersAllowList() { + final var headers = new ImmutableMap.Builder(); + for (final Header header : this.getHeaders()) { + final String name = header.getHeaderName(); + final String value = Strings.nullToEmpty(header.getContent()).trim(); + if (Strings.isNullOrEmpty(value) || value.contains("\n")) { + continue; + } + if (HEADER_ALLOW_LIST.contains(name)) { + headers.put(name, value); + } + } + return headers.buildKeepingLast(); + } } From b7711c0bd97e712da8fa50507ba3eaca72af6c1b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 May 2025 11:39:40 +0200 Subject: [PATCH 15/87] delete avatar via managers --- .../services/XmppConnectionService.java | 98 ++++++------------- .../xmpp/manager/AvatarManager.java | 10 ++ .../xmpp/manager/VCardManager.java | 29 ++++++ 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 69276d9512e1cf91f2a0ae337e4287f80b423aab..4f2def45574c7d051be8c70237240d4f4a55ca2e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -3551,27 +3551,40 @@ public class XmppConnectionService extends Service { } public void deleteAvatar(final Account account) { - final AtomicBoolean executed = new AtomicBoolean(false); - final Runnable onDeleted = - () -> { - if (executed.compareAndSet(false, true)) { + final var connection = account.getXmppConnection(); + + final var vCardPhotoDeletionFuture = + connection.getManager(VCardManager.class).deletePhoto(); + final var pepDeletionFuture = connection.getManager(AvatarManager.class).delete(); + + final var deletionFuture = Futures.allAsList(vCardPhotoDeletionFuture, pepDeletionFuture); + + Futures.addCallback( + deletionFuture, + new FutureCallback<>() { + @Override + public void onSuccess(List result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": deleted avatar from server"); account.setAvatar(null); databaseBackend.updateAccount(account); getAvatarService().clear(account); updateAccountUi(); } - }; - // TODO execute this via the respective Managers - deleteVcardAvatar(account, onDeleted); - deletePepNode(account, Namespace.AVATAR_DATA); - deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted); - } - public void deletePepNode(final Account account, final String node) { - deletePepNode(account, node, null); + @Override + public void onFailure(Throwable t) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not delete avatar", + t); + } + }, + MoreExecutors.directExecutor()); } - private void deletePepNode(final Account account, final String node, final Runnable runnable) { + public void deletePepNode(final Account account, final String node) { final Iq request = mIqGenerator.deleteNode(node); sendIqPacket( account, @@ -3583,9 +3596,6 @@ public class XmppConnectionService extends Service { account.getJid().asBareJid() + ": successfully deleted pep node " + node); - if (runnable != null) { - runnable.run(); - } } else { Log.d( Config.LOGTAG, @@ -3594,53 +3604,6 @@ public class XmppConnectionService extends Service { }); } - private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { - final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); - sendIqPacket( - account, - retrieveVcard, - (response) -> { - if (response.getType() != Iq.Type.RESULT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": no vCard set. nothing to do"); - return; - } - final Element vcard = response.findChild("vCard", "vcard-temp"); - if (vcard == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": no vCard set. nothing to do"); - return; - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - final Iq publication = new Iq(Iq.Type.SET); - publication.setTo(account.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket( - account, - publication, - (publicationResponse) -> { - if (publicationResponse.getType() == Iq.Type.RESULT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully deleted vcard avatar"); - runnable.run(); - } else { - Log.d( - Config.LOGTAG, - "failed to publish vcard " - + publicationResponse.getErrorCondition()); - } - }); - }); - } - private boolean hasEnabledAccounts() { if (this.accounts == null) { return false; @@ -4576,12 +4539,9 @@ public class XmppConnectionService extends Service { public void onFailure(@NonNull Throwable t) { Log.d( Config.LOGTAG, - "could not retrieve avatar from " - + address - + " (" - + avatar.sha1sum - + ")", - t); + account.getJid().asBareJid() + + ": could not retrieve vCard avatar of " + + avatar.owner); } }, MoreExecutors.directExecutor()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 53960dbbd1b7aae1192e6d3eac0a691b70ba8627..1f3660a10e3669749880360346b089a7cdd20444 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -159,4 +159,14 @@ public class AvatarManager extends AbstractManager { public boolean hasPepToVCardConversion() { return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION); } + + public ListenableFuture delete() { + final var pepManager = getManager(PepManager.class); + final var deleteMetaDataFuture = pepManager.delete(Namespace.AVATAR_METADATA); + final var deleteDataFuture = pepManager.delete(Namespace.AVATAR_DATA); + return Futures.transform( + Futures.allAsList(deleteDataFuture, deleteMetaDataFuture), + vs -> null, + MoreExecutors.directExecutor()); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java index a2d5873664a2d3ca75b386fc262ad2cd1d11e41c..76f8c6fdb724d1ca4e1a75c7a80e304dbf5a3705 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -1,13 +1,16 @@ package eu.siacs.conversations.xmpp.manager; import android.content.Context; +import android.util.Log; 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.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.vcard.VCard; +import java.util.Objects; public class VCardManager extends AbstractManager { @@ -49,4 +52,30 @@ public class VCardManager extends AbstractManager { }, MoreExecutors.directExecutor()); } + + public ListenableFuture publish(final VCard vCard) { + final var iq = new Iq(Iq.Type.SET, vCard); + iq.setTo(getAccount().getJid().asBareJid()); + return Futures.transform( + connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); + } + + public ListenableFuture deletePhoto() { + final var vCardFuture = retrieve(getAccount().getJid().asBareJid()); + return Futures.transformAsync( + vCardFuture, + vCard -> { + final var photo = vCard.getPhoto(); + if (photo == null) { + return Futures.immediateFuture(null); + } + Log.d( + Config.LOGTAG, + "deleting photo from vCard. binaryValue=" + + Objects.nonNull(photo.getBinaryValue())); + photo.clearChildren(); + return publish(vCard); + }, + MoreExecutors.directExecutor()); + } } From e23176867951e479768971c0dc9ff30601cc66e0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 24 May 2025 13:56:34 +0200 Subject: [PATCH 16/87] publish muc avatar via new APIs --- .../conversations/generator/IqGenerator.java | 7 -- .../conversations/parser/PresenceParser.java | 1 - .../persistance/FileBackend.java | 2 +- .../services/XmppConnectionService.java | 76 ++++++++----------- .../xmpp/manager/VCardManager.java | 48 +++++++++++- .../android/xmpp/model/Extension.java | 10 +++ .../android/xmpp/model/vcard/Photo.java | 5 ++ .../android/xmpp/model/vcard/Type.java | 12 +++ 8 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/vcard/Type.java diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index f48e2023d436a35b3a05476913bf89b408f85dbe..512248c06734289970afb04b1b006fefc00b8680 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -87,13 +87,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq retrieveVcardAvatar(final Jid to) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(to); - packet.addChild("vCard", "vcard-temp"); - return packet; - } - public Iq retrieveAvatarMetaData(final Jid to) { final Iq packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 95f232d2f244ef0aa1305665de484fc58b210543..d5ebbf1deacaaf2f5040b2075851dc4674bd3a74 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -491,7 +491,6 @@ public class PresenceParser extends AbstractParser @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { - // Log.d(Config.LOGTAG,"<--"+packet); if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2b95a1cf8de62a2661cfd72015f32eb8e65d4ac9..a261afb7fd6f63c99f7242132bf3ede67fbc73d8 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1220,7 +1220,7 @@ public class FileBackend { try { ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); Base64OutputStream mBase64OutputStream = - new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT | Base64.NO_WRAP); MessageDigest digest = MessageDigest.getInstance("SHA-1"); DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 4f2def45574c7d051be8c70237240d4f4a55ca2e..94a19b172791f1c1e259f3c2b9823b1f520ce5bd 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4366,53 +4366,39 @@ public class XmppConnectionService extends Service { } private void publishMucAvatar( - Conversation conversation, Avatar avatar, OnAvatarPublication callback) { + final Conversation conversation, + final Avatar avatar, + final OnAvatarPublication callback) { final var account = conversation.getAccount(); - final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar); - sendIqPacket( - account, - retrieve, - (response) -> { - boolean itemNotFound = - response.getType() == Iq.Type.ERROR - && response.hasChild("error") - && response.findChild("error").hasChild("item-not-found"); - if (response.getType() == Iq.Type.RESULT || itemNotFound) { - Element vcard = response.findChild("vCard", "vcard-temp"); - if (vcard == null) { - vcard = new Element("vCard", "vcard-temp"); - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - photo.addChild("TYPE").setContent(avatar.type); - photo.addChild("BINVAL").setContent(avatar.image); - final Iq publication = new Iq(Iq.Type.SET); - publication.setTo(conversation.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket( - account, - publication, - (publicationResponse) -> { - if (publicationResponse.getType() == Iq.Type.RESULT) { - callback.onAvatarPublicationSucceeded(); - } else { - Log.d( - Config.LOGTAG, - "failed to publish vcard " - + publicationResponse.getErrorCondition()); - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - }); - } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response); + final var connection = account.getXmppConnection(); + final var future = + connection + .getManager(VCardManager.class) + .publishPhoto( + avatar.owner, + avatar.type, + BaseEncoding.base64().decode(avatar.image)); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, "published muc avatar"); + final var c = account.getRoster().getContact(avatar.owner); + c.setAvatar(avatar); + getAvatarService().clear(c); + getAvatarService().clear(conversation.getMucOptions()); + callback.onAvatarPublicationSucceeded(); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not publish muc avatar", t); callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_no_server_support); + R.string.error_publish_avatar_server_reject); } - }); + }, + MoreExecutors.directExecutor()); } public void cancelAvatarFetches(final Account account) { @@ -4563,6 +4549,8 @@ public class XmppConnectionService extends Service { getAvatarService().clear(account); updateAccountUi(); } else { + // TODO if this is a MUC clear MucOptions too + // TODO do the same clearing for when setting a cached version final Contact contact = account.getRoster().getContact(avatar.owner); contact.setAvatar(avatar); account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java index 76f8c6fdb724d1ca4e1a75c7a80e304dbf5a3705..05ab21a94fad9d9cb3d192c11e826d7d120426cd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -8,7 +8,11 @@ import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.vcard.BinaryValue; +import im.conversations.android.xmpp.model.vcard.Photo; import im.conversations.android.xmpp.model.vcard.VCard; import java.util.Objects; @@ -54,8 +58,12 @@ public class VCardManager extends AbstractManager { } public ListenableFuture publish(final VCard vCard) { + return publish(getAccount().getJid().asBareJid(), vCard); + } + + public ListenableFuture publish(final Jid address, final VCard vCard) { final var iq = new Iq(Iq.Type.SET, vCard); - iq.setTo(getAccount().getJid().asBareJid()); + iq.setTo(address); return Futures.transform( connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor()); } @@ -78,4 +86,42 @@ public class VCardManager extends AbstractManager { }, MoreExecutors.directExecutor()); } + + public ListenableFuture publishPhoto( + final Jid address, final String type, final byte[] image) { + final var retrieveFuture = this.retrieve(address); + + final var caughtFuture = + Futures.catchingAsync( + retrieveFuture, + IqErrorException.class, + ex -> { + final var error = ex.getError(); + if (error != null + && error.getCondition() instanceof Condition.ItemNotFound) { + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(ex); + } + }, + MoreExecutors.directExecutor()); + + return Futures.transformAsync( + caughtFuture, + existing -> { + final VCard vCard; + if (existing == null) { + Log.d(Config.LOGTAG, "item-not-found. created fresh vCard"); + vCard = new VCard(); + } else { + vCard = existing; + } + final var photo = new Photo(); + photo.setType(type); + photo.addExtension(new BinaryValue()).setContent(image); + vCard.setExtension(photo); + return publish(address, vCard); + }, + MoreExecutors.directExecutor()); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/Extension.java b/src/main/java/im/conversations/android/xmpp/model/Extension.java index 5299b353e23ab554f87c2a4add562a26c77aeea0..8f061e47780b33c6a1b719df9d7b6cfaf10bd857 100644 --- a/src/main/java/im/conversations/android/xmpp/model/Extension.java +++ b/src/main/java/im/conversations/android/xmpp/model/Extension.java @@ -59,6 +59,16 @@ public class Extension extends Element { return child; } + public T setExtension(T child) { + final var iterator = this.children.iterator(); + while (iterator.hasNext()) { + if (iterator.next().getClass().isInstance(child)) { + iterator.remove(); + } + } + return this.addExtension(child); + } + public void addExtensions(final Collection extensions) { for (final Extension extension : extensions) { addExtension(extension); diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java index 3ca485341cea54ba7db5d892e3390a6b5d959ce7..79f8a0a1ff0c7eba8523d72797f7b6971ecda15c 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -12,4 +12,9 @@ public class Photo extends Extension { public BinaryValue getBinaryValue() { return this.getExtension(BinaryValue.class); } + + public void setType(final String value) { + final var type = this.addExtension(new Type()); + type.setContent(value); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Type.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Type.java new file mode 100644 index 0000000000000000000000000000000000000000..c1c903b7ac1343b29a1f59522faa0fe6e0311e5e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Type.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "TYPE") +public class Type extends Extension { + + public Type() { + super(Type.class); + } +} From 0c52e3db306f90652654e1b5137a812ffa4226d5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 27 May 2025 13:03:45 +0200 Subject: [PATCH 17/87] upload large avatar via AvatarManager/HttpUploadManager --- build.gradle | 9 +- src/main/AndroidManifest.xml | 2 + .../java/eu/siacs/conversations/Config.java | 2 + .../http/HttpConnectionManager.java | 2 +- .../persistance/FileBackend.java | 42 +-- .../services/XmppConnectionService.java | 53 ++-- .../conversations/utils/Compatibility.java | 4 + .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/manager/AvatarManager.java | 261 +++++++++++++++++- .../xmpp/manager/HttpUploadManager.java | 99 ++++++- .../siacs/conversations/xmpp/pep/Avatar.java | 20 ++ .../android/xmpp/model/avatar/Info.java | 28 +- .../xmpp/model/upload/purpose/Ephemeral.java | 11 + .../xmpp/model/upload/purpose/Message.java | 11 + .../xmpp/model/upload/purpose/Permanent.java | 11 + .../xmpp/model/upload/purpose/Profile.java | 11 + .../xmpp/model/upload/purpose/Purpose.java | 10 + .../model/upload/purpose/package-info.java | 5 + 18 files changed, 510 insertions(+), 72 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/Ephemeral.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/Message.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/Permanent.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/Profile.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/Purpose.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/upload/purpose/package-info.java diff --git a/build.gradle b/build.gradle index 4de6bf686a763d81bb5992abc78577e23e1a5838..7772b7f795cae82db5ed12648e51ad431ce41e25 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.9.1' + classpath 'com.android.tools.build:gradle:8.9.3' classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2" } } @@ -51,14 +51,15 @@ dependencies { implementation 'androidx.emoji2:emoji2:1.5.0' freeImplementation 'androidx.emoji2:emoji2-bundled:1.5.0' implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0' - implementation 'androidx.exifinterface:exifinterface:1.4.0' + implementation 'androidx.exifinterface:exifinterface:1.4.1' + implementation 'androidx.heifwriter:heifwriter:1.1.0-beta01' implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.sharetarget:sharetarget:1.2.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.viewpager:viewpager:1.1.0' - implementation 'androidx.work:work-runtime:2.10.0' + implementation 'androidx.work:work-runtime:2.10.1' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' - implementation 'com.google.android.material:material:1.13.0-alpha12' + implementation 'com.google.android.material:material:1.13.0-alpha13' implementation 'com.google.guava:guava:33.4.6-android' implementation 'com.google.zxing:core:3.5.3' implementation 'com.leinardi.android:speed-dial:3.3.0' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9ad0b4b364d3197a1399dc7334a9a468d5e6014a..cee52eaf8d1b693dc43468f3233eaee5b46e581c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -60,6 +60,8 @@ android:name="android.hardware.microphone" android:required="false" /> + + diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 571c02c348de4da5bcac6dea2c1e0a52323f47a0..388abd1e36ed7f184254ce92f8b17e4ebe7f1a92 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -79,6 +79,7 @@ public final class Config { // and webp public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG; + public static final int AVATAR_FULL_SIZE = 1024; public static final int AVATAR_CHAR_LIMIT = 9400; public static final int IMAGE_SIZE = 1920; @@ -131,6 +132,7 @@ public final class Config { false; // require a/v calls to be verified with OMEMO public static final boolean JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK = false; public static final boolean JINGLE_MESSAGE_INIT_STRICT_DEVICE_TIMEOUT = false; + // TODO extend this to 12s public static final long DEVICE_DISCOVERY_TIMEOUT = 6000; // in milliseconds public static final boolean ONLY_INTERNAL_STORAGE = diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 1af4ba48bd2fe4e17a889e563c9868c86294464d..533cf0c11915e290463fd87fd7352dd292857fcf 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -136,7 +136,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { return buildHttpClient(url, account, 30, interactive); } - OkHttpClient buildHttpClient( + public OkHttpClient buildHttpClient( final HttpUrl url, final Account account, int readTimeout, boolean interactive) { final String slotHostname = url.host(); final boolean onionSlot = slotHostname.endsWith(".onion"); diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index a261afb7fd6f63c99f7242132bf3ede67fbc73d8..e8a2cc2caf55072e098e8e01675f3c7df310e1d4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -773,7 +773,7 @@ public class FileBackend { throw new ImageCompressionException("Source file had alpha channel"); } Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); - final int rotation = getRotation(image); + final int rotation = getRotation(mXmppConnectionService, image); scaledBitmap = rotate(scaledBitmap, rotation); boolean targetSizeReached = false; int quality = Config.IMAGE_QUALITY; @@ -930,9 +930,8 @@ public class FileBackend { } } - private int getRotation(final Uri image) { - try (final InputStream is = - mXmppConnectionService.getContentResolver().openInputStream(image)) { + private static int getRotation(final Context context, final Uri image) { + try (final InputStream is = context.getContentResolver().openInputStream(image)) { return is == null ? 0 : getRotation(is); } catch (final Exception e) { return 0; @@ -1123,7 +1122,6 @@ public class FileBackend { } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap cropCenterSquarePdf(final Uri uri, final int size) { try { ParcelFileDescriptor fileDescriptor = @@ -1137,7 +1135,6 @@ public class FileBackend { } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap renderPdfDocument( ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException { final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor); @@ -1173,7 +1170,8 @@ public class FileBackend { return getUriForFile(mXmppConnectionService, file); } - public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + public Avatar getPepAvatar( + final Uri image, final int size, final Bitmap.CompressFormat format) { final Avatar uncompressAvatar = getUncompressedAvatar(image); if (uncompressAvatar != null @@ -1261,6 +1259,7 @@ public class FileBackend { } } + // this was used by republishAvatarIfNeeded() public Avatar getStoredPepAvatar(String hash) { if (hash == null) { return null; @@ -1385,8 +1384,12 @@ public class FileBackend { return new File(mXmppConnectionService.getFilesDir(), "/avatars/"); } - private File getAvatarFile(String avatar) { - return new File(mXmppConnectionService.getCacheDir(), "/avatars/" + avatar); + public File getAvatarFile(final String avatar) { + return getAvatarFile(mXmppConnectionService, avatar); + } + + public static File getAvatarFile(Context context, final String avatar) { + return new File(context.getCacheDir(), "/avatars/" + avatar); } public Uri getAvatarUri(String avatar) { @@ -1394,18 +1397,21 @@ public class FileBackend { } public Bitmap cropCenterSquare(final Uri image, final int size) { + return cropCenterSquare(mXmppConnectionService, image, size); + } + + public static Bitmap cropCenterSquare(final Context context, final Uri image, final int size) { if (image == null) { return null; } final BitmapFactory.Options options = new BitmapFactory.Options(); try { - options.inSampleSize = calcSampleSize(image, size); + options.inSampleSize = calcSampleSize(context, image, size); } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to calculate sample size for " + image, e); return null; } - try (final InputStream is = - mXmppConnectionService.getContentResolver().openInputStream(image)) { + try (final InputStream is = context.getContentResolver().openInputStream(image)) { if (is == null) { return null; } @@ -1413,7 +1419,7 @@ public class FileBackend { if (originalBitmap == null) { return null; } else { - final var bitmap = rotate(originalBitmap, getRotation(image)); + final var bitmap = rotate(originalBitmap, getRotation(context, image)); return cropCenterSquare(bitmap, size); } } catch (final SecurityException | IOException e) { @@ -1465,7 +1471,7 @@ public class FileBackend { } } - public Bitmap cropCenterSquare(Bitmap input, int size) { + public static Bitmap cropCenterSquare(Bitmap input, int size) { int w = input.getWidth(); int h = input.getHeight(); @@ -1487,10 +1493,14 @@ public class FileBackend { } private int calcSampleSize(final Uri image, int size) throws IOException, SecurityException { + return calcSampleSize(mXmppConnectionService, image, size); + } + + private static int calcSampleSize(final Context context, final Uri image, int size) + throws IOException, SecurityException { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - try (final InputStream inputStream = - mXmppConnectionService.getContentResolver().openInputStream(image)) { + try (final InputStream inputStream = context.getContentResolver().openInputStream(image)) { BitmapFactory.decodeStream(inputStream, null, options); return calcSampleSize(options, size); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 94a19b172791f1c1e259f3c2b9823b1f520ce5bd..288afe6809fdb953f567aaa56e48a177b1aa22ff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4329,40 +4329,29 @@ public class XmppConnectionService extends Service { final Uri image, final boolean open, final OnAvatarPublication callback) { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar == null) { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); - return; - } - if (fileBackend.save(avatar)) { - final var connection = account.getXmppConnection(); - final var future = connection.getManager(AvatarManager.class).publish(avatar, open); - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - callback.onAvatarPublicationSucceeded(); - } - @Override - public void onFailure(@NonNull Throwable t) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not publish avatar", - t); - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - }, - MoreExecutors.directExecutor()); + final var connection = account.getXmppConnection(); + final var publicationFuture = + connection.getManager(AvatarManager.class).uploadAndPublish(image, open); - } else { - Log.d(Config.LOGTAG, "could not save avatar"); - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - } + Futures.addCallback( + publicationFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final Void result) { + Log.d(Config.LOGTAG, "published avatar"); + callback.onAvatarPublicationSucceeded(); + } + + @Override + public void onFailure(@NonNull final Throwable t) { + Log.d(Config.LOGTAG, "avatar upload failed", t); + // TODO actually figure out what went wrong + callback.onAvatarPublicationFailed( + R.string.error_publish_avatar_server_reject); + } + }, + MoreExecutors.directExecutor()); } private void publishMucAvatar( diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 36e7048e5cd2444a1e66df523b4fa65aea3fba1a..5b2b3380da327d115beede69ba0e2ee4f9197009 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -37,6 +37,10 @@ public class Compatibility { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; } + public static boolean thirtyFour() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + } + public static void startService(final Context context, final Intent intent) { try { if (Compatibility.twentySix()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 6ee41f5f709049ffb11e66fc1d6b536ccdbfae4b..37115cbae4c12793e80ea1f00a076d9de1ef19f2 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -33,6 +33,7 @@ public final class Namespace { public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register"; public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0"; + public static final String HTTP_UPLOAD_PURPOSE = "urn:xmpp:http:upload:purpose:0"; public static final String STANZA_IDS = "urn:xmpp:sid:0"; public static final String IDLE = "urn:xmpp:idle:1"; public static final String DATA = "jabber:x:data"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 1f3660a10e3669749880360346b089a7cdd20444..b98da6a5204f5be805627dddb0e0afa562fdf9d6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,15 +1,27 @@ package eu.siacs.conversations.xmpp.manager; +import android.graphics.Bitmap; +import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; +import androidx.heifwriter.AvifWriter; +import androidx.heifwriter.HeifWriter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingOutputStream; import com.google.common.io.BaseEncoding; +import com.google.common.io.Files; 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.Contact; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Compatibility; +import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; @@ -20,9 +32,23 @@ import im.conversations.android.xmpp.model.avatar.Data; import im.conversations.android.xmpp.model.avatar.Info; import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.upload.purpose.Profile; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; public class AvatarManager extends AbstractManager { + private static final Executor AVATAR_COMPRESSION_EXECUTOR = + MoreExecutors.newSequentialExecutor(Executors.newSingleThreadScheduledExecutor()); + private final XmppConnectionService service; public AvatarManager(final XmppConnectionService service, XmppConnection connection) { @@ -133,29 +159,207 @@ public class AvatarManager extends AbstractManager { } } - public ListenableFuture publish(final Avatar avatar, final boolean open) { + private Info resizeAndStoreAvatar( + final Uri image, final int size, final ImageFormat format, final Integer charLimit) + throws Exception { + final var centerSquare = FileBackend.cropCenterSquare(context, image, size); + if (charLimit == null || format == ImageFormat.PNG) { + return resizeAndStoreAvatar(centerSquare, format, 90); + } else { + Info avatar = null; + for (int quality = 90; quality >= 50; quality = quality - 2) { + if (avatar != null) { + FileBackend.getAvatarFile(context, avatar.getId()).delete(); + } + Log.d(Config.LOGTAG, "trying to save thumbnail with quality " + quality); + avatar = resizeAndStoreAvatar(centerSquare, format, quality); + if (avatar.getBytes() <= charLimit) { + return avatar; + } + } + return avatar; + } + } + + private Info resizeAndStoreAvatar(final Bitmap image, ImageFormat format, final int quality) + throws Exception { + return switch (format) { + case PNG -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.PNG, quality); + case JPEG -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.JPEG, quality); + case WEBP -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.WEBP, quality); + case HEIF -> resizeAndStoreAvatarAsHeif(image, quality); + case AVIF -> resizeAndStoreAvatarAsAvif(image, quality); + }; + } + + private Info resizeAndStoreAvatar( + final Bitmap image, final Bitmap.CompressFormat format, final int quality) + throws IOException { + final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString()); + final var fileOutputStream = new FileOutputStream(randomFile); + final var hashingOutputStream = new HashingOutputStream(Hashing.sha1(), fileOutputStream); + image.compress(format, quality, hashingOutputStream); + hashingOutputStream.close(); + final var sha1 = hashingOutputStream.hash().toString(); + final var avatarFile = FileBackend.getAvatarFile(context, sha1); + if (randomFile.renameTo(avatarFile)) { + return new Info( + sha1, + avatarFile.length(), + ImageFormat.of(format).toContentType(), + image.getHeight(), + image.getWidth()); + } + throw new IllegalStateException( + String.format("Could not move file to %s", avatarFile.getAbsolutePath())); + } + + private Info resizeAndStoreAvatarAsHeif(final Bitmap image, final int quality) + throws Exception { + final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString()); + try (final var fileOutputStream = new FileOutputStream(randomFile); + final var heifWriter = + new HeifWriter.Builder( + fileOutputStream.getFD(), + image.getWidth(), + image.getHeight(), + HeifWriter.INPUT_MODE_BITMAP) + .setMaxImages(1) + .setQuality(quality) + .build()) { + + heifWriter.start(); + heifWriter.addBitmap(image); + heifWriter.stop(3_000); + } + return storeAsAvatar(randomFile, ImageFormat.HEIF, image.getHeight(), image.getWidth()); + } + + private Info resizeAndStoreAvatarAsAvif(final Bitmap image, final int quality) + throws Exception { + final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString()); + try (final var fileOutputStream = new FileOutputStream(randomFile); + final var avifWriter = + new AvifWriter.Builder( + fileOutputStream.getFD(), + image.getWidth(), + image.getHeight(), + AvifWriter.INPUT_MODE_BITMAP) + .setMaxImages(1) + .setQuality(quality) + .build()) { + avifWriter.start(); + avifWriter.addBitmap(image); + avifWriter.stop(3_000); + } + return storeAsAvatar(randomFile, ImageFormat.AVIF, image.getHeight(), image.getWidth()); + } + + private Info storeAsAvatar( + final File randomFile, final ImageFormat type, final int height, final int width) + throws IOException { + final var sha1 = Files.asByteSource(randomFile).hash(Hashing.sha1()).toString(); + final var avatarFile = FileBackend.getAvatarFile(context, sha1); + if (randomFile.renameTo(avatarFile)) { + return new Info(sha1, avatarFile.length(), type.toContentType(), height, width); + } + throw new IllegalStateException( + String.format("Could not move file to %s", avatarFile.getAbsolutePath())); + } + + public ListenableFuture> uploadAvatar(final Uri image, final int size) { + final var avatarFutures = new ImmutableList.Builder>(); + final var avatarFuture = resizeAndStoreAvatarAsync(image, size, ImageFormat.JPEG); + final var avatarWithUrlFuture = + Futures.transformAsync(avatarFuture, this::upload, MoreExecutors.directExecutor()); + avatarFutures.add(avatarWithUrlFuture); + + if (Compatibility.twentyEight() && !PhoneHelper.isEmulator()) { + final var avatarHeifFuture = resizeAndStoreAvatarAsync(image, size, ImageFormat.HEIF); + final var avatarHeifWithUrlFuture = + Futures.transformAsync( + avatarHeifFuture, this::upload, MoreExecutors.directExecutor()); + avatarFutures.add(avatarHeifWithUrlFuture); + } + if (Compatibility.thirtyFour() && !PhoneHelper.isEmulator()) { + final var avatarAvifFuture = resizeAndStoreAvatarAsync(image, size, ImageFormat.AVIF); + final var avatarAvifWithUrlFuture = + Futures.transformAsync( + avatarAvifFuture, this::upload, MoreExecutors.directExecutor()); + avatarFutures.add(avatarAvifWithUrlFuture); + } + + final var avatarThumbnailFuture = + resizeAndStoreAvatarAsync( + image, Config.AVATAR_SIZE, ImageFormat.JPEG, Config.AVATAR_CHAR_LIMIT); + avatarFutures.add(avatarThumbnailFuture); + + return Futures.allAsList(avatarFutures.build()); + } + + private ListenableFuture upload(final Info avatar) { + final var file = FileBackend.getAvatarFile(context, avatar.getId()); + final var urlFuture = + getManager(HttpUploadManager.class).upload(file, avatar.getType(), new Profile()); + return Futures.transform( + urlFuture, + url -> { + avatar.setUrl(url); + return avatar; + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture resizeAndStoreAvatarAsync( + final Uri image, final int size, final ImageFormat format) { + return resizeAndStoreAvatarAsync(image, size, format, null); + } + + private ListenableFuture resizeAndStoreAvatarAsync( + final Uri image, final int size, final ImageFormat format, final Integer charLimit) { + return Futures.submit( + () -> resizeAndStoreAvatar(image, size, format, charLimit), + AVATAR_COMPRESSION_EXECUTOR); + } + + public ListenableFuture publish(final Collection avatars, final boolean open) { + final Info mainAvatarInfo; + final byte[] mainAvatar; + try { + mainAvatarInfo = Iterables.find(avatars, a -> Objects.isNull(a.getUrl())); + mainAvatar = + Files.asByteSource(FileBackend.getAvatarFile(context, mainAvatarInfo.getId())) + .read(); + } catch (final IOException | NoSuchElementException e) { + return Futures.immediateFailedFuture(e); + } final NodeConfiguration configuration = open ? NodeConfiguration.OPEN : NodeConfiguration.PRESENCE; final var avatarData = new Data(); - avatarData.setContent(avatar.getImageAsBytes()); + avatarData.setContent(mainAvatar); final var future = - getManager(PepManager.class).publish(avatarData, avatar.sha1sum, configuration); + getManager(PepManager.class) + .publish(avatarData, mainAvatarInfo.getId(), configuration); return Futures.transformAsync( future, v -> { - final var id = avatar.sha1sum; + final var id = mainAvatarInfo.getId(); final var metadata = new Metadata(); - final var info = metadata.addExtension(new Info()); - info.setBytes(avatar.size); - info.setId(avatar.sha1sum); - info.setHeight(avatar.height); - info.setWidth(avatar.width); - info.setType(avatar.type); + metadata.addExtensions(avatars); return getManager(PepManager.class).publish(metadata, id, configuration); }, MoreExecutors.directExecutor()); } + public ListenableFuture uploadAndPublish(final Uri image, final boolean open) { + final var infoFuture = + connection + .getManager(AvatarManager.class) + .uploadAvatar(image, Config.AVATAR_FULL_SIZE); + return Futures.transformAsync( + infoFuture, avatars -> publish(avatars, open), MoreExecutors.directExecutor()); + } + public boolean hasPepToVCardConversion() { return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION); } @@ -169,4 +373,41 @@ public class AvatarManager extends AbstractManager { vs -> null, MoreExecutors.directExecutor()); } + + private String asContentType(final ImageFormat format) { + return switch (format) { + case WEBP -> "image/webp"; + case PNG -> "image/png"; + case JPEG -> "image/jpeg"; + case AVIF -> "image/avif"; + case HEIF -> "image/heif"; + }; + } + + public enum ImageFormat { + PNG, + JPEG, + WEBP, + HEIF, + AVIF; + + public String toContentType() { + return switch (this) { + case WEBP -> "image/webp"; + case PNG -> "image/png"; + case JPEG -> "image/jpeg"; + case AVIF -> "image/avif"; + case HEIF -> "image/heif"; + }; + } + + public static ImageFormat of(final Bitmap.CompressFormat compressFormat) { + return switch (compressFormat) { + case PNG -> PNG; + case WEBP -> WEBP; + case JPEG -> JPEG; + default -> throw new AssertionError("Not implemented"); + }; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java index 13c26b77a02b88a0020c168d1bb9f0ccd121761d..ea33b9e4b9ca03ff1a53da1a7e66b879e201b396 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java @@ -1,47 +1,122 @@ package eu.siacs.conversations.xmpp.manager; -import android.content.Context; import android.util.Base64; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; 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 eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; +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.model.stanza.Iq; import im.conversations.android.xmpp.model.upload.Request; +import im.conversations.android.xmpp.model.upload.purpose.Purpose; +import java.io.File; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; import java.util.UUID; +import okhttp3.Call; +import okhttp3.Callback; import okhttp3.Headers; import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.Response; public class HttpUploadManager extends AbstractManager { - public HttpUploadManager(final Context context, final XmppConnection connection) { - super(context, connection); + private final XmppConnectionService service; + + public HttpUploadManager(final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); + this.service = service; } public ListenableFuture request(final DownloadableFile file, final String mime) { + return request(file.getName(), mime, file.getExpectedSize(), null); + } + + public ListenableFuture request( + final String filename, + final String mime, + final long size, + @Nullable final Purpose purpose) { final var result = getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD); if (result == null) { return Futures.immediateFailedFuture( new IllegalStateException("No HTTP upload host found")); } - return requestHttpUpload(result.getKey(), file, mime); + return requestHttpUpload(result.getKey(), filename, mime, size, purpose); + } + + public ListenableFuture upload( + final File file, final String mime, final Purpose purpose) { + final var filename = file.getName(); + final var size = file.length(); + final var slotFuture = request(filename, mime, size, purpose); + return Futures.transformAsync( + slotFuture, slot -> upload(file, mime, slot), MoreExecutors.directExecutor()); + } + + private ListenableFuture upload(final File file, final String mime, final Slot slot) { + final SettableFuture future = SettableFuture.create(); + final OkHttpClient client = + service.getHttpConnectionManager() + .buildHttpClient(slot.put, getAccount(), 0, false); + final var body = RequestBody.create(MediaType.parse(mime), file); + final okhttp3.Request request = + new okhttp3.Request.Builder().url(slot.put).put(body).headers(slot.headers).build(); + client.newCall(request) + .enqueue( + new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + future.setException(e); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + future.set(slot.get); + } else { + future.setException( + new IllegalStateException( + String.format( + "Response code was %s", + response.code()))); + } + } + }); + return future; } private ListenableFuture requestHttpUpload( - final Jid host, final DownloadableFile file, final String mime) { + final Jid host, + final String filename, + final String mime, + final long size, + @Nullable final Purpose purpose) { final Iq iq = new Iq(Iq.Type.GET); iq.setTo(host); final var request = iq.addExtension(new Request()); - request.setFilename(convertFilename(file.getName())); - request.setSize(file.getExpectedSize()); + request.setFilename(convertFilename(filename)); + request.setSize(size); request.setContentType(mime); + if (purpose != null) { + request.addExtension(purpose); + } + Log.d(Config.LOGTAG, "-->" + iq); final var iqFuture = this.connection.sendIqPacket(iq); return Futures.transform( iqFuture, @@ -104,5 +179,15 @@ public class HttpUploadManager extends AbstractManager { private Slot(final HttpUrl put, final HttpUrl get, final Map headers) { this(put, get, Headers.of(headers)); } + + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("put", put) + .add("get", get) + .add("headers", headers) + .toString(); + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 88d35e7ba5f30e6e1ca580008bc054f65e3c9442..068e59b89d9d6c808d87740dd228e4519d6915db 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -1,12 +1,31 @@ package eu.siacs.conversations.xmpp.pep; import android.util.Base64; +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.xmpp.model.avatar.Metadata; +import okhttp3.HttpUrl; public class Avatar { + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("type", type) + .add("sha1sum", sha1sum) + .add("url", url) + .add("image", image) + .add("height", height) + .add("width", width) + .add("size", size) + .add("owner", owner) + .add("origin", origin) + .toString(); + } + public enum Origin { PEP, VCARD @@ -14,6 +33,7 @@ public class Avatar { public String type; public String sha1sum; + public HttpUrl url; public String image; public int height; public int width; diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java index 31099ff79e246d884da1b4a2c738571cb359f57a..446c04486530ac746e01228c427b5ed45a50a4bf 100644 --- a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java @@ -1,8 +1,10 @@ package im.conversations.android.xmpp.model.avatar; +import com.google.common.base.Strings; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import okhttp3.HttpUrl; @XmlElement(namespace = Namespace.AVATAR_METADATA) public class Info extends Extension { @@ -11,6 +13,20 @@ public class Info extends Extension { super(Info.class); } + public Info( + final String id, + final long bytes, + final String type, + final int height, + final int width) { + this(); + this.setId(id); + this.setBytes(bytes); + this.setType(type); + this.setHeight(height); + this.setWidth(width); + } + public long getHeight() { return this.getLongAttribute("height"); } @@ -27,8 +43,12 @@ public class Info extends Extension { return this.getAttribute("type"); } - public String getUrl() { - return this.getAttribute("url"); + public HttpUrl getUrl() { + final var url = this.getAttribute("url"); + if (Strings.isNullOrEmpty(url)) { + return null; + } + return HttpUrl.parse(url); } public String getId() { @@ -54,4 +74,8 @@ public class Info extends Extension { public void setType(final String type) { this.setAttribute("type", type); } + + public void setUrl(final HttpUrl url) { + this.setAttribute("url", url.toString()); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Ephemeral.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Ephemeral.java new file mode 100644 index 0000000000000000000000000000000000000000..213e402696d445f18a106083be41a17d8f5b897f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Ephemeral.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.upload.purpose; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Ephemeral extends Purpose { + + public Ephemeral() { + super(Ephemeral.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Message.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..94eba9db457c5a05f711d53097184af1446252e1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Message.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.upload.purpose; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Message extends Purpose { + + public Message() { + super(Purpose.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Permanent.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Permanent.java new file mode 100644 index 0000000000000000000000000000000000000000..4f2173c2ba803cc42274f5f05307c712e29045d8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Permanent.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.upload.purpose; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Permanent extends Purpose { + + public Permanent() { + super(Permanent.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Profile.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Profile.java new file mode 100644 index 0000000000000000000000000000000000000000..b968423cc2ecf0a718f2bc00f807432a9fdde899 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Profile.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.upload.purpose; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Profile extends Purpose { + + public Profile() { + super(Profile.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Purpose.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Purpose.java new file mode 100644 index 0000000000000000000000000000000000000000..566b5523ef7e2f6cd3b75d102b2dbde88cddff4c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/Purpose.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.upload.purpose; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class Purpose extends Extension { + + protected Purpose(final Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/purpose/package-info.java b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..e85818b2f50c664433b0db248bff373af504d569 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/purpose/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.HTTP_UPLOAD_PURPOSE) +package im.conversations.android.xmpp.model.upload.purpose; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; From 26882baa8b42eb9d5f6245a36f82c7d921a06b4a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 28 May 2025 11:44:38 +0200 Subject: [PATCH 18/87] pick higher resolution avatar from metadata node --- .../eu/siacs/conversations/AppSettings.java | 22 +- .../siacs/conversations/entities/Contact.java | 28 +- .../conversations/entities/MucOptions.java | 18 +- .../conversations/generator/IqGenerator.java | 8 - .../conversations/parser/PresenceParser.java | 11 +- .../conversations/services/AvatarService.java | 34 +- .../services/XmppConnectionService.java | 60 +-- .../conversations/ui/EditAccountActivity.java | 30 +- .../xmpp/manager/AvatarManager.java | 383 +++++++++++++++--- .../xmpp/manager/PepManager.java | 4 + .../xmpp/manager/PubSubManager.java | 11 + .../android/xmpp/model/avatar/Metadata.java | 5 + 12 files changed, 432 insertions(+), 182 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index 7318091ea0d97689fa4b2824f03070435a39b434..2f68ddea7cc4086b3a686fd592b4e924b44b492d 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Environment; import androidx.annotation.BoolRes; +import androidx.annotation.IntegerRes; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.google.common.base.Joiner; @@ -14,6 +15,7 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.Compatibility; import java.security.SecureRandom; +import java.util.Optional; public class AppSettings { @@ -52,6 +54,7 @@ public class AppSettings { public static final String CALL_INTEGRATION = "call_integration"; public static final String ALIGN_START = "align_start"; public static final String BACKUP_LOCATION = "backup_location"; + public static final String AUTO_ACCEPT_FILE_SIZE = "auto_accept_file_size"; private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers"; private static final String INSTALLATION_ID = "im.conversations.android.install_id"; @@ -163,12 +166,23 @@ public class AppSettings { || getBooleanPreference(KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service); } - private boolean getBooleanPreference(@NonNull final String name, @BoolRes int res) { + private boolean getBooleanPreference(@NonNull final String name, @BoolRes final int res) { final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); return sharedPreferences.getBoolean(name, context.getResources().getBoolean(res)); } + private long getLongPreference(final String name, @IntegerRes final int res) { + final long defaultValue = context.getResources().getInteger(res); + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + try { + return Long.parseLong(sharedPreferences.getString(name, String.valueOf(defaultValue))); + } catch (final NumberFormatException e) { + return defaultValue; + } + } + public String getOmemo() { final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -246,6 +260,12 @@ public class AppSettings { return installationId; } + public Optional getAutoAcceptFileSize() { + final long autoAcceptFileSize = + getLongPreference(AUTO_ACCEPT_FILE_SIZE, R.integer.auto_accept_filesize); + return autoAcceptFileSize <= 0 ? Optional.empty() : Optional.of(autoAcceptFileSize); + } + public synchronized void resetInstallationId() { final var secureRandom = new SecureRandom(); final var installationId = secureRandom.nextLong(); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 8b023b9b46a3bab029dcd80099f6dbdd0a278759..86b14ace6e87a0ba7278bec02afb8757223fdc55 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -16,7 +16,6 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.Collection; @@ -58,7 +57,7 @@ public class Contact implements ListItem, Blockable { private JSONArray groups = new JSONArray(); private final Presences presences = new Presences(this); protected Account account; - protected Avatar avatar; + protected String avatar; private boolean mActive = false; private long mLastseen = 0; @@ -95,11 +94,7 @@ public class Contact implements ListItem, Blockable { tmpJsonObject = new JSONObject(); } this.keys = tmpJsonObject; - if (avatar != null) { - this.avatar = new Avatar(); - this.avatar.sha1sum = avatar; - this.avatar.origin = Avatar.Origin.VCARD; // always assume worst - } + this.avatar = avatar; try { this.groups = (groups == null ? new JSONArray() : new JSONArray(groups)); } catch (JSONException e) { @@ -241,7 +236,7 @@ public class Contact implements ListItem, Blockable { values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null); values.put(PHOTOURI, photoUri); values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar == null ? null : avatar.getFilename()); + values.put(AVATAR, avatar); values.put(LAST_PRESENCE, mLastPresence); values.put(LAST_TIME, mLastseen); values.put(GROUPS, groups.toString()); @@ -437,25 +432,16 @@ public class Contact implements ListItem, Blockable { return getJid().getDomain().toString(); } - public boolean setAvatar(final Avatar avatar) { + public boolean setAvatar(final String avatar) { if (this.avatar != null && this.avatar.equals(avatar)) { return false; } - if (this.avatar != null - && this.avatar.origin == Avatar.Origin.PEP - && avatar.origin == Avatar.Origin.VCARD) { - return false; - } this.avatar = avatar; return true; } - public String getAvatarFilename() { - return avatar == null ? null : avatar.getFilename(); - } - - public Avatar getAvatar() { - return avatar; + public String getAvatar() { + return this.avatar; } public boolean mutualPresenceSubscription() { @@ -570,7 +556,7 @@ public class Contact implements ListItem, Blockable { } public boolean hasAvatarOrPresenceName() { - return (avatar != null && avatar.getFilename() != null) || presenceName != null; + return avatar != null || presenceName != null; } public boolean refreshRtpCapability() { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 2dbed2c1470e2713377aa973a9204ef1096d29fc..a425de52b84c2a6639717ca46e2c7ca13e7d1488 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -158,7 +158,7 @@ public class MucOptions { } public String getAvatar() { - return account.getRoster().getContact(conversation.getJid()).getAvatarFilename(); + return account.getRoster().getContact(conversation.getJid()).getAvatar(); } public boolean hasFeature(String feature) { @@ -903,14 +903,20 @@ public class MucOptions { } public String getAvatar() { + + // TODO prefer potentially better quality avatars from contact + // TODO use getContact and if that’s not null and avatar is set use that + + getContact(); + if (avatar != null) { return avatar.getFilename(); } - Avatar avatar = - realJid != null - ? getAccount().getRoster().getContact(realJid).getAvatar() - : null; - return avatar == null ? null : avatar.getFilename(); + if (realJid == null) { + return null; + } + final var contact = getAccount().getRoster().getContact(realJid); + return contact.getAvatar(); } public Account getAccount() { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 512248c06734289970afb04b1b006fefc00b8680..3a4fe249444650a68539c48ae63ea7e7e2600f5c 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -87,14 +87,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq retrieveAvatarMetaData(final Jid to) { - final Iq packet = retrieve("urn:xmpp:avatar:metadata", null); - if (to != null) { - packet.setTo(to); - } - return packet; - } - public Iq retrieveDeviceIds(final Jid to) { final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index d5ebbf1deacaaf2f5040b2075851dc4674bd3a74..4e73be5f880507a7dff46702e82174cea32be4c2 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -168,13 +168,16 @@ public class PresenceParser extends AbstractParser if (user.setAvatar(avatar)) { mXmppConnectionService.getAvatarService().clear(user); } + + // TODO don’t do that. This will just overwrite (better) PEP avatars + if (user.getRealJid() != null) { final Contact c = conversation .getAccount() .getRoster() .getContact(user.getRealJid()); - if (c.setAvatar(avatar)) { + if (c.setAvatar(avatar.sha1sum)) { connection .getManager(RosterManager.class) .writeToDatabaseAsync(); @@ -342,6 +345,10 @@ public class PresenceParser extends AbstractParser final Contact contact = account.getRoster().getContact(from); if (type == null) { final String resource = from.isBareJid() ? "" : from.getResource(); + + // TODO simply don’t parse avatars for contacts at all. Only if presence is bare and a + // MUC + final Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) { @@ -354,7 +361,7 @@ public class PresenceParser extends AbstractParser mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } else { - if (contact.setAvatar(avatar)) { + if (contact.setAvatar(avatar.sha1sum)) { connection.getManager(RosterManager.class).writeToDatabaseAsync(); mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.updateConversationUi(); diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index fd2cf0099a2c8ab267713ee4ae8dbb3a4fe0b072..304bf0a0e6aaf604735aa4f09cef3ee617d692d9 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -107,11 +107,8 @@ public class AvatarService { if (avatar != null || cachedOnly) { return avatar; } - if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) { - avatar = - mXmppConnectionService - .getFileBackend() - .getAvatar(contact.getAvatarFilename(), size); + if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) { + avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); } if (avatar == null && contact.getProfilePhoto() != null) { avatar = @@ -119,11 +116,8 @@ public class AvatarService { .getFileBackend() .cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); } - if (avatar == null && contact.getAvatarFilename() != null) { - avatar = - mXmppConnectionService - .getFileBackend() - .getAvatar(contact.getAvatarFilename(), size); + if (avatar == null && contact.getAvatar() != null) { + avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); } if (avatar == null) { avatar = @@ -227,7 +221,7 @@ public class AvatarService { Contact c = user.getContact(); if (c != null && (c.getProfilePhoto() != null - || c.getAvatarFilename() != null + || c.getAvatar() != null || user.getAvatar() == null)) { return get(c, size, cachedOnly); } else { @@ -322,7 +316,7 @@ public class AvatarService { Jid jid = bookmark.getJid(); Account account = bookmark.getAccount(); Contact contact = jid == null ? null : account.getRoster().getContact(jid); - if (contact != null && contact.getAvatarFilename() != null) { + if (contact != null && contact.getAvatar() != null) { return get(contact, size, cachedOnly); } String seed = jid != null ? jid.asBareJid().toString() : null; @@ -497,7 +491,7 @@ public class AvatarService { return get(message.getCounterparts(), size, cachedOnly); } else if (message.getStatus() == Message.STATUS_RECEIVED) { Contact c = message.getContact(); - if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null)) { + if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { return get(c, size, cachedOnly); } else if (conversation instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { @@ -621,18 +615,12 @@ public class AvatarService { Contact contact = user.getContact(); if (contact != null) { Uri uri = null; - if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) { - uri = - mXmppConnectionService - .getFileBackend() - .getAvatarUri(contact.getAvatarFilename()); + if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar()); } else if (contact.getProfilePhoto() != null) { uri = Uri.parse(contact.getProfilePhoto()); - } else if (contact.getAvatarFilename() != null) { - uri = - mXmppConnectionService - .getFileBackend() - .getAvatarUri(contact.getAvatarFilename()); + } else if (contact.getAvatar() != null) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar()); } if (drawTile(canvas, uri, left, top, right, bottom)) { return true; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 288afe6809fdb953f567aaa56e48a177b1aa22ff..08f6cdb2c69e65976dfc1ba384a986493959bb76 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -147,9 +147,7 @@ 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.avatar.Metadata; import im.conversations.android.xmpp.model.disco.info.InfoQuery; -import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.up.Push; import java.io.File; @@ -4292,6 +4290,8 @@ public class XmppConnectionService extends Service { connection.getManager(RosterManager.class).deleteRosterItem(contact); } + // TODO get thumbnail via AvatarManager + // TODO call AvatarManager.getInbandAvatar form vcard manager and simplify publication process public void publishMucAvatar( final Conversation conversation, final Uri image, final OnAvatarPublication callback) { new Thread( @@ -4316,6 +4316,7 @@ public class XmppConnectionService extends Service { .start(); } + // TODO get rid of the async part. Manager is already async public void publishAvatarAsync( final Account account, final Uri image, @@ -4374,7 +4375,7 @@ public class XmppConnectionService extends Service { public void onSuccess(Void result) { Log.d(Config.LOGTAG, "published muc avatar"); final var c = account.getRoster().getContact(avatar.owner); - c.setAvatar(avatar); + c.setAvatar(avatar.sha1sum); getAvatarService().clear(c); getAvatarService().clear(conversation.getMucOptions()); callback.onAvatarPublicationSucceeded(); @@ -4459,7 +4460,7 @@ public class XmppConnectionService extends Service { } else { final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar); + contact.setAvatar(avatar.sha1sum); account.getXmppConnection() .getManager(RosterManager.class) .writeToDatabaseAsync(); @@ -4522,6 +4523,7 @@ public class XmppConnectionService extends Service { MoreExecutors.directExecutor()); } + // TODO move this into VCard manager private void setVCardAvatar(final Account account, final Avatar avatar) { Log.d( Config.LOGTAG, @@ -4541,7 +4543,7 @@ public class XmppConnectionService extends Service { // TODO if this is a MUC clear MucOptions too // TODO do the same clearing for when setting a cached version final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar); + contact.setAvatar(avatar.sha1sum); account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); getAvatarService().clear(contact); updateRosterUi(); @@ -4557,9 +4559,10 @@ public class XmppConnectionService extends Service { updateConversationUi(); updateMucRosterUi(); } + // TODO don’t do that. this will put lower quality vCard avatars into contacts if (user.getRealJid() != null) { Contact contact = account.getRoster().getContact(user.getRealJid()); - contact.setAvatar(avatar); + contact.setAvatar(avatar.sha1sum); account.getXmppConnection() .getManager(RosterManager.class) .writeToDatabaseAsync(); @@ -4571,46 +4574,11 @@ public class XmppConnectionService extends Service { } } - public void checkForAvatar(final Account account, final UiCallback callback) { - final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket( - account, - packet, - response -> { - if (response.getType() != Iq.Type.RESULT) { - callback.error(0, null); - } - final var pubsub = packet.getExtension(PubSub.class); - if (pubsub == null) { - callback.error(0, null); - return; - } - final var items = pubsub.getItems(); - if (items == null) { - callback.error(0, null); - return; - } - final var item = items.getFirstItemWithId(Metadata.class); - if (item == null) { - callback.error(0, null); - return; - } - final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue()); - if (avatar == null) { - callback.error(0, null); - return; - } - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - }); + public ListenableFuture checkForAvatar(final Account account) { + final var connection = account.getXmppConnection(); + return connection + .getManager(AvatarManager.class) + .fetchAndStore(account.getJid().asBareJid()); } public void notifyAccountAvatarHasChanged(final Account account) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index c531fb7b8c87d6a7c94ff5994f94b65166c35d4d..95f25e8ab2bb73b7abf6062c062d6879e8c79983 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -41,6 +41,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; import de.gultsch.common.Linkify; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; @@ -79,7 +82,6 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.manager.CarbonsManager; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Arrays; import java.util.List; @@ -116,22 +118,19 @@ public class EditAccountActivity extends OmemoActivity deleteAccountAndReturnIfNecessary(); finish(); }; - private final UiCallback mAvatarFetchCallback = - new UiCallback() { + private final FutureCallback mAvatarFetchCallback = + new FutureCallback<>() { @Override - public void userInputRequired(final PendingIntent pi, final Avatar avatar) { - finishInitialSetup(avatar); + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, "found pre-existing avatar"); + finishInitialSetup(true); } @Override - public void success(final Avatar avatar) { - finishInitialSetup(avatar); - } - - @Override - public void error(final int errorCode, final Avatar avatar) { - finishInitialSetup(avatar); + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "failed to fetch avatar", t); + finishInitialSetup(false); } }; private final OnClickListener mAvatarClickListener = @@ -454,7 +453,8 @@ public class EditAccountActivity extends OmemoActivity } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) { if (!mFetchingAvatar) { mFetchingAvatar = true; - xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback); + final var future = xmppConnectionService.checkForAvatar(mAccount); + Futures.addCallback(future, mAvatarFetchCallback, MoreExecutors.directExecutor()); } } if (mAccount != null) { @@ -521,7 +521,7 @@ public class EditAccountActivity extends OmemoActivity refreshUi(); } - protected void finishInitialSetup(final Avatar avatar) { + protected void finishInitialSetup(final boolean avatar) { runOnUiThread( () -> { SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); @@ -530,7 +530,7 @@ public class EditAccountActivity extends OmemoActivity final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1; - if (avatar != null || (connection != null && !connection.getFeatures().pep())) { + if (avatar || (connection != null && !connection.getFeatures().pep())) { intent = new Intent( getApplicationContext(), StartConversationActivity.class); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index b98da6a5204f5be805627dddb0e0afa562fdf9d6..f60d45eb6f74907fa3c6a8184cc9299cc1218456 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -6,16 +6,22 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.heifwriter.AvifWriter; import androidx.heifwriter.HeifWriter; +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; +import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; import com.google.common.hash.Hashing; import com.google.common.hash.HashingOutputStream; -import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; import com.google.common.io.Files; 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 eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.persistance.FileBackend; @@ -25,7 +31,6 @@ import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.NodeConfiguration; import im.conversations.android.xmpp.model.ByteContent; import im.conversations.android.xmpp.model.avatar.Data; @@ -38,14 +43,55 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class AvatarManager extends AbstractManager { + private static final Object RENAME_LOCK = new Object(); + + private static final List SUPPORTED_CONTENT_TYPES; + + private static final Ordering AVATAR_ORDERING = + new Ordering<>() { + @Override + public int compare(Info left, Info right) { + return ComparisonChain.start() + .compare( + right.getWidth() * right.getHeight(), + left.getWidth() * left.getHeight()) + .compare( + ImageFormat.formatPriority(right.getType()), + ImageFormat.formatPriority(left.getType())) + .result(); + } + }; + + static { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + builder.add(ImageFormat.JPEG, ImageFormat.PNG, ImageFormat.WEBP); + if (Compatibility.twentyEight()) { + builder.add(ImageFormat.HEIF); + } + if (Compatibility.thirtyFour()) { + builder.add(ImageFormat.AVIF); + } + final var supportedFormats = builder.build(); + SUPPORTED_CONTENT_TYPES = + ImmutableList.copyOf( + Collections2.transform(supportedFormats, ImageFormat::toContentType)); + } + private static final Executor AVATAR_COMPRESSION_EXECUTOR = MoreExecutors.newSequentialExecutor(Executors.newSingleThreadScheduledExecutor()); @@ -56,43 +102,154 @@ public class AvatarManager extends AbstractManager { this.service = service; } - public ListenableFuture fetch(final Jid address, final String itemId) { + private ListenableFuture fetch(final Jid address, final String itemId) { final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class); return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor()); } - public ListenableFuture fetchAndStore(final Avatar avatar) { - final var future = fetch(avatar.owner, avatar.sha1sum); - return Futures.transform( + private ListenableFuture fetchAndStoreWithFallback( + final Jid address, final Info picked, final Info fallback) { + Preconditions.checkArgument(fallback.getUrl() == null, "fallback avatar must be in-band"); + final var url = picked.getUrl(); + if (url != null) { + final var httpDownloadFuture = fetchAndStoreHttp(url, picked); + return Futures.catchingAsync( + httpDownloadFuture, + Exception.class, + ex -> { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": could not download avatar for " + + address + + " from " + + url, + ex); + return fetchAndStoreInBand(address, fallback); + }, + MoreExecutors.directExecutor()); + } else { + return fetchAndStoreInBand(address, picked); + } + } + + private ListenableFuture fetchAndStoreInBand(final Jid address, final Info avatar) { + final var future = fetch(address, avatar.getId()); + return Futures.transformAsync( future, data -> { - avatar.image = BaseEncoding.base64().encode(data); - if (service.getFileBackend().save(avatar)) { - setPepAvatar(avatar); - return null; - } else { - throw new IllegalStateException("Could not store avatar"); + final var actualHash = Hashing.sha1().hashBytes(data).toString(); + if (!actualHash.equals(avatar.getId())) { + throw new IllegalStateException( + String.format("In-band avatar hash of %s did not match", address)); + } + + final var file = FileBackend.getAvatarFile(context, avatar.getId()); + if (file.exists()) { + return Futures.immediateFuture(avatar); } + return Futures.transform( + write(file, data), v -> avatar, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor()); } - private void setPepAvatar(final Avatar avatar) { + private ListenableFuture write(final File destination, byte[] bytes) { + return Futures.submit( + () -> { + final var randomFile = + new File(context.getCacheDir(), UUID.randomUUID().toString()); + Files.write(bytes, randomFile); + if (moveAvatarIntoCache(randomFile, destination)) { + return null; + } + throw new IllegalStateException( + String.format( + "Could not move file to %s", destination.getAbsolutePath())); + }, + AVATAR_COMPRESSION_EXECUTOR); + } + + private ListenableFuture fetchAndStoreHttp(final HttpUrl url, final Info avatar) { + final SettableFuture settableFuture = SettableFuture.create(); + final OkHttpClient client = + service.getHttpConnectionManager().buildHttpClient(url, getAccount(), 30, false); + final var request = new Request.Builder().url(url).get().build(); + client.newCall(request) + .enqueue( + new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + settableFuture.setException(e); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + try { + write(avatar, response); + } catch (final Exception e) { + settableFuture.setException(e); + return; + } + settableFuture.set(avatar); + } else { + settableFuture.setException( + new IOException("HTTP call was not successful")); + } + } + }); + return settableFuture; + } + + private void write(final Info avatar, Response response) throws IOException { + final var body = response.body(); + if (body == null) { + throw new IOException("Body was null"); + } + final long bytes = avatar.getBytes(); + final long actualBytes; + final var inputStream = ByteStreams.limit(body.byteStream(), avatar.getBytes()); + final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString()); + final String actualHash; + try (final var fileOutputStream = new FileOutputStream(randomFile); + var hashingOutputStream = + new HashingOutputStream(Hashing.sha1(), fileOutputStream)) { + actualBytes = ByteStreams.copy(inputStream, hashingOutputStream); + actualHash = hashingOutputStream.hash().toString(); + } + if (actualBytes != bytes) { + throw new IllegalStateException("File size did not meet expected size"); + } + if (!actualHash.equals(avatar.getId())) { + throw new IllegalStateException("File hash did not match"); + } + final var avatarFile = FileBackend.getAvatarFile(context, avatar.getId()); + if (moveAvatarIntoCache(randomFile, avatarFile)) { + return; + } + throw new IOException("Could not move avatar to avatar location"); + } + + private void setAvatar(final Jid from, final Info info) { + Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + info.getId()); final var account = getAccount(); - if (account.getJid().asBareJid().equals(avatar.owner)) { - if (account.setAvatar(avatar.getFilename())) { + if (account.getJid().asBareJid().equals(from)) { + if (account.setAvatar(info.getId())) { getDatabase().updateAccount(account); + service.notifyAccountAvatarHasChanged(account); } - this.service.getAvatarService().clear(account); - this.service.updateConversationUi(); - this.service.updateAccountUi(); + service.getAvatarService().clear(account); + service.updateConversationUi(); + service.updateAccountUi(); } else { - final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar); - account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); - this.service.getAvatarService().clear(contact); - this.service.updateConversationUi(); - this.service.updateRosterUi(); + final Contact contact = account.getRoster().getContact(from); + if (contact.setAvatar(info.getId())) { + connection.getManager(RosterManager.class).writeToDatabaseAsync(); + service.getAvatarService().clear(contact); + service.updateConversationUi(); + service.updateRosterUi(); + } } } @@ -100,43 +257,34 @@ public class AvatarManager extends AbstractManager { final var account = getAccount(); // TODO support retract final var entry = items.getFirstItemWithId(Metadata.class); - final var avatar = - entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); + if (entry == null) { + return; + } + final var avatar = getPreferredFallback(entry); if (avatar == null) { - Log.d(Config.LOGTAG, "could not parse avatar metadata from " + from); return; } - avatar.owner = from.asBareJid(); - if (service.getFileBackend().isAvatarCached(avatar)) { - if (account.getJid().asBareJid().equals(from)) { - if (account.setAvatar(avatar.getFilename())) { - service.databaseBackend.updateAccount(account); - service.notifyAccountAvatarHasChanged(account); - } - service.getAvatarService().clear(account); - service.updateConversationUi(); - service.updateAccountUi(); - } else { - final Contact contact = account.getRoster().getContact(from); - if (contact.setAvatar(avatar)) { - connection.getManager(RosterManager.class).writeToDatabaseAsync(); - service.getAvatarService().clear(contact); - service.updateConversationUi(); - service.updateRosterUi(); - } - } + + Log.d(Config.LOGTAG, "picked avatar from " + from + ": " + avatar.preferred); + + final var cache = FileBackend.getAvatarFile(context, avatar.preferred.getId()); + + if (cache.exists()) { + setAvatar(from, avatar.preferred); } else if (service.isDataSaverDisabled()) { - final var future = this.fetchAndStore(avatar); + final var future = + this.fetchAndStoreWithFallback(from, avatar.preferred, avatar.fallback); Futures.addCallback( future, - new FutureCallback() { + new FutureCallback() { @Override - public void onSuccess(Void result) { + public void onSuccess(Info result) { + setAvatar(from, result); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully fetched pep avatar for " - + avatar.owner); + + from); } @Override @@ -148,6 +296,46 @@ public class AvatarManager extends AbstractManager { } } + private PreferredFallback getPreferredFallback(final Map.Entry entry) { + final var mainItemId = entry.getKey(); + final var infos = entry.getValue().getInfos(); + + final var inBandAvatar = Iterables.find(infos, i -> mainItemId.equals(i.getId()), null); + + if (inBandAvatar == null || inBandAvatar.getUrl() != null) { + return null; + } + + final var optionalAutoAcceptSize = new AppSettings(context).getAutoAcceptFileSize(); + if (optionalAutoAcceptSize.isEmpty()) { + return new PreferredFallback(inBandAvatar); + } else { + + final var supported = + Collections2.filter( + infos, + i -> + Objects.nonNull(i.getId()) + && i.getBytes() > 0 + && i.getHeight() > 0 + && i.getWidth() > 0 + && SUPPORTED_CONTENT_TYPES.contains(i.getType())); + + final var autoAcceptSize = optionalAutoAcceptSize.get(); + + final var supportedBelowLimit = + Collections2.filter(supported, i -> i.getBytes() <= autoAcceptSize); + + if (supportedBelowLimit.isEmpty()) { + return new PreferredFallback(inBandAvatar); + } else { + final var preferred = + Iterables.getFirst(AVATAR_ORDERING.sortedCopy(supportedBelowLimit), null); + return new PreferredFallback(preferred, inBandAvatar); + } + } + } + public void handleDelete(final Jid from) { final var account = getAccount(); final boolean isAccount = account.getJid().asBareJid().equals(from); @@ -202,7 +390,7 @@ public class AvatarManager extends AbstractManager { hashingOutputStream.close(); final var sha1 = hashingOutputStream.hash().toString(); final var avatarFile = FileBackend.getAvatarFile(context, sha1); - if (randomFile.renameTo(avatarFile)) { + if (moveAvatarIntoCache(randomFile, avatarFile)) { return new Info( sha1, avatarFile.length(), @@ -260,7 +448,7 @@ public class AvatarManager extends AbstractManager { throws IOException { final var sha1 = Files.asByteSource(randomFile).hash(Hashing.sha1()).toString(); final var avatarFile = FileBackend.getAvatarFile(context, sha1); - if (randomFile.renameTo(avatarFile)) { + if (moveAvatarIntoCache(randomFile, avatarFile)) { return new Info(sha1, avatarFile.length(), type.toContentType(), height, width); } throw new IllegalStateException( @@ -374,14 +562,59 @@ public class AvatarManager extends AbstractManager { MoreExecutors.directExecutor()); } - private String asContentType(final ImageFormat format) { - return switch (format) { - case WEBP -> "image/webp"; - case PNG -> "image/png"; - case JPEG -> "image/jpeg"; - case AVIF -> "image/avif"; - case HEIF -> "image/heif"; - }; + public ListenableFuture fetchAndStore(final Jid address) { + final var metaDataFuture = + getManager(PubSubManager.class).fetchItems(address, Metadata.class); + return Futures.transformAsync( + metaDataFuture, + metaData -> { + final var entry = Iterables.getFirst(metaData.entrySet(), null); + if (entry == null) { + throw new IllegalStateException("Metadata item not found"); + } + final var avatar = getPreferredFallback(entry); + + if (avatar == null) { + throw new IllegalStateException("No avatar found"); + } + + final var cache = FileBackend.getAvatarFile(context, avatar.preferred.getId()); + + if (cache.exists()) { + Log.d( + Config.LOGTAG, + "fetchAndStore. file existed " + cache.getAbsolutePath()); + setAvatar(address, avatar.preferred); + return Futures.immediateVoidFuture(); + } else { + final var future = + this.fetchAndStoreWithFallback( + address, avatar.preferred, avatar.fallback); + return Futures.transform( + future, + info -> { + setAvatar(address, info); + return null; + }, + MoreExecutors.directExecutor()); + } + }, + MoreExecutors.directExecutor()); + } + + private static boolean moveAvatarIntoCache(final File randomFile, final File destination) { + synchronized (RENAME_LOCK) { + if (destination.exists()) { + return true; + } + final var directory = destination.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d( + Config.LOGTAG, + "create avatar cache directory: " + directory.getAbsolutePath()); + } + return randomFile.renameTo(destination); + } } public enum ImageFormat { @@ -401,6 +634,22 @@ public class AvatarManager extends AbstractManager { }; } + public static int formatPriority(final String type) { + final var format = ofContentType(type); + return format == null ? Integer.MIN_VALUE : format.ordinal(); + } + + private static ImageFormat ofContentType(final String type) { + return switch (type) { + case "image/png" -> PNG; + case "image/jpeg" -> JPEG; + case "image/webp" -> WEBP; + case "image/heif" -> HEIF; + case "image/avif" -> AVIF; + default -> null; + }; + } + public static ImageFormat of(final Bitmap.CompressFormat compressFormat) { return switch (compressFormat) { case PNG -> PNG; @@ -410,4 +659,18 @@ public class AvatarManager extends AbstractManager { }; } } + + private static final class PreferredFallback { + private final Info preferred; + private final Info fallback; + + private PreferredFallback(final Info fallback) { + this(fallback, fallback); + } + + private PreferredFallback(Info preferred, Info fallback) { + this.preferred = preferred; + this.fallback = fallback; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java index fd5bd6f155b9040d2ff37797021914e077486c24..b77286c53d41fa02330a6bcfe14079b0c63948ce 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java @@ -22,6 +22,10 @@ public class PepManager extends AbstractManager { return pubSubManager().fetchItems(pepService(), clazz); } + public ListenableFuture fetchMostRecentItem(final Class clazz) { + return pubSubManager().fetchMostRecentItem(pepService(), clazz); + } + public ListenableFuture fetchMostRecentItem( final String node, final Class clazz) { return pubSubManager().fetchMostRecentItem(pepService(), node, clazz); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java index 956049335d9db2f2e724b16807b0cb1f4991afb7..0dfd2637f34c4241cd83e75f559bcae08b7e39ad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java @@ -136,6 +136,17 @@ public class PubSubManager extends AbstractManager { MoreExecutors.directExecutor()); } + public ListenableFuture fetchMostRecentItem( + final Jid address, final Class clazz) { + final var id = ExtensionFactory.id(clazz); + if (id == null) { + return Futures.immediateFailedFuture( + new IllegalArgumentException( + String.format("%s is not a registered extension", clazz.getName()))); + } + return fetchMostRecentItem(address, id.namespace, clazz); + } + public ListenableFuture fetchMostRecentItem( final Jid address, final String node, final Class clazz) { final Iq request = new Iq(Iq.Type.GET); diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java index 400f989572c2813aaeacdce5863e012ba128af3e..708b0e2018099acba02805e8105fed54ee8d58ac 100644 --- a/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java @@ -3,6 +3,7 @@ package im.conversations.android.xmpp.model.avatar; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; @XmlElement(namespace = Namespace.AVATAR_METADATA) public class Metadata extends Extension { @@ -10,4 +11,8 @@ public class Metadata extends Extension { public Metadata() { super(Metadata.class); } + + public Collection getInfos() { + return this.getExtensions(Info.class); + } } From a2485811cbf7059326b29b2f93d7e1ad84cb5de3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 28 May 2025 16:15:15 +0200 Subject: [PATCH 19/87] add activity to view full screen avatars --- src/main/AndroidManifest.xml | 4 +- .../eu/siacs/conversations/ui/Activities.java | 9 +++- .../siacs/conversations/ui/BaseActivity.java | 4 +- .../ui/ContactDetailsActivity.java | 7 +++ .../ui/ViewProfilePictureActivity.java | 49 +++++++++++++++++++ .../layout/activity_view_profile_picture.xml | 28 +++++++++++ src/main/res/values-night/themes.xml | 31 +----------- src/main/res/values/themes.xml | 31 ++++++++++++ 8 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java create mode 100644 src/main/res/layout/activity_view_profile_picture.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index cee52eaf8d1b693dc43468f3233eaee5b46e581c..2efd545cb7aa7a0dce65c6535b3ed23751108593 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -60,7 +60,7 @@ android:name="android.hardware.microphone" android:required="false" /> - + @@ -365,6 +365,8 @@ + diff --git a/src/main/java/eu/siacs/conversations/ui/Activities.java b/src/main/java/eu/siacs/conversations/ui/Activities.java index d95d6b4acd04e9aba716480294210202ab1d6f23..a0a118afb9fdc08aae679983abc230650540055e 100644 --- a/src/main/java/eu/siacs/conversations/ui/Activities.java +++ b/src/main/java/eu/siacs/conversations/ui/Activities.java @@ -17,7 +17,14 @@ public final class Activities { public static void setStatusAndNavigationBarColors( final Activity activity, final View view, final boolean raisedStatusBar) { - final var isLightMode = isLightMode(activity); + setStatusAndNavigationBarColors(activity, view, isLightMode(activity), raisedStatusBar); + } + + public static void setStatusAndNavigationBarColors( + final Activity activity, + final View view, + final boolean isLightMode, + final boolean raisedStatusBar) { final var window = activity.getWindow(); final var flags = view.getSystemUiVisibility(); // an elevation of 4 matches the MaterialToolbar elevation diff --git a/src/main/java/eu/siacs/conversations/ui/BaseActivity.java b/src/main/java/eu/siacs/conversations/ui/BaseActivity.java index cea58c15ecef8e1fbff19b263b4b9980c243fde0..259809f9995a0764425bccae6d7b42405eb1b626 100644 --- a/src/main/java/eu/siacs/conversations/ui/BaseActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/BaseActivity.java @@ -1,10 +1,8 @@ package eu.siacs.conversations.ui; import android.util.Log; - import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; - import eu.siacs.conversations.Conversations; import eu.siacs.conversations.ui.util.SettingsUtils; @@ -23,7 +21,7 @@ public abstract class BaseActivity extends AppCompatActivity { } @Override - protected void onResume(){ + protected void onResume() { super.onResume(); SettingsUtils.applyScreenshotSetting(this); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 06a1183d369490c51ddd9040d53cf713c9d74ec7..8b0ca874dc146f25bddf9c55031334a3b50f265f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -637,6 +637,13 @@ public class ContactDetailsActivity extends OmemoActivity } private void onBadgeClick(final View view) { + final var intent = new Intent(this, ViewProfilePictureActivity.class); + intent.setData(Uri.fromParts("avatar", contact.getAvatar(), null)); + intent.putExtra(ViewProfilePictureActivity.EXTRA_DISPLAY_NAME, contact.getDisplayName()); + startActivity(intent); + } + + private void onAddToAddressBookClick(final View view) { if (QuickConversationsService.isContactListIntegration(this)) { final Uri systemAccount = contact.getSystemAccount(); if (systemAccount == null) { diff --git a/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..00f3c1f48b49b5efd95792aa9268c1f4bd16f95f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java @@ -0,0 +1,49 @@ +package eu.siacs.conversations.ui; + +import static eu.siacs.conversations.ui.XmppActivity.configureActionBar; + +import android.net.Uri; +import android.os.Bundle; +import androidx.databinding.DataBindingUtil; +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityViewProfilePictureBinding; +import eu.siacs.conversations.persistance.FileBackend; + +public class ViewProfilePictureActivity extends ActionBarActivity { + + public static final String EXTRA_DISPLAY_NAME = "eu.siacs.conversations.extra.DISPLAY_NAME"; + + private ActivityViewProfilePictureBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_view_profile_picture); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot(), false, false); + + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + } + + @Override + public void onStart() { + super.onStart(); + final var intent = getIntent(); + if (intent == null) { + return; + } + final var uri = intent.getData(); + if (uri == null) { + return; + } + final var avatar = uri.getSchemeSpecificPart(); + if (avatar == null) { + return; + } + final var displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME); + final var file = FileBackend.getAvatarFile(this, avatar); + this.binding.imageView.setImageURI(Uri.fromFile(file)); + setTitle(displayName); + } +} diff --git a/src/main/res/layout/activity_view_profile_picture.xml b/src/main/res/layout/activity_view_profile_picture.xml new file mode 100644 index 0000000000000000000000000000000000000000..d5b8658692ee54ccdb670f3c2476e266a9517d04 --- /dev/null +++ b/src/main/res/layout/activity_view_profile_picture.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values-night/themes.xml b/src/main/res/values-night/themes.xml index c6a2926e8ff2443987133d14d2f26e69e5141a9e..945addb9788876976829bc1452045d67f0fa877f 100644 --- a/src/main/res/values-night/themes.xml +++ b/src/main/res/values-night/themes.xml @@ -1,35 +1,6 @@ - + + + From 0da674ae822dcb78a4bfa498f7c699e096547d62 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 28 May 2025 20:35:58 +0200 Subject: [PATCH 20/87] store contacts in map --- .../persistance/DatabaseBackend.java | 10 +++---- .../ui/ViewProfilePictureActivity.java | 2 -- .../xmpp/manager/RosterManager.java | 28 +++++++++---------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 9da8e6bc8e5b2853b5eb1b4ae487a787d6ae92df..15eb5d7d2fb7a515b2fe9a213a13e18b55a83817 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -11,7 +11,7 @@ import android.os.SystemClock; import android.util.Base64; import android.util.Log; import com.google.common.base.Stopwatch; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -1809,8 +1809,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { return rows == 1; } - public List readRoster(final Account account) { - final var builder = new ImmutableList.Builder(); + public Map readRoster(final Account account) { + final var builder = new ImmutableMap.Builder(); final SQLiteDatabase db = this.getReadableDatabase(); final String[] args = {account.getUuid()}; try (final Cursor cursor = @@ -1819,11 +1819,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { final var contact = Contact.fromCursor(cursor); if (contact != null) { contact.setAccount(account); - builder.add(contact); + builder.put(contact.getJid(), contact); } } } - return builder.build(); + return builder.buildKeepingLast(); } public void writeRoster( diff --git a/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java index 00f3c1f48b49b5efd95792aa9268c1f4bd16f95f..30705664c9f33d4e0053d564b443c1b6157299e8 100644 --- a/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.ui; -import static eu.siacs.conversations.ui.XmppActivity.configureActionBar; - import android.net.Uri; import android.os.Bundle; import androidx.databinding.DataBindingUtil; diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java index ff2d13e50748efb5af9bc3c271882a6e4f5f9ee8..318f6591a33c6822b01c2a55b6837017f362598b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java @@ -5,7 +5,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -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.MoreExecutors; @@ -22,9 +21,10 @@ import im.conversations.android.xmpp.model.error.Error; import im.conversations.android.xmpp.model.roster.Item; import im.conversations.android.xmpp.model.roster.Query; import im.conversations.android.xmpp.model.stanza.Iq; -import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; public class RosterManager extends AbstractManager implements Roster { @@ -32,7 +32,7 @@ public class RosterManager extends AbstractManager implements Roster { private final ReplacingSerialSingleThreadExecutor dbExecutor = new ReplacingSerialSingleThreadExecutor(RosterManager.class.getName()); - private final List contacts = new ArrayList<>(); + private final Map contacts = new HashMap<>(); private String version; private final XmppConnectionService service; @@ -196,14 +196,13 @@ public class RosterManager extends AbstractManager implements Roster { @NonNull public Contact getContactInternal(@NonNull final Jid jid) { - final var existing = - Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null); + final var existing = this.contacts.get(jid.asBareJid()); if (existing != null) { return existing; } final var contact = new Contact(jid.asBareJid()); contact.setAccount(getAccount()); - this.contacts.add(contact); + this.contacts.put(jid.asBareJid(), contact); return contact; } @@ -211,8 +210,7 @@ public class RosterManager extends AbstractManager implements Roster { @Nullable public Contact getContactFromContactList(@NonNull final Jid jid) { synchronized (this.contacts) { - final var contact = - Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null); + final var contact = this.contacts.get(jid.asBareJid()); if (contact != null && contact.showInContactList()) { return contact; } else { @@ -224,7 +222,7 @@ public class RosterManager extends AbstractManager implements Roster { @Override public List getContacts() { synchronized (this.contacts) { - return ImmutableList.copyOf(this.contacts); + return ImmutableList.copyOf(this.contacts.values()); } } @@ -234,20 +232,20 @@ public class RosterManager extends AbstractManager implements Roster { final int option = Contact.getOption(clazz); synchronized (this.contacts) { return ImmutableList.copyOf( - Collections2.filter(this.contacts, c -> c.getOption(option))); + Collections2.filter(this.contacts.values(), c -> c.getOption(option))); } } public void clearPresences() { synchronized (this.contacts) { - for (final var contact : this.contacts) { + for (final var contact : this.contacts.values()) { contact.clearPresences(); } } } private void markAllAsNotInRoster() { - for (final var contact : this.contacts) { + for (final var contact : this.contacts.values()) { contact.resetOption(Contact.Options.IN_ROSTER); } } @@ -255,7 +253,7 @@ public class RosterManager extends AbstractManager implements Roster { public void restore() { synchronized (this.contacts) { this.contacts.clear(); - this.contacts.addAll(getDatabase().readRoster(getAccount())); + this.contacts.putAll(getDatabase().readRoster(getAccount())); } } @@ -268,7 +266,7 @@ public class RosterManager extends AbstractManager implements Roster { final List contacts; final String version; synchronized (this.contacts) { - contacts = ImmutableList.copyOf(this.contacts); + contacts = ImmutableList.copyOf(this.contacts.values()); version = this.version; } getDatabase().writeRoster(account, version, contacts); @@ -276,7 +274,7 @@ public class RosterManager extends AbstractManager implements Roster { public void syncDirtyContacts() { synchronized (this.contacts) { - for (final var contact : this.contacts) { + for (final var contact : this.contacts.values()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { addRosterItem(contact, null); } From 8a6ec09b9e23a4de9f7724787788b75a1faf0f17 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 May 2025 15:33:22 +0200 Subject: [PATCH 21/87] slightly redesign contact details --- .../conversations/entities/MucOptions.java | 16 +-- .../ui/ConferenceDetailsActivity.java | 8 +- .../ui/ContactDetailsActivity.java | 67 +++++++--- src/main/res/drawable/ic_contacts_24dp.xml | 10 ++ .../res/layout/activity_contact_details.xml | 124 +++++++++++------- src/main/res/values/strings.xml | 4 +- 6 files changed, 156 insertions(+), 73 deletions(-) create mode 100644 src/main/res/drawable/ic_contacts_24dp.xml diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index a425de52b84c2a6639717ca46e2c7ca13e7d1488..7b3b6efa455a003a938d2ba287df088254f748e1 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -472,17 +472,17 @@ public class MucOptions { return subset; } - public static List sub(List users, int max) { - ArrayList subset = new ArrayList<>(); - HashSet jids = new HashSet<>(); - for (User user : users) { - jids.add(user.getAccount().getJid().asBareJid()); - if (user.getRealJid() == null - || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) { + public static List sub(final List users, final int max) { + final var subset = new ArrayList(); + final var addresses = new HashSet(); + for (final var user : users) { + addresses.add(user.getAccount().getJid().asBareJid()); + final var address = user.getRealJid(); + if (address == null || (address.getLocal() != null && addresses.add(address))) { subset.add(user); } if (subset.size() >= max) { - break; + return subset; } } return subset; diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index fe2da072e2acc8aff33559e7414ddccb3c8d45c5..72f41044c86e56dc055a2a61d991f20a2328b2da 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -641,8 +641,12 @@ public class ConferenceDetailsActivity extends XmppActivity } } }); - this.mUserPreviewAdapter.submitList( - MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users))); + this.binding.users.post( + () -> { + final var list = + MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)); + this.mUserPreviewAdapter.submitList(list); + }); this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE); this.binding.showUsers.setVisibility(users.size() > 0 ? View.VISIBLE : View.GONE); this.binding.showUsers.setText( diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 8b0ca874dc146f25bddf9c55031334a3b50f265f..914d5afaa4f27ac87539b0262c64e696a03511d7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -14,9 +14,11 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; +import android.provider.Settings; import android.text.Spannable; import android.text.SpannableString; import android.text.style.RelativeSizeSpan; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -145,16 +147,14 @@ public class ContactDetailsActivity extends OmemoActivity private void checkContactPermissionAndShowAddDialog() { if (hasContactsPermission()) { showAddToPhoneBookDialog(); - } else if (QuickConversationsService.isContactListIntegration(this) - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + } else if (QuickConversationsService.isContactListIntegration(this)) { requestPermissions( new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); } } private boolean hasContactsPermission() { - if (QuickConversationsService.isContactListIntegration(this) - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (QuickConversationsService.isContactListIntegration(this)) { return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; } else { @@ -175,7 +175,7 @@ public class ContactDetailsActivity extends OmemoActivity value = jid.toString(); } final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setTitle(getString(R.string.save_to_contact)); builder.setMessage(getString(R.string.add_phone_book_text, value)); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton( @@ -295,14 +295,35 @@ public class ContactDetailsActivity extends OmemoActivity int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // TODO check for Camera / Scan permission super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (grantResults.length > 0) + if (grantResults.length == 0) { + return; + } + if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { - showAddToPhoneBookDialog(); - xmppConnectionService.loadPhoneContacts(); - xmppConnectionService.startContactObserver(); - } + showAddToPhoneBookDialog(); + xmppConnectionService.loadPhoneContacts(); + xmppConnectionService.startContactObserver(); + } else { + showRedirectToAppSettings(); } + } + } + + private void showRedirectToAppSettings() { + final var dialogBuilder = new MaterialAlertDialogBuilder(this); + dialogBuilder.setTitle(R.string.save_to_contact); + dialogBuilder.setMessage( + getString(R.string.no_contacts_permission, getString(R.string.app_name))); + dialogBuilder.setPositiveButton( + R.string.continue_btn, + (d, w) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + startActivity(intent); + }); + dialogBuilder.setNegativeButton(R.string.cancel, null); + dialogBuilder.create().show(); } @Override @@ -481,29 +502,41 @@ public class ContactDetailsActivity extends OmemoActivity } if (contact.isBlocked() && !this.showDynamicTags) { - binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText(R.string.contact_blocked); + binding.detailsLastSeen.setVisibility(View.VISIBLE); + binding.detailsLastSeen.setText(R.string.contact_blocked); } else { if (showLastSeen && contact.getLastseen() > 0 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { - binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText( + binding.detailsLastSeen.setVisibility(View.VISIBLE); + binding.detailsLastSeen.setText( UIHelper.lastseen( getApplicationContext(), contact.isActive(), contact.getLastseen())); } else { - binding.detailsLastseen.setVisibility(View.GONE); + binding.detailsLastSeen.setVisibility(View.GONE); } } - binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid())); + binding.detailsContactXmppAddress.setText( + IrregularUnicodeDetector.style(this, contact.getJid())); final String account = contact.getAccount().getJid().asBareJid().toString(); binding.detailsAccount.setText(getString(R.string.using_account, account)); AvatarWorkerTask.loadAvatar( contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); binding.detailsContactBadge.setOnClickListener(this::onBadgeClick); + if (QuickConversationsService.isContactListIntegration(this)) { + if (contact.getSystemAccount() == null) { + binding.addAddressBook.setText(R.string.save_to_contact); + } else { + binding.addAddressBook.setText(R.string.show_in_contacts); + } + binding.addAddressBook.setVisibility(View.VISIBLE); + binding.addAddressBook.setOnClickListener(this::onAddToAddressBookClick); + } else { + binding.addAddressBook.setVisibility(View.GONE); + } binding.detailsContactKeys.removeAllViews(); boolean hasKeys = false; diff --git a/src/main/res/drawable/ic_contacts_24dp.xml b/src/main/res/drawable/ic_contacts_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..7af18f9437567c13ce7f5537f4c183385e9d773e --- /dev/null +++ b/src/main/res/drawable/ic_contacts_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index b4687b8c245f6a846a520ecc60405e19b49559cb..83a325e2420634a68d02618a1a33f7f2e778b3af 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -46,76 +46,109 @@ + + + + + android:layout_below="@+id/details_contact_xmpp_address" + android:layout_centerHorizontal="true" + android:layout_marginTop="16dp" + android:orientation="horizontal"> - + android:layout_marginHorizontal="8dp" + android:text="@string/add_contact" /> - - - - + android:layout_marginHorizontal="8dp" + android:text="@string/save_to_contact" + app:icon="@drawable/ic_contacts_24dp" /> + - + + + app:flow_horizontalBias="0" + app:flow_horizontalGap="8sp" + app:flow_horizontalStyle="packed" + app:flow_verticalGap="4sp" + app:flow_wrapMode="chain" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + -