provide alternative api for XMPP extensions

Daniel Gultsch created

Change summary

build.gradle                                                                                                   |   4 
libs/annotation-processor/build.gradle                                                                         |  20 
libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java | 185 
libs/annotation/build.gradle                                                                                   |   6 
libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java                              |  15 
libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java                              |  12 
proguard-rules.pro                                                                                             |   1 
settings.gradle                                                                                                |   2 
src/free/java/eu/siacs/conversations/services/PushManagementService.java                                       |  11 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java                                        | 375 
src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java                                      |  36 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                                                | 162 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                                           |  92 
src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java                                          |  39 
src/main/java/eu/siacs/conversations/http/SlotRequester.java                                                   |  14 
src/main/java/eu/siacs/conversations/parser/AbstractParser.java                                                |  14 
src/main/java/eu/siacs/conversations/parser/IqParser.java                                                      |  82 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                                                 |  68 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java                                                |  17 
src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java                                     |  48 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java                                       |  20 
src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java                                           |  15 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java                                       | 594 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                                                      |  11 
src/main/java/eu/siacs/conversations/xml/Element.java                                                          |  17 
src/main/java/eu/siacs/conversations/xml/LocalizedContent.java                                                 |   2 
src/main/java/eu/siacs/conversations/xml/Namespace.java                                                        |  49 
src/main/java/eu/siacs/conversations/xml/TagWriter.java                                                        |  15 
src/main/java/eu/siacs/conversations/xml/XmlReader.java                                                        |  25 
src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java                                                      |   6 
src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java                                              |   8 
src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java                                         |   7 
src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java                                        |   8 
src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java                                                  |   5 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                                                  | 549 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java                                       |  10 
src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java                                 |  49 
src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java                                   |   5 
src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java                                               |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java                                  |  60 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java                             | 166 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java                                      | 191 
src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java                                   |   7 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java                                            |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java                          |  11 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java                    |  14 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java                     |  21 
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java                    |   8 
src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java                                              |   6 
src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java                           |  42 
src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java                                          |  53 
src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java                                                |  75 
src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java                                           | 100 
src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java                                          |   8 
src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java                                        |  11 
src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java                                      |  11 
src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java                                    |  14 
src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java                                 |  14 
src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java                                |  13 
src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java                                 |  15 
src/main/java/im/conversations/android/xmpp/Entity.java                                                        |  34 
src/main/java/im/conversations/android/xmpp/EntityCapabilities.java                                            | 133 
src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java                                           | 185 
src/main/java/im/conversations/android/xmpp/ExtensionFactory.java                                              |  78 
src/main/java/im/conversations/android/xmpp/NodeConfiguration.java                                             | 112 
src/main/java/im/conversations/android/xmpp/Page.java                                                          |  31 
src/main/java/im/conversations/android/xmpp/Range.java                                                         |  40 
src/main/java/im/conversations/android/xmpp/Timestamps.java                                                    |  44 
src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java                             |  12 
src/main/java/im/conversations/android/xmpp/model/ByteContent.java                                             |  33 
src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java                                         |  10 
src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java                                  |   8 
src/main/java/im/conversations/android/xmpp/model/Extension.java                                               |  62 
src/main/java/im/conversations/android/xmpp/model/Hash.java                                                    |  46 
src/main/java/im/conversations/android/xmpp/model/StreamElement.java                                           |   8 
src/main/java/im/conversations/android/xmpp/model/StreamFeature.java                                           |   8 
src/main/java/im/conversations/android/xmpp/model/addressing/Address.java                                      |  11 
src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java                                    |  11 
src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java                                 |   6 
src/main/java/im/conversations/android/xmpp/model/avatar/Data.java                                             |  14 
src/main/java/im/conversations/android/xmpp/model/avatar/Info.java                                             |  37 
src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java                                         |  13 
src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java                                          |  60 
src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java                                          |  22 
src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java                                      |  35 
src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java                              |  23 
src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java                                       |  24 
src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java                                          |  45 
src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java                                              |  13 
src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java                                     |  12 
src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java                                             |  29 
src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java                                         |  13 
src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java                                          |  21 
src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java                                         |  12 
src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java                                    |  21 
src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java                           |  13 
src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java                                    |   5 
src/main/java/im/conversations/android/xmpp/model/bind/Bind.java                                               |  34 
src/main/java/im/conversations/android/xmpp/model/bind/Jid.java                                                |  13 
src/main/java/im/conversations/android/xmpp/model/bind/Resource.java                                           |  16 
src/main/java/im/conversations/android/xmpp/model/bind/package-info.java                                       |   5 
src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java                                              |  24 
src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java                                             |  11 
src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java                                           |  12 
src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java                                            |  12 
src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java                                      |   5 
src/main/java/im/conversations/android/xmpp/model/blocking/Block.java                                          |  12 
src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java                                      |  11 
src/main/java/im/conversations/android/xmpp/model/blocking/Item.java                                           |  17 
src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java                                        |  12 
src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java                                   |   5 
src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java                                     |  32 
src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java                                     |  12 
src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java                                           |  12 
src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java                                   |   5 
src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java                                |  43 
src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java                          |  39 
src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java                          |  45 
src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java                                          |  12 
src/main/java/im/conversations/android/xmpp/model/carbons/Received.java                                        |  17 
src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java                                            |  17 
src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java                                    |   5 
src/main/java/im/conversations/android/xmpp/model/correction/Replace.java                                      |  24 
src/main/java/im/conversations/android/xmpp/model/csi/Active.java                                              |  12 
src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java                               |  12 
src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java                                            |  12 
src/main/java/im/conversations/android/xmpp/model/csi/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/data/Data.java                                               | 110 
src/main/java/im/conversations/android/xmpp/model/data/Field.java                                              |  29 
src/main/java/im/conversations/android/xmpp/model/data/Option.java                                             |  12 
src/main/java/im/conversations/android/xmpp/model/data/Value.java                                              |  12 
src/main/java/im/conversations/android/xmpp/model/data/package-info.java                                       |   5 
src/main/java/im/conversations/android/xmpp/model/delay/Delay.java                                             |  30 
src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java                                  |  12 
src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java                                 |  12 
src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java                             |   5 
src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java                                      |  19 
src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java                                     |  39 
src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java                                    |  38 
src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java                                 |   5 
src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java                                        |  22 
src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java                                  |  19 
src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java                                |   5 
src/main/java/im/conversations/android/xmpp/model/error/Condition.java                                         | 188 
src/main/java/im/conversations/android/xmpp/model/error/Error.java                                             |  55 
src/main/java/im/conversations/android/xmpp/model/error/Text.java                                              |  13 
src/main/java/im/conversations/android/xmpp/model/fast/Fast.java                                               |  11 
src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java                                          |  11 
src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java                                       |  17 
src/main/java/im/conversations/android/xmpp/model/fast/Token.java                                              |  12 
src/main/java/im/conversations/android/xmpp/model/fast/package-info.java                                       |   5 
src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java                                       |  18 
src/main/java/im/conversations/android/xmpp/model/hints/Store.java                                             |  12 
src/main/java/im/conversations/android/xmpp/model/hints/package-info.java                                      |   6 
src/main/java/im/conversations/android/xmpp/model/jabber/Body.java                                             |  21 
src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java                                         |  12 
src/main/java/im/conversations/android/xmpp/model/jabber/Show.java                                             |  11 
src/main/java/im/conversations/android/xmpp/model/jabber/Status.java                                           |  13 
src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java                                          |  12 
src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java                                           |  12 
src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java                                           | 110 
src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java                            |  44 
src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java                                              |  11 
src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java                                       |  14 
src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java                                             |  24 
src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java                                             |  38 
src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java                                              |  11 
src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java                                             |  11 
src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/mam/End.java                                                 |  15 
src/main/java/im/conversations/android/xmpp/model/mam/Fin.java                                                 |  16 
src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java                                            |  20 
src/main/java/im/conversations/android/xmpp/model/mam/Query.java                                               |  16 
src/main/java/im/conversations/android/xmpp/model/mam/Result.java                                              |  25 
src/main/java/im/conversations/android/xmpp/model/mam/Start.java                                               |  16 
src/main/java/im/conversations/android/xmpp/model/mam/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java                                       |  16 
src/main/java/im/conversations/android/xmpp/model/markers/Markable.java                                        |  12 
src/main/java/im/conversations/android/xmpp/model/markers/Received.java                                        |  20 
src/main/java/im/conversations/android/xmpp/model/markers/package-info.java                                    |   5 
src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java                                           |  12 
src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java                                         |   9 
src/main/java/im/conversations/android/xmpp/model/muc/History.java                                             |  20 
src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java                                       |  12 
src/main/java/im/conversations/android/xmpp/model/muc/Role.java                                                |   8 
src/main/java/im/conversations/android/xmpp/model/muc/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java                                           |  58 
src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java                                        |  27 
src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java                                         |  16 
src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java                                   |   5 
src/main/java/im/conversations/android/xmpp/model/nick/Nick.java                                               |  13 
src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java                                     |  19 
src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java                                       |  18 
src/main/java/im/conversations/android/xmpp/model/oob/URL.java                                                 |  12 
src/main/java/im/conversations/android/xmpp/model/oob/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java                                            |  17 
src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java                                           |  14 
src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java                                              |  15 
src/main/java/im/conversations/android/xmpp/model/ping/Ping.java                                               |  13 
src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java                                             |  10 
src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java                                            |  52 
src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java                                           |  64 
src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java                                          |  16 
src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java                                   |  21 
src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java                                          |  20 
src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java                                |  19 
src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java                               |   5 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java                                      |  56 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java                                      |  16 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java                                    |  16 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java                               |   5 
src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java                                  |  21 
src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java                                |  12 
src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java                               |   5 
src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java                                      |  17 
src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java                                     |  36 
src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java                                  |   5 
src/main/java/im/conversations/android/xmpp/model/receipts/Received.java                                       |  20 
src/main/java/im/conversations/android/xmpp/model/receipts/Request.java                                        |  12 
src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java                                   |   5 
src/main/java/im/conversations/android/xmpp/model/register/Instructions.java                                   |  10 
src/main/java/im/conversations/android/xmpp/model/register/Password.java                                       |  10 
src/main/java/im/conversations/android/xmpp/model/register/Register.java                                       |  21 
src/main/java/im/conversations/android/xmpp/model/register/Remove.java                                         |  10 
src/main/java/im/conversations/android/xmpp/model/register/Username.java                                       |  12 
src/main/java/im/conversations/android/xmpp/model/register/package-info.java                                   |   5 
src/main/java/im/conversations/android/xmpp/model/roster/Group.java                                            |  12 
src/main/java/im/conversations/android/xmpp/model/roster/Item.java                                             |  61 
src/main/java/im/conversations/android/xmpp/model/roster/Query.java                                            |  21 
src/main/java/im/conversations/android/xmpp/model/roster/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/rsm/After.java                                               |  12 
src/main/java/im/conversations/android/xmpp/model/rsm/Before.java                                              |  12 
src/main/java/im/conversations/android/xmpp/model/rsm/Count.java                                               |  23 
src/main/java/im/conversations/android/xmpp/model/rsm/First.java                                               |  12 
src/main/java/im/conversations/android/xmpp/model/rsm/Last.java                                                |  12 
src/main/java/im/conversations/android/xmpp/model/rsm/Max.java                                                 |  16 
src/main/java/im/conversations/android/xmpp/model/rsm/Set.java                                                 |  55 
src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java                                               |  12 
src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java                                          |  12 
src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java                                         |  29 
src/main/java/im/conversations/android/xmpp/model/sasl/Response.java                                           |  12 
src/main/java/im/conversations/android/xmpp/model/sasl/Success.java                                            |  13 
src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java                                       |   5 
src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java                                      |  12 
src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java                                    |  30 
src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java                           |  28 
src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java                                            |  34 
src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java                                         |  12 
src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java                                          |  12 
src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java                                           |  23 
src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java                                      |   5 
src/main/java/im/conversations/android/xmpp/model/sm/Ack.java                                                  |  23 
src/main/java/im/conversations/android/xmpp/model/sm/Enable.java                                               |  13 
src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java                                              |  35 
src/main/java/im/conversations/android/xmpp/model/sm/Failed.java                                               |  17 
src/main/java/im/conversations/android/xmpp/model/sm/Request.java                                              |  12 
src/main/java/im/conversations/android/xmpp/model/sm/Resume.java                                               |  18 
src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java                                              |  18 
src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java                                     |  12 
src/main/java/im/conversations/android/xmpp/model/sm/package-info.java                                         |   5 
src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java                                               |  77 
src/main/java/im/conversations/android/xmpp/model/stanza/Message.java                                          |  64 
src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java                                         |  12 
src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java                                           |  74 
src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/state/Active.java                                            |  11 
src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java                             |  10 
src/main/java/im/conversations/android/xmpp/model/state/Composing.java                                         |  11 
src/main/java/im/conversations/android/xmpp/model/state/Gone.java                                              |  11 
src/main/java/im/conversations/android/xmpp/model/state/Inactive.java                                          |  11 
src/main/java/im/conversations/android/xmpp/model/state/Paused.java                                            |  11 
src/main/java/im/conversations/android/xmpp/model/state/package-info.java                                      |   5 
src/main/java/im/conversations/android/xmpp/model/streams/Features.java                                        |  33 
src/main/java/im/conversations/android/xmpp/model/streams/package-info.java                                    |   5 
src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java                                             |  13 
src/main/java/im/conversations/android/xmpp/model/tls/Required.java                                            |  11 
src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java                                            |  15 
src/main/java/im/conversations/android/xmpp/model/tls/package-info.java                                        |   5 
src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java                                         |  12 
src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java                                         |  21 
src/main/java/im/conversations/android/xmpp/model/unique/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/upload/Get.java                                              |  22 
src/main/java/im/conversations/android/xmpp/model/upload/Header.java                                           |  16 
src/main/java/im/conversations/android/xmpp/model/upload/Put.java                                              |  27 
src/main/java/im/conversations/android/xmpp/model/upload/Request.java                                          |  24 
src/main/java/im/conversations/android/xmpp/model/upload/Slot.java                                             |  12 
src/main/java/im/conversations/android/xmpp/model/upload/package-info.java                                     |   5 
src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java                                       |  13 
src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java                                             |  11 
src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java                                             |  12 
src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java                                      |   5 
src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java                                      |  12 
src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java                                |  21 
src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java                               |   5 
src/main/java/im/conversations/android/xmpp/model/version/Version.java                                         |  25 
src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java                                       |  90 
src/playstore/java/eu/siacs/conversations/services/PushManagementService.java                                  | 143 
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java                                |  10 
302 files changed, 6,849 insertions(+), 1,942 deletions(-)

Detailed changes

build.gradle 🔗

@@ -33,6 +33,10 @@ configurations {
 dependencies {
     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
 
+    implementation project(':libs:annotation')
+    annotationProcessor project(':libs:annotation-processor')
+
+
     implementation 'androidx.viewpager:viewpager:1.0.0'
 
     playstoreImplementation('com.google.firebase:firebase-messaging:24.0.0') {

libs/annotation-processor/build.gradle 🔗

@@ -0,0 +1,20 @@
+apply plugin: "java-library"
+
+repositories {
+    google()
+    mavenCentral()
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
+dependencies {
+
+    implementation project(':libs:annotation')
+
+    annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
+    api 'com.google.auto.service:auto-service-annotations:1.0.1'
+    implementation 'com.google.guava:guava:31.1-jre'
+
+}

libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java 🔗

@@ -0,0 +1,185 @@
+package im.conversations.android.annotation.processor;
+
+import com.google.auto.service.AutoService;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.annotation.XmlPackage;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
+import javax.tools.JavaFileObject;
+
+@AutoService(Processor.class)
+@SupportedSourceVersion(SourceVersion.RELEASE_17)
+@SupportedAnnotationTypes("im.conversations.android.annotation.XmlElement")
+public class XmlElementProcessor extends AbstractProcessor {
+
+    @Override
+    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
+        final Set<? extends Element> elements =
+                roundEnvironment.getElementsAnnotatedWith(XmlElement.class);
+        final ImmutableMap.Builder<Id, String> builder = ImmutableMap.builder();
+        for (final Element element : elements) {
+            if (element instanceof final TypeElement typeElement) {
+                final Id id = of(typeElement);
+                builder.put(id, typeElement.getQualifiedName().toString());
+            }
+        }
+        final ImmutableMap<Id, String> maps = builder.build();
+        if (maps.isEmpty()) {
+            return false;
+        }
+        final JavaFileObject extensionFile;
+        try {
+            extensionFile =
+                    processingEnv
+                            .getFiler()
+                            .createSourceFile("im.conversations.android.xmpp.Extensions");
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+        try (final PrintWriter out = new PrintWriter(extensionFile.openWriter())) {
+            out.println("package im.conversations.android.xmpp;");
+            out.println("import com.google.common.collect.BiMap;");
+            out.println("import com.google.common.collect.ImmutableBiMap;");
+            out.println("import im.conversations.android.xmpp.ExtensionFactory;");
+            out.println("import im.conversations.android.xmpp.model.Extension;");
+            out.print("\n");
+            out.println("public final class Extensions {");
+            out.println(
+                    "public static final BiMap<ExtensionFactory.Id, Class<? extends Extension>>"
+                            + " EXTENSION_CLASS_MAP;");
+            out.println("static {");
+            out.println(
+                    "final var builder = new ImmutableBiMap.Builder<ExtensionFactory.Id, Class<?"
+                            + " extends Extension>>();");
+            for (final Map.Entry<Id, String> entry : maps.entrySet()) {
+                Id id = entry.getKey();
+                String clazz = entry.getValue();
+                out.format(
+                        "builder.put(new ExtensionFactory.Id(\"%s\",\"%s\"),%s.class);",
+                        id.name, id.namespace, clazz);
+                out.print("\n");
+            }
+            out.println("EXTENSION_CLASS_MAP = builder.build();");
+            out.println("}");
+            out.println(" private Extensions() {}");
+            out.println("}");
+            // writing generated file to out …
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return true;
+    }
+
+    private static Id of(final TypeElement typeElement) {
+        final XmlElement xmlElement = typeElement.getAnnotation(XmlElement.class);
+        final PackageElement packageElement = getPackageElement(typeElement);
+        final XmlPackage xmlPackage =
+                packageElement == null ? null : packageElement.getAnnotation(XmlPackage.class);
+        if (xmlElement == null) {
+            throw new IllegalStateException(
+                    String.format(
+                            "%s is not annotated as @XmlElement",
+                            typeElement.getQualifiedName().toString()));
+        }
+        final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace();
+        final String elementName = xmlElement.name();
+        final String elementNamespace = xmlElement.namespace();
+        final String namespace;
+        if (!Strings.isNullOrEmpty(elementNamespace)) {
+            namespace = elementNamespace;
+        } else if (!Strings.isNullOrEmpty(packageNamespace)) {
+            namespace = packageNamespace;
+        } else {
+            throw new IllegalStateException(
+                    String.format(
+                            "%s does not declare a namespace",
+                            typeElement.getQualifiedName().toString()));
+        }
+        if (!hasEmptyDefaultConstructor(typeElement)) {
+            throw new IllegalStateException(
+                    String.format(
+                            "%s does not have an empty default constructor",
+                            typeElement.getQualifiedName().toString()));
+        }
+        final String name;
+        if (Strings.isNullOrEmpty(elementName)) {
+            name =
+                    CaseFormat.UPPER_CAMEL.to(
+                            CaseFormat.LOWER_HYPHEN, typeElement.getSimpleName().toString());
+        } else {
+            name = elementName;
+        }
+        return new Id(name, namespace);
+    }
+
+    private static PackageElement getPackageElement(final TypeElement typeElement) {
+        final Element parent = typeElement.getEnclosingElement();
+        if (parent instanceof PackageElement) {
+            return (PackageElement) parent;
+        } else {
+            final Element nextParent = parent.getEnclosingElement();
+            if (nextParent instanceof PackageElement) {
+                return (PackageElement) nextParent;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private static boolean hasEmptyDefaultConstructor(final TypeElement typeElement) {
+        final List<ExecutableElement> constructors =
+                ElementFilter.constructorsIn(typeElement.getEnclosedElements());
+        for (final ExecutableElement constructor : constructors) {
+            if (constructor.getParameters().isEmpty()
+                    && constructor.getModifiers().contains(Modifier.PUBLIC)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static class Id {
+        public final String name;
+        public final String namespace;
+
+        public Id(String name, String namespace) {
+            this.name = name;
+            this.namespace = namespace;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Id id = (Id) o;
+            return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(name, namespace);
+        }
+    }
+}

libs/annotation/build.gradle 🔗

@@ -0,0 +1,6 @@
+apply plugin: "java-library"
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}

libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java 🔗

@@ -0,0 +1,15 @@
+package im.conversations.android.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+public @interface XmlElement {
+
+    String name() default "";
+
+    String namespace() default "";
+}

libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.PACKAGE)
+public @interface XmlPackage {
+    String namespace();
+}

proguard-rules.pro 🔗

@@ -1,6 +1,7 @@
 -dontobfuscate
 
 -keep class eu.siacs.conversations.**
+-keep class im.conversations.**
 
 -keep class org.whispersystems.**
 

settings.gradle 🔗

@@ -1 +1,3 @@
+include ':libs:annotation', ':libs:annotation-processor:'
+
 rootProject.name = 'Conversations'

src/free/java/eu/siacs/conversations/services/PushManagementService.java 🔗

@@ -1,7 +1,6 @@
 package eu.siacs.conversations.services;
 
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Conversation;
 
 public class PushManagementService {
 
@@ -11,11 +10,7 @@ public class PushManagementService {
 		this.mXmppConnectionService = service;
 	}
 
-	void registerPushTokenOnServer(Account account) {
-		//stub implementation. only affects playstore flavor
-	}
-
-	void unregisterChannel(Account account, String hash) {
+	public void registerPushTokenOnServer(Account account) {
 		//stub implementation. only affects playstore flavor
 	}
 
@@ -26,8 +21,4 @@ public class PushManagementService {
 	public boolean isStub() {
 		return true;
 	}
-
-	public boolean availableAndUseful(Account account) {
-		return false;
-	}
 }

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -61,7 +61,6 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jingle.DescriptionTransport;
 import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
 import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
@@ -70,8 +69,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
@@ -392,20 +390,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
             return;
         }
-        IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid());
-        mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
-                    Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
-                } else {
-                    //TODO consider calling registerDevices only after item-not-found to account for broken PEPs
-                    Element item = mXmppConnectionService.getIqParser().getItem(packet);
-                    Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds);
-                    registerDevices(account.getJid().asBareJid(), deviceIds);
-                }
+        Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid());
+        mXmppConnectionService.sendIqPacket(account, packet, response -> {
+            if (packet.getType() == Iq.Type.TIMEOUT) {
+                Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
+            } else {
+                //TODO consider calling registerDevices only after item-not-found to account for broken PEPs
+                final Element item = IqParser.getItem(packet);
+                final Set<Integer> deviceIds = IqParser.deviceIds(item);
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds);
+                registerDevices(account.getJid().asBareJid(), deviceIds);
             }
+
         });
     }
 
@@ -455,40 +451,37 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
     private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
         final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
-        IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
-        mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
-                final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet);
-                if (firstAttempt && preConditionNotMet) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration");
-                    mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
-                        @Override
-                        public void onPushSucceeded() {
-                            publishDeviceIdsAndRefineAccessModel(ids, false);
-                        }
-
-                        @Override
-                        public void onPushFailed() {
-                            publishDeviceIdsAndRefineAccessModel(ids, false);
-                        }
-                    });
-                } else {
-                    if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode");
-                        account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
-                        mXmppConnectionService.databaseBackend.updateAccount(account);
+        final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
+        mXmppConnectionService.sendIqPacket(account, publish, response -> {
+            final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
+            final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response);
+            if (firstAttempt && preConditionNotMet) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration");
+                mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
+                    @Override
+                    public void onPushSucceeded() {
+                        publishDeviceIdsAndRefineAccessModel(ids, false);
                     }
-                    if (packet.getType() == IqPacket.TYPE.ERROR) {
-                        if (preConditionNotMet) {
-                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt");
-                        } else if (error != null) {
-                            pepBroken = true;
-                            Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
-                        }
 
+                    @Override
+                    public void onPushFailed() {
+                        publishDeviceIdsAndRefineAccessModel(ids, false);
                     }
+                });
+            } else {
+                if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode");
+                    account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
+                    mXmppConnectionService.databaseBackend.updateAccount(account);
+                }
+                if (response.getType() == Iq.Type.ERROR) {
+                    if (preConditionNotMet) {
+                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt");
+                    } else if (error != null) {
+                        pepBroken = true;
+                        Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error"));
+                    }
+
                 }
             }
         });
@@ -506,26 +499,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             verifier.initSign(x509PrivateKey, SECURE_RANDOM);
             verifier.update(axolotlPublicKey.serialize());
             byte[] signature = verifier.sign();
-            IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
+            final Iq packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
             Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId());
-            mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
-                @Override
-                public void onIqPacketReceived(final Account account, IqPacket packet) {
-                    String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
-                    mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
-                        @Override
-                        public void onPushSucceeded() {
-                            Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable");
-                            publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
-                        }
+            mXmppConnectionService.sendIqPacket(account, packet, response -> {
+                String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
+                mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
+                    @Override
+                    public void onPushSucceeded() {
+                        Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable");
+                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
+                    }
 
-                        @Override
-                        public void onPushFailed() {
-                            Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node");
-                            publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
-                        }
-                    });
-                }
+                    @Override
+                    public void onPushFailed() {
+                        Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node");
+                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
+                    }
+                });
             });
         } catch (Exception e) {
             e.printStackTrace();
@@ -549,109 +539,106 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         if (this.changeAccessMode.get()) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model");
         }
-        IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
-        mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
+        final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
+        mXmppConnectionService.sendIqPacket(account, packet, response -> {
 
-                if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
-                    return; //ignore timeout. do nothing
-                }
+            if (response.getType() == Iq.Type.TIMEOUT) {
+                return; //ignore timeout. do nothing
+            }
 
-                if (packet.getType() == IqPacket.TYPE.ERROR) {
-                    Element error = packet.findChild("error");
-                    if (error == null || !error.hasChild("item-not-found")) {
-                        pepBroken = true;
-                        Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet);
-                        return;
-                    }
+            if (response.getType() == Iq.Type.ERROR) {
+                Element error = response.findChild("error");
+                if (error == null || !error.hasChild("item-not-found")) {
+                    pepBroken = true;
+                    Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response);
+                    return;
                 }
+            }
 
-                PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
-                Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
-                boolean flush = false;
-                if (bundle == null) {
-                    Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
-                    bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
-                    flush = true;
-                }
-                if (keys == null) {
-                    Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
+            PreKeyBundle bundle = IqParser.bundle(response);
+            final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
+            boolean flush = false;
+            if (bundle == null) {
+                Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response);
+                bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
+                flush = true;
+            }
+            if (keys == null) {
+                Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response);
+            }
+            try {
+                boolean changed = false;
+                // Validate IdentityKey
+                IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
+                if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
+                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
+                    changed = true;
                 }
-                try {
-                    boolean changed = false;
-                    // Validate IdentityKey
-                    IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
-                    if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
-                        Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
-                        changed = true;
-                    }
 
-                    // Validate signedPreKeyRecord + ID
-                    SignedPreKeyRecord signedPreKeyRecord;
-                    int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
-                    try {
-                        signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
-                        if (flush
-                                || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
-                                || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
-                            Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
-                            signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
-                            axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
-                            changed = true;
-                        }
-                    } catch (InvalidKeyIdException e) {
+                // Validate signedPreKeyRecord + ID
+                SignedPreKeyRecord signedPreKeyRecord;
+                int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
+                try {
+                    signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+                    if (flush
+                            || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+                            || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
                         Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
                         signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
                         axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
                         changed = true;
                     }
+                } catch (InvalidKeyIdException e) {
+                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
+                    signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
+                    axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+                    changed = true;
+                }
 
-                    // Validate PreKeys
-                    Set<PreKeyRecord> preKeyRecords = new HashSet<>();
-                    if (keys != null) {
-                        for (Integer id : keys.keySet()) {
-                            try {
-                                PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
-                                if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
-                                    preKeyRecords.add(preKeyRecord);
-                                }
-                            } catch (InvalidKeyIdException ignored) {
+                // Validate PreKeys
+                Set<PreKeyRecord> preKeyRecords = new HashSet<>();
+                if (keys != null) {
+                    for (Integer id : keys.keySet()) {
+                        try {
+                            PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
+                            if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
+                                preKeyRecords.add(preKeyRecord);
                             }
+                        } catch (InvalidKeyIdException ignored) {
                         }
                     }
-                    int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
-                    if (newKeys > 0) {
-                        List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
-                                axolotlStore.getCurrentPreKeyId() + 1, newKeys);
-                        preKeyRecords.addAll(newRecords);
-                        for (PreKeyRecord record : newRecords) {
-                            axolotlStore.storePreKey(record.getId(), record);
-                        }
-                        changed = true;
-                        Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
+                }
+                int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
+                if (newKeys > 0) {
+                    List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
+                            axolotlStore.getCurrentPreKeyId() + 1, newKeys);
+                    preKeyRecords.addAll(newRecords);
+                    for (PreKeyRecord record : newRecords) {
+                        axolotlStore.storePreKey(record.getId(), record);
                     }
+                    changed = true;
+                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
+                }
 
 
-                    if (changed || changeAccessMode.get()) {
-                        if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
-                            mXmppConnectionService.publishDisplayName(account);
-                            publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
-                        } else {
-                            publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
-                        }
+                if (changed || changeAccessMode.get()) {
+                    if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
+                        mXmppConnectionService.publishDisplayName(account);
+                        publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
                     } else {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
-                        if (wipe) {
-                            wipeOtherPepDevices();
-                        } else if (announce) {
-                            Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
-                            publishOwnDeviceIdIfNeeded();
-                        }
+                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
+                    }
+                } else {
+                    Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
+                    if (wipe) {
+                        wipeOtherPepDevices();
+                    } else if (announce) {
+                        Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
+                        publishOwnDeviceIdIfNeeded();
                     }
-                } catch (InvalidKeyException e) {
-                    Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
                 }
+            } catch (InvalidKeyException e) {
+                Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
             }
         });
     }
@@ -669,44 +656,41 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                                      final boolean wipe,
                                      final boolean firstAttempt) {
         final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
-        final IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
+        final Iq publish = mXmppConnectionService.getIqGenerator().publishBundles(
                 signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
                 preKeyRecords, getOwnDeviceId(), publishOptions);
         Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing...");
-        mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(final Account account, IqPacket packet) {
-                final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet);
-                if (firstAttempt && preconditionNotMet) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration");
-                    final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
-                    mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
-                        @Override
-                        public void onPushSucceeded() {
-                            publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
-                        }
-
-                        @Override
-                        public void onPushFailed() {
-                            publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
-                        }
-                    });
-                } else if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
-                    if (wipe) {
-                        wipeOtherPepDevices();
-                    } else if (announceAfter) {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
-                        publishOwnDeviceIdIfNeeded();
+        mXmppConnectionService.sendIqPacket(account, publish, response -> {
+            final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response);
+            if (firstAttempt && preconditionNotMet) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration");
+                final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
+                mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
+                    @Override
+                    public void onPushSucceeded() {
+                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
                     }
-                } else if (packet.getType() == IqPacket.TYPE.ERROR) {
-                    if (preconditionNotMet) {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt");
-                    } else {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.toString());
+
+                    @Override
+                    public void onPushFailed() {
+                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
                     }
-                    pepBroken = true;
+                });
+            } else if (response.getType() == Iq.Type.RESULT) {
+                Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
+                if (wipe) {
+                    wipeOtherPepDevices();
+                } else if (announceAfter) {
+                    Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
+                    publishOwnDeviceIdIfNeeded();
+                }
+            } else if (response.getType() == Iq.Type.ERROR) {
+                if (preconditionNotMet) {
+                    Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt");
+                } else {
+                    Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString());
                 }
+                pepBroken = true;
             }
         });
     }
@@ -759,9 +743,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             return Futures.immediateFuture(session);
         }
         final SettableFuture<XmppAxolotlSession> future = SettableFuture.create();
-        final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
-        mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
-            Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(response);
+        final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
+        mXmppConnectionService.sendIqPacket(account, packet, (response) -> {
+            Pair<X509Certificate[], byte[]> verification = IqParser.verification(response);
             if (verification != null) {
                 try {
                     Signature verifier = Signature.getInstance("sha256WithRSA");
@@ -846,7 +830,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) {
-        IqPacket packet;
+        final Iq packet;
         synchronized (this.fetchDeviceIdsMap) {
             List<OnDeviceIdsFetched> callbacks = this.fetchDeviceIdsMap.get(jid);
             if (callbacks != null) {
@@ -866,11 +850,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             }
         }
         if (packet != null) {
-            mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
-                if (response.getType() == IqPacket.TYPE.RESULT) {
+            mXmppConnectionService.sendIqPacket(account, packet, (response) -> {
+                if (response.getType() == Iq.Type.RESULT) {
                     fetchDeviceListStatus.put(jid, true);
-                    Element item = mXmppConnectionService.getIqParser().getItem(response);
-                    Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+                    final Element item = IqParser.getItem(response);
+                    final Set<Integer> deviceIds = IqParser.deviceIds(item);
                     registerDevices(jid, deviceIds);
                     final List<OnDeviceIdsFetched> callbacks;
                     synchronized (fetchDeviceIdsMap) {
@@ -882,7 +866,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                         }
                     }
                 } else {
-                    if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
                         fetchDeviceListStatus.remove(jid);
                     } else {
                         fetchDeviceListStatus.put(jid, false);
@@ -929,16 +913,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         }
         final Jid jid = Jid.of(address.getName());
         final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid());
-        IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
-        mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+        final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
+        mXmppConnectionService.sendIqPacket(account, bundlesPacket, (packet) -> {
+            if (packet.getType() == Iq.Type.TIMEOUT) {
                 fetchStatusMap.put(address, FetchStatus.TIMEOUT);
                 sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout"));
-            } else if (packet.getType() == IqPacket.TYPE.RESULT) {
+            } else if (packet.getType() == Iq.Type.RESULT) {
                 Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
-                final IqParser parser = mXmppConnectionService.getIqParser();
-                final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
-                final PreKeyBundle bundle = parser.bundle(packet);
+                final List<PreKeyBundle> preKeyBundleList = IqParser.preKeys(packet);
+                final PreKeyBundle bundle = IqParser.bundle(packet);
                 if (preKeyBundleList.isEmpty() || bundle == null) {
                     Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
                     fetchStatusMap.put(address, FetchStatus.ERROR);
@@ -1544,7 +1527,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         axolotlMessage.addDevice(session, true);
         try {
             final Jid jid = Jid.of(session.getRemoteAddress().getName());
-            MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
+            final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
             mXmppConnectionService.sendMessagePacket(account, packet);
         } catch (IllegalArgumentException e) {
             throw new Error("Remote addresses are created from jid and should convert back to jid", e);

src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java 🔗

@@ -24,7 +24,7 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.forms.Field;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class ServiceDiscoveryResult {
 	public static final String TABLENAME = "discovery_results";
@@ -36,7 +36,7 @@ public class ServiceDiscoveryResult {
 	protected final List<String> features;
 	protected final List<Data> forms;
 	private final List<Identity> identities;
-	public ServiceDiscoveryResult(final IqPacket packet) {
+	public ServiceDiscoveryResult(final Iq packet) {
 		this.identities = new ArrayList<>();
 		this.features = new ArrayList<>();
 		this.forms = new ArrayList<>();
@@ -275,7 +275,7 @@ public class ServiceDiscoveryResult {
 		return values;
 	}
 
-	public static class Identity implements Comparable {
+	public static class Identity implements Comparable<Identity> {
 		protected final String type;
 		protected final String lang;
 		protected final String name;
@@ -323,8 +323,21 @@ public class ServiceDiscoveryResult {
 			return this.name;
 		}
 
-		public int compareTo(@NonNull Object other) {
-			Identity o = (Identity) other;
+		JSONObject toJSON() {
+			try {
+				JSONObject o = new JSONObject();
+				o.put("category", this.getCategory());
+				o.put("type", this.getType());
+				o.put("lang", this.getLang());
+				o.put("name", this.getName());
+				return o;
+			} catch (JSONException e) {
+				return null;
+			}
+		}
+
+		@Override
+		public int compareTo(final Identity o) {
 			int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
 			if (r == 0) {
 				r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
@@ -338,18 +351,5 @@ public class ServiceDiscoveryResult {
 
 			return r;
 		}
-
-		JSONObject toJSON() {
-			try {
-				JSONObject o = new JSONObject();
-				o.put("category", this.getCategory());
-				o.put("type", this.getType());
-				o.put("lang", this.getLang());
-				o.put("name", this.getName());
-				return o;
-			} catch (JSONException e) {
-				return null;
-			}
-		}
 	}
 }

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -34,7 +34,7 @@ import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.pep.Avatar;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class IqGenerator extends AbstractGenerator {
 
@@ -42,8 +42,8 @@ public class IqGenerator extends AbstractGenerator {
         super(service);
     }
 
-    public IqPacket discoResponse(final Account account, final IqPacket request) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT);
+    public Iq discoResponse(final Account account, final Iq request) {
+        final var packet = new Iq(Iq.Type.RESULT);
         packet.setId(request.getId());
         packet.setTo(request.getFrom());
         final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
@@ -58,8 +58,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket versionResponse(final IqPacket request) {
-        final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
+    public Iq versionResponse(final Iq request) {
+        final var packet = request.generateResponse(Iq.Type.RESULT);
         Element query = packet.query("jabber:iq:version");
         query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
         query.addChild("version").setContent(getIdentityVersion());
@@ -71,8 +71,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket entityTimeResponse(IqPacket request) {
-        final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
+    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));
@@ -91,14 +91,14 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket purgeOfflineMessages() {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public static Iq purgeOfflineMessages() {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
         return packet;
     }
 
-    protected IqPacket publish(final String node, final Element item, final Bundle options) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    protected Iq publish(final String node, final Element item, final Bundle options) {
+        final var packet = new Iq(Iq.Type.SET);
         final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
         final Element publish = pubsub.addChild("publish");
         publish.setAttribute("node", node);
@@ -110,12 +110,12 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    protected IqPacket publish(final String node, final Element item) {
+    protected Iq publish(final String node, final Element item) {
         return publish(node, item, null);
     }
 
-    private IqPacket retrieve(String node, Element item) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    private Iq retrieve(String node, Element item) {
+        final var packet = new Iq(Iq.Type.GET);
         final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
         final Element items = pubsub.addChild("items");
         items.setAttribute("node", node);
@@ -125,30 +125,30 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket retrieveBookmarks() {
+    public Iq retrieveBookmarks() {
         return retrieve(Namespace.BOOKMARKS2, null);
     }
 
-    public IqPacket retrieveMds() {
+    public Iq retrieveMds() {
         return retrieve(Namespace.MDS_DISPLAYED, null);
     }
 
-    public IqPacket publishNick(String nick) {
+    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 IqPacket deleteNode(final String node) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq deleteNode(final String node) {
+        final var packet = new Iq(Iq.Type.SET);
         final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
         pubsub.addChild("delete").setAttribute("node", node);
         return packet;
     }
 
-    public IqPacket deleteItem(final String node, final String id) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    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);
@@ -157,7 +157,7 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket publishAvatar(Avatar avatar, Bundle options) {
+    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);
@@ -165,14 +165,14 @@ public class IqGenerator extends AbstractGenerator {
         return publish(Namespace.AVATAR_DATA, item, options);
     }
 
-    public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle 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 IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle 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
@@ -186,57 +186,57 @@ public class IqGenerator extends AbstractGenerator {
         return publish(Namespace.AVATAR_METADATA, item, options);
     }
 
-    public IqPacket retrievePepAvatar(final Avatar avatar) {
+    public Iq retrievePepAvatar(final Avatar avatar) {
         final Element item = new Element("item");
         item.setAttribute("id", avatar.sha1sum);
-        final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item);
+        final var packet = retrieve(Namespace.AVATAR_DATA, item);
         packet.setTo(avatar.owner);
         return packet;
     }
 
-    public IqPacket retrieveVcardAvatar(final Avatar avatar) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq retrieveVcardAvatar(final Avatar avatar) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(avatar.owner);
         packet.addChild("vCard", "vcard-temp");
         return packet;
     }
 
-    public IqPacket retrieveVcardAvatar(final Jid to) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    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 IqPacket retrieveAvatarMetaData(final Jid to) {
-        final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
+    public Iq retrieveAvatarMetaData(final Jid to) {
+        final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
         if (to != null) {
             packet.setTo(to);
         }
         return packet;
     }
 
-    public IqPacket retrieveDeviceIds(final Jid to) {
-        final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
+    public Iq retrieveDeviceIds(final Jid to) {
+        final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
         if (to != null) {
             packet.setTo(to);
         }
         return packet;
     }
 
-    public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
-        final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
+    public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
+        final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
         packet.setTo(to);
         return packet;
     }
 
-    public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) {
-        final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
+    public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
+        final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
         packet.setTo(to);
         return packet;
     }
 
-    public IqPacket publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
+    public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
         final Element item = new Element("item");
         item.setAttribute("id", "current");
         final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
@@ -286,7 +286,7 @@ public class IqGenerator extends AbstractGenerator {
         return displayed;
     }
 
-    public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
+    public Iq publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
                                    final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) {
         final Element item = new Element("item");
         item.setAttribute("id", "current");
@@ -310,7 +310,7 @@ public class IqGenerator extends AbstractGenerator {
         return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
     }
 
-    public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
+    public Iq publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
         final Element item = new Element("item");
         item.setAttribute("id", "current");
         final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
@@ -328,8 +328,8 @@ public class IqGenerator extends AbstractGenerator {
         return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
     }
 
-    public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
+        final Iq packet = new Iq(Iq.Type.SET);
         final Element query = packet.query(mam.version.namespace);
         query.setAttribute("queryid", mam.getQueryId());
         final Data data = new Data();
@@ -359,15 +359,15 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket generateGetBlockList() {
-        final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
+    public Iq generateGetBlockList() {
+        final Iq iq = new Iq(Iq.Type.GET);
         iq.addChild("blocklist", Namespace.BLOCKING);
 
         return iq;
     }
 
-    public IqPacket generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) {
-        final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+    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) {
@@ -383,15 +383,15 @@ public class IqGenerator extends AbstractGenerator {
         return iq;
     }
 
-    public IqPacket generateSetUnblockRequest(final Jid jid) {
-        final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+    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 IqPacket generateSetPassword(final Account account, final String newPassword) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq generateSetPassword(final Account account, final String newPassword) {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(account.getDomain());
         final Element query = packet.addChild("query", Namespace.REGISTER);
         final Jid jid = account.getJid();
@@ -400,14 +400,14 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) {
+    public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
         List<Jid> jids = new ArrayList<>();
         jids.add(jid);
         return changeAffiliation(conference, jids, affiliation);
     }
 
-    public IqPacket changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(conference.getJid().asBareJid());
         packet.setFrom(conference.getAccount().getJid());
         Element query = packet.query("http://jabber.org/protocol/muc#admin");
@@ -419,8 +419,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket changeRole(Conversation conference, String nick, String role) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq changeRole(Conversation conference, String nick, String role) {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(conference.getJid().asBareJid());
         packet.setFrom(conference.getAccount().getJid());
         Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
@@ -429,8 +429,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(host);
         Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
         request.setAttribute("filename", convertFilename(file.getName()));
@@ -439,8 +439,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(host);
         Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY);
         request.addChild("filename").setContent(convertFilename(file.getName()));
@@ -466,8 +466,8 @@ public class IqGenerator extends AbstractGenerator {
         }
     }
 
-    public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
-        final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
+    public static Iq generateCreateAccountWithCaptcha(final Account account, final String id, final Data data) {
+        final Iq register = new Iq(Iq.Type.SET);
         register.setFrom(account.getJid().asBareJid());
         register.setTo(account.getDomain());
         register.setId(id);
@@ -478,12 +478,12 @@ public class IqGenerator extends AbstractGenerator {
         return register;
     }
 
-    public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
+    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
         return pushTokenToAppServer(appServer, token, deviceId, null);
     }
 
-    public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(appServer);
         final Element command = packet.addChild("command", Namespace.COMMANDS);
         command.setAttribute("node", "register-push-fcm");
@@ -499,8 +499,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
-        final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
+        final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(appServer);
         final Element command = packet.addChild("command", Namespace.COMMANDS);
         command.setAttribute("node", "unregister-push-fcm");
@@ -513,8 +513,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket enablePush(final Jid jid, final String node, final String secret) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq enablePush(final Jid jid, final String node, final String secret) {
+        final Iq packet = new Iq(Iq.Type.SET);
         Element enable = packet.addChild("enable", Namespace.PUSH);
         enable.setAttribute("jid", jid);
         enable.setAttribute("node", node);
@@ -528,16 +528,16 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket disablePush(final Jid jid, final String node) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq disablePush(final Jid jid, final String node) {
+        Iq packet = new Iq(Iq.Type.SET);
         Element disable = packet.addChild("disable", Namespace.PUSH);
         disable.setAttribute("jid", jid);
         disable.setAttribute("node", node);
         return packet;
     }
 
-    public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq queryAffiliation(Conversation conversation, String affiliation) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(conversation.getJid().asBareJid());
         packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation);
         return packet;
@@ -570,16 +570,16 @@ public class IqGenerator extends AbstractGenerator {
         return options;
     }
 
-    public IqPacket requestPubsubConfiguration(Jid jid, String node) {
+    public Iq requestPubsubConfiguration(Jid jid, String node) {
         return pubsubConfiguration(jid, node, null);
     }
 
-    public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) {
+    public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
         return pubsubConfiguration(jid, node, data);
     }
 
-    private IqPacket pubsubConfiguration(Jid jid, String node, Data data) {
-        IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
+    private Iq pubsubConfiguration(Jid jid, String node, Data data) {
+        final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
         packet.setTo(jid);
         Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
         Element configure = pubsub.addChild("configure").setAttribute("node", node);
@@ -589,15 +589,15 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket queryDiscoItems(Jid jid) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq queryDiscoItems(final Jid jid) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(jid);
         packet.addChild("query",Namespace.DISCO_ITEMS);
         return packet;
     }
 
-    public IqPacket queryDiscoInfo(Jid jid) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq queryDiscoInfo(final Jid jid) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(jid);
         packet.addChild("query",Namespace.DISCO_INFO);
         return packet;

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -22,7 +22,6 @@ 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.stanzas.Reason;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public class MessageGenerator extends AbstractGenerator {
     private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo";
@@ -32,25 +31,25 @@ public class MessageGenerator extends AbstractGenerator {
         super(service);
     }
 
-    private MessagePacket preparePacket(Message message) {
+    private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message) {
         Conversation conversation = (Conversation) message.getConversation();
         Account account = conversation.getAccount();
-        MessagePacket packet = new MessagePacket();
+        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
         final boolean isWithSelf = conversation.getContact().isSelf();
         if (conversation.getMode() == Conversation.MODE_SINGLE) {
             packet.setTo(message.getCounterpart());
-            packet.setType(MessagePacket.TYPE_CHAT);
+            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
             if (!isWithSelf) {
                 packet.addChild("request", "urn:xmpp:receipts");
             }
         } else if (message.isPrivateMessage()) {
             packet.setTo(message.getCounterpart());
-            packet.setType(MessagePacket.TYPE_CHAT);
+            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
             packet.addChild("x", "http://jabber.org/protocol/muc#user");
             packet.addChild("request", "urn:xmpp:receipts");
         } else {
             packet.setTo(message.getCounterpart().asBareJid());
-            packet.setType(MessagePacket.TYPE_GROUPCHAT);
+            packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
         }
         if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) {
             packet.addChild("markable", "urn:xmpp:chat-markers:0");
@@ -66,7 +65,7 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public void addDelay(MessagePacket packet, long timestamp) {
+    public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
         final SimpleDateFormat mDateFormat = new SimpleDateFormat(
                 "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
         mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -75,8 +74,8 @@ public class MessageGenerator extends AbstractGenerator {
         delay.setAttribute("stamp", mDateFormat.format(date));
     }
 
-    public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
-        MessagePacket packet = preparePacket(message);
+    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
+        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
         if (axolotlMessage == null) {
             return null;
         }
@@ -89,17 +88,17 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
-        MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_CHAT);
+    public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
+        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
         packet.setTo(to);
         packet.setAxolotlMessage(axolotlMessage.toElement());
         packet.addChild("store", "urn:xmpp:hints");
         return packet;
     }
 
-    public MessagePacket generateChat(Message message) {
-        MessagePacket packet = preparePacket(message);
+    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
+        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
         String content;
         if (message.hasFileOnRemoteHost()) {
             final Message.FileParams fileParams = message.getFileParams();
@@ -112,8 +111,8 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket generatePgpChat(Message message) {
-        MessagePacket packet = preparePacket(message);
+    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
+        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message);
         if (message.hasFileOnRemoteHost()) {
             Message.FileParams fileParams = message.getFileParams();
             final String url = fileParams.url;
@@ -134,10 +133,10 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket generateChatState(Conversation conversation) {
+    public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) {
         final Account account = conversation.getAccount();
-        MessagePacket packet = new MessagePacket();
-        packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
         packet.setTo(conversation.getJid().asBareJid());
         packet.setFrom(account.getJid());
         packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
@@ -146,11 +145,11 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket confirm(final Message message) {
+    public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) {
         final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
         final Jid to = message.getCounterpart();
-        final MessagePacket packet = new MessagePacket();
-        packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
         packet.setTo(groupChat ? to.asBareJid() : to);
         final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
         if (groupChat) {
@@ -168,18 +167,18 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket conferenceSubject(Conversation conversation, String subject) {
-        MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_GROUPCHAT);
+    public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) {
+        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
         packet.setTo(conversation.getJid().asBareJid());
         packet.addChild("subject").setContent(subject);
         packet.setFrom(conversation.getAccount().getJid().asBareJid());
         return packet;
     }
 
-    public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
-        MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_NORMAL);
+    public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) {
+        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
         packet.setTo(contact);
         packet.setFrom(conversation.getAccount().getJid());
         Element x = packet.addChild("x", "jabber:x:conference");
@@ -195,8 +194,8 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket invite(final Conversation conversation, final Jid contact) {
-        final MessagePacket packet = new MessagePacket();
+    public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) {
+        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
         packet.setTo(conversation.getJid().asBareJid());
         packet.setFrom(conversation.getAccount().getJid());
         Element x = new Element("x");
@@ -208,8 +207,9 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
-        final MessagePacket receivedPacket = new MessagePacket();
+    public im.conversations.android.xmpp.model.stanza.Message received(Account account, final Jid from, final String id, ArrayList<String> namespaces, im.conversations.android.xmpp.model.stanza.Message.Type type) {
+        final var receivedPacket =
+                new im.conversations.android.xmpp.model.stanza.Message();
         receivedPacket.setType(type);
         receivedPacket.setTo(from);
         receivedPacket.setFrom(account.getJid());
@@ -220,8 +220,8 @@ public class MessageGenerator extends AbstractGenerator {
         return receivedPacket;
     }
 
-    public MessagePacket received(Account account, Jid to, String id) {
-        MessagePacket packet = new MessagePacket();
+    public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) {
+        im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
         packet.setFrom(account.getJid());
         packet.setTo(to);
         packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
@@ -229,10 +229,10 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket sessionFinish(
+    public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
             final Jid with, final String sessionId, final Reason reason) {
-        final MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_CHAT);
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
         packet.setTo(with);
         final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
         finish.setAttribute("id", sessionId);
@@ -242,9 +242,9 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
-        final MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
+    public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
         packet.setTo(proposal.with);
         packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
         final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
@@ -257,9 +257,9 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
-        final MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
+    public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
         packet.setTo(proposal.with);
         final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
         propose.setAttribute("id", proposal.sessionId);
@@ -268,9 +268,9 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket sessionReject(final Jid with, final String sessionId) {
-        final MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
+    public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) {
+        final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
         packet.setTo(with);
         final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
         propose.setAttribute("id", sessionId);

src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java 🔗

@@ -9,7 +9,6 @@ import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 
 public class PresenceGenerator extends AbstractGenerator {
 
@@ -17,20 +16,20 @@ public class PresenceGenerator extends AbstractGenerator {
         super(service);
     }
 
-    private PresencePacket subscription(String type, Contact contact) {
-        PresencePacket packet = new PresencePacket();
+    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 PresencePacket requestPresenceUpdatesFrom(final Contact contact) {
+    public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) {
         return requestPresenceUpdatesFrom(contact, null);
     }
 
-    public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) {
-        PresencePacket packet = subscription("subscribe", contact);
+    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);
@@ -41,24 +40,24 @@ public class PresenceGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public PresencePacket stopPresenceUpdatesFrom(Contact contact) {
+    public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(Contact contact) {
         return subscription("unsubscribe", contact);
     }
 
-    public PresencePacket stopPresenceUpdatesTo(Contact contact) {
+    public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(Contact contact) {
         return subscription("unsubscribed", contact);
     }
 
-    public PresencePacket sendPresenceUpdatesTo(Contact contact) {
+    public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(Contact contact) {
         return subscription("subscribed", contact);
     }
 
-    public PresencePacket selfPresence(Account account, Presence.Status status) {
+    public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Status status) {
         return selfPresence(account, status, true);
     }
 
-    public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal) {
-        final PresencePacket packet = new PresencePacket();
+    public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal) {
+        final im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
         if (personal) {
             final String sig = account.getPgpSignature();
             final String message = account.getPresenceStatusMessage();
@@ -83,16 +82,16 @@ public class PresenceGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public PresencePacket leave(final MucOptions mucOptions) {
-        PresencePacket presencePacket = new PresencePacket();
-        presencePacket.setTo(mucOptions.getSelf().getFullJid());
-        presencePacket.setFrom(mucOptions.getAccount().getJid());
-        presencePacket.setAttribute("type", "unavailable");
-        return presencePacket;
+    public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) {
+        im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence();
+        presence.setTo(mucOptions.getSelf().getFullJid());
+        presence.setFrom(mucOptions.getAccount().getJid());
+        presence.setAttribute("type", "unavailable");
+        return presence;
     }
 
-    public PresencePacket sendOfflinePresence(Account account) {
-        PresencePacket packet = new PresencePacket();
+    public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) {
+        im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
         packet.setFrom(account.getJid());
         packet.setAttribute("type", "unavailable");
         return packet;

src/main/java/eu/siacs/conversations/http/SlotRequester.java 🔗

@@ -43,7 +43,7 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.IqResponseException;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 import okhttp3.Headers;
 import okhttp3.HttpUrl;
 
@@ -67,9 +67,9 @@ public class SlotRequester {
 
     private ListenableFuture<Slot> requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) {
         final SettableFuture<Slot> future = SettableFuture.create();
-        final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
-        service.sendIqPacket(account, request, (a, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.RESULT) {
+        final Iq request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
+        service.sendIqPacket(account, request, (packet) -> {
+            if (packet.getType() == Iq.Type.RESULT) {
                 final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
                 if (slotElement != null) {
                     try {
@@ -97,9 +97,9 @@ public class SlotRequester {
 
     private ListenableFuture<Slot> requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) {
         final SettableFuture<Slot> future = SettableFuture.create();
-        final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
-        service.sendIqPacket(account, request, (a, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.RESULT) {
+        final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
+        service.sendIqPacket(account, request, (packet) -> {
+            if (packet.getType() == Iq.Type.RESULT) {
                 final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
                 if (slotElement != null) {
                     try {

src/main/java/eu/siacs/conversations/parser/AbstractParser.java 🔗

@@ -16,14 +16,16 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import im.conversations.android.xmpp.model.stanza.Stanza;
 
 public abstract class AbstractParser {
 
-	protected XmppConnectionService mXmppConnectionService;
+	protected final XmppConnectionService mXmppConnectionService;
+	protected final Account account;
 
-	protected AbstractParser(XmppConnectionService service) {
+	protected AbstractParser(final XmppConnectionService service, final Account account) {
 		this.mXmppConnectionService = service;
+		this.account = account;
 	}
 
 	public static Long parseTimestamp(Element element, Long d) {
@@ -34,8 +36,8 @@ public abstract class AbstractParser {
 		long min = Long.MAX_VALUE;
 		boolean returnDefault = true;
 		final Jid to;
-		if (ignoreCsiAndSm && element instanceof AbstractStanza) {
-			to = ((AbstractStanza) element).getTo();
+		if (ignoreCsiAndSm && element instanceof Stanza stanza) {
+			to = stanza.getTo();
 		} else {
 			to = null;
 		}
@@ -123,7 +125,7 @@ public abstract class AbstractParser {
 		contact.setLastResource(from.isBareJid() ? "" : from.getResource());
 	}
 
-	protected String avatarData(Element items) {
+	protected static String avatarData(Element items) {
 		Element item = items.findChild("item");
 		if (item == null) {
 			return null;

src/main/java/eu/siacs/conversations/parser/IqParser.java 🔗

@@ -26,6 +26,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -37,18 +38,17 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
-public class IqParser extends AbstractParser implements OnIqPacketReceived {
+public class IqParser extends AbstractParser implements Consumer<Iq> {
 
-    public IqParser(final XmppConnectionService service) {
-        super(service);
+    public IqParser(final XmppConnectionService service, final Account account) {
+        super(service, account);
     }
 
-    public static List<Jid> items(IqPacket packet) {
+    public static List<Jid> items(final Iq packet) {
         ArrayList<Jid> items = new ArrayList<>();
         final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
         if (query == null) {
@@ -65,7 +65,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         return items;
     }
 
-    public static Room parseRoom(IqPacket packet) {
+    public static Room parseRoom(Iq packet) {
         final Element query = packet.findChild("query", Namespace.DISCO_INFO);
         if (query == null) {
             return null;
@@ -143,7 +143,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         mXmppConnectionService.syncRoster(account);
     }
 
-    public String avatarData(final IqPacket packet) {
+    public static String avatarData(final Iq packet) {
         final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
         if (pubsub == null) {
             return null;
@@ -152,10 +152,10 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         if (items == null) {
             return null;
         }
-        return super.avatarData(items);
+        return AbstractParser.avatarData(items);
     }
 
-    public Element getItem(final IqPacket packet) {
+    public static Element getItem(final Iq packet) {
         final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
         if (pubsub == null) {
             return null;
@@ -168,7 +168,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
     }
 
     @NonNull
-    public Set<Integer> deviceIds(final Element item) {
+    public static Set<Integer> deviceIds(final Element item) {
         Set<Integer> deviceIds = new HashSet<>();
         if (item != null) {
             final Element list = item.findChild("list");
@@ -189,7 +189,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         return deviceIds;
     }
 
-    private Integer signedPreKeyId(final Element bundle) {
+    private static Integer signedPreKeyId(final Element bundle) {
         final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
         if (signedPreKeyPublic == null) {
             return null;
@@ -201,7 +201,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         }
     }
 
-    private ECPublicKey signedPreKeyPublic(final Element bundle) {
+    private static ECPublicKey signedPreKeyPublic(final Element bundle) {
         ECPublicKey publicKey = null;
         final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic");
         if (signedPreKeyPublic == null) {
@@ -215,7 +215,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         return publicKey;
     }
 
-    private byte[] signedPreKeySignature(final Element bundle) {
+    private static byte[] signedPreKeySignature(final Element bundle) {
         final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature");
         if (signedPreKeySignature == null) {
             return null;
@@ -228,7 +228,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         }
     }
 
-    private IdentityKey identityKey(final Element bundle) {
+    private static IdentityKey identityKey(final Element bundle) {
         final String identityKey = bundle.findChildContent("identityKey");
         if (identityKey == null) {
             return null;
@@ -241,7 +241,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         }
     }
 
-    public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
+    public static Map<Integer, ECPublicKey> preKeyPublics(final Iq packet) {
         Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
         Element item = getItem(packet);
         if (item == null) {
@@ -284,7 +284,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input));
     }
 
-    public Pair<X509Certificate[], byte[]> verification(final IqPacket packet) {
+    public static Pair<X509Certificate[], byte[]> verification(final Iq packet) {
         Element item = getItem(packet);
         Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
         Element chain = verification != null ? verification.findChild("chain") : null;
@@ -312,7 +312,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
         }
     }
 
-    public PreKeyBundle bundle(final IqPacket bundle) {
+    public static PreKeyBundle bundle(final Iq bundle) {
         final Element bundleItem = getItem(bundle);
         if (bundleItem == null) {
             return null;
@@ -336,7 +336,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
                 signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
     }
 
-    public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
+    public static List<PreKeyBundle> preKeys(final Iq preKeys) {
         List<PreKeyBundle> bundles = new ArrayList<>();
         Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
         if (preKeyPublics != null) {
@@ -351,15 +351,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
     }
 
     @Override
-    public void onIqPacketReceived(final Account account, final IqPacket packet) {
-        final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
-        if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
+    public void accept(final Iq packet) {
+        final boolean isGet = packet.getType() == Iq.Type.GET;
+        if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
             return;
         }
         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() == IqPacket.TYPE.RESULT) {
+            if (packet.getType() == Iq.Type.RESULT) {
                 account.getRoster().markAllAsNotInRoster();
             }
             this.rosterItems(account, query);
@@ -373,7 +373,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
                     (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() == IqPacket.TYPE.RESULT) {
+            if (packet.getType() == Iq.Type.RESULT) {
                 account.clearBlocklist();
                 account.getXmppConnection().getFeatures().setBlockListRequested(true);
             }
@@ -389,7 +389,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
                     }
                 }
                 account.getBlocklist().addAll(jids);
-                if (packet.getType() == IqPacket.TYPE.SET) {
+                if (packet.getType() == Iq.Type.SET) {
                     boolean removed = false;
                     for (Jid jid : jids) {
                         removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
@@ -401,15 +401,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
             }
             // Update the UI
             mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
-            if (packet.getType() == IqPacket.TYPE.SET) {
-                final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
+            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() == IqPacket.TYPE.SET) {
+                packet.fromServer(account) && packet.getType() == Iq.Type.SET) {
             Log.d(Config.LOGTAG, "Received unblock update from server");
             final Collection<Element> items = packet.findChild("unblock", Namespace.BLOCKING).getChildren();
-            if (items.size() == 0) {
+            if (items.isEmpty()) {
                 // No children to unblock == unblock all
                 account.getBlocklist().clear();
             } else {
@@ -425,7 +425,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
                 account.getBlocklist().removeAll(jids);
             }
             mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
-            final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
+            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")
@@ -433,18 +433,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
             mXmppConnectionService.getJingleConnectionManager()
                     .deliverIbbPacket(account, packet);
         } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
-            final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
+            final Iq response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
             mXmppConnectionService.sendIqPacket(account, response, null);
         } else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
-            final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
+            final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
             mXmppConnectionService.sendIqPacket(account, response, null);
         } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
-            final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
+            final Iq response = packet.generateResponse(Iq.Type.RESULT);
             mXmppConnectionService.sendIqPacket(account, response, null);
         } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
-            final IqPacket response;
+            final Iq response;
             if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
-                response = packet.generateResponse(IqPacket.TYPE.ERROR);
+                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");
@@ -452,18 +452,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
                 response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
             }
             mXmppConnectionService.sendIqPacket(account, response, null);
-        } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) {
+        } 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 IqPacket response;
+            final Iq response;
             if (success) {
-                response = packet.generateResponse(IqPacket.TYPE.RESULT);
+                response = packet.generateResponse(Iq.Type.RESULT);
             } else {
-                response = packet.generateResponse(IqPacket.TYPE.ERROR);
+                response = packet.generateResponse(Iq.Type.ERROR);
                 final Element error = response.addChild("error");
                 error.setAttribute("type", "cancel");
                 error.setAttribute("code", "404");
@@ -471,8 +471,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
             }
             mXmppConnectionService.sendIqPacket(account, response, null);
         } else {
-            if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
-                final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
+            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");

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -15,6 +15,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.Consumer;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
@@ -49,17 +50,20 @@ 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.pep.Avatar;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.carbons.Received;
+import im.conversations.android.xmpp.model.carbons.Sent;
+import im.conversations.android.xmpp.model.forward.Forwarded;
 
-public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
+public class MessageParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
 
     private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
 
     private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
             Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
 
-    public MessageParser(XmppConnectionService service) {
-        super(service);
+    public MessageParser(final XmppConnectionService service, final Account account) {
+        super(service, account);
     }
 
     private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
@@ -98,7 +102,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         return result != null ? result : fallback;
     }
 
-    private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
+    private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) {
         ChatState state = ChatState.parse(packet);
         if (state != null && c != null) {
             final Account account = c.getAccount();
@@ -240,7 +244,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             }
         } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
             Element item = items.findChild("item");
-            Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+            final Set<Integer> deviceIds = IqParser.deviceIds(item);
             Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
             final AxolotlService axolotlService = account.getAxolotlService();
             axolotlService.registerDevices(from, deviceIds);
@@ -347,10 +351,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         mXmppConnectionService.updateAccountUi();
     }
 
-    private boolean handleErrorMessage(final Account account, final MessagePacket packet) {
-        if (packet.getType() == MessagePacket.TYPE_ERROR) {
+    private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) {
+        if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
             if (packet.fromServer(account)) {
-                final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS);
+                final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS);
                 if (forwarded != null) {
                     return handleErrorMessage(account, forwarded.first);
                 }
@@ -393,11 +397,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
     }
 
     @Override
-    public void onMessagePacketReceived(Account account, MessagePacket original) {
+    public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
         if (handleErrorMessage(account, original)) {
             return;
         }
-        final MessagePacket packet;
+        final im.conversations.android.xmpp.model.stanza.Message packet;
         Long timestamp = null;
         boolean isCarbon = false;
         String serverMsgId = null;
@@ -411,7 +415,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
         final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
         if (query != null && query.validFrom(original.getFrom())) {
-            final Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
+            final var f = getForwardedMessagePacket(original,"result", query.version.namespace);
             if (f == null) {
                 return;
             }
@@ -426,9 +430,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
             return;
         } else if (original.fromServer(account)) {
-            Pair<MessagePacket, Long> f;
-            f = original.getForwardedMessagePacket("received", Namespace.CARBONS);
-            f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f;
+            Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
+            f = getForwardedMessagePacket(original, Received.class);
+            f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
             packet = f != null ? f.first : original;
             if (handleErrorMessage(account, packet)) {
                 return;
@@ -468,7 +472,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             return;
         }
 
-        boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
+        boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
         if (query != null && !query.muc() && isTypeGroupChat) {
             Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
             return;
@@ -1106,6 +1110,34 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         }
     }
 
+    private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class<? extends Extension> clazz) {
+        final var extension = original.getExtension(clazz);
+        final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
+        if (forwarded == null) {
+            return null;
+        }
+        final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
+        final var forwardedMessage = forwarded.getMessage();
+        if (forwardedMessage == null) {
+            return null;
+        }
+        return new Pair<>(forwardedMessage,timestamp);
+    }
+
+    private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) {
+        final Element wrapper = original.findChild(name, namespace);
+        final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD);
+        if (forwardedElement instanceof Forwarded forwarded) {
+            final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
+            final var forwardedMessage = forwarded.getMessage();
+            if (forwardedMessage == null) {
+                return null;
+            }
+            return new Pair<>(forwardedMessage,timestamp);
+        }
+        return null;
+    }
+
     private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
         final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
         if (conversation != null && (query == null || query.isCatchup())) {
@@ -1118,7 +1150,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         }
     }
 
-    private void processMessageReceipts(final Account account, final MessagePacket packet, final String remoteMsgId, MessageArchiveService.Query query) {
+    private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) {
         final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
         final boolean request = packet.hasChild("request", "urn:xmpp:receipts");
         if (query == null) {
@@ -1130,7 +1162,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                 receiptsNamespaces.add("urn:xmpp:receipts");
             }
             if (receiptsNamespaces.size() > 0) {
-                final MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
+                final var receipt = mXmppConnectionService.getMessageGenerator().received(account,
                         packet.getFrom(),
                         remoteMsgId,
                         receiptsNamespaces,

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -19,22 +19,21 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 import eu.siacs.conversations.xmpp.pep.Avatar;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 
 import org.openintents.openpgp.util.OpenPgpUtils;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
-public class PresenceParser extends AbstractParser implements OnPresencePacketReceived {
+public class PresenceParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Presence> {
 
-    public PresenceParser(XmppConnectionService service) {
-        super(service);
+    public PresenceParser(final XmppConnectionService service, final Account account) {
+        super(service, account);
     }
 
-    public void parseConferencePresence(PresencePacket packet, Account account) {
+    public void parseConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) {
         final Conversation conversation =
                 packet.getFrom() == null
                         ? null
@@ -58,7 +57,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe
         }
     }
 
-    private void processConferencePresence(PresencePacket packet, Conversation conversation) {
+    private void processConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Conversation conversation) {
         final Account account = conversation.getAccount();
         final MucOptions mucOptions = conversation.getMucOptions();
         final Jid jid = conversation.getAccount().getJid();
@@ -297,7 +296,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe
         return codes;
     }
 
-    private void parseContactPresence(final PresencePacket packet, final Account account) {
+    private void parseContactPresence(final im.conversations.android.xmpp.model.stanza.Presence packet, final Account account) {
         final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
         final Jid from = packet.getFrom();
         if (from == null || from.equals(account.getJid())) {
@@ -431,7 +430,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe
     }
 
     @Override
-    public void onPresencePacketReceived(Account account, PresencePacket packet) {
+    public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
         if (packet.hasChild("x", Namespace.MUC_USER)) {
             this.parseConferencePresence(packet, account);
         } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {

src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java 🔗

@@ -20,10 +20,9 @@ import eu.siacs.conversations.http.services.MuclumbusService;
 import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.utils.TLSSocketFactory;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.XmppConnection;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
+import im.conversations.android.xmpp.model.stanza.Iq;
 import okhttp3.OkHttpClient;
 import okhttp3.ResponseBody;
 
@@ -202,7 +201,7 @@ public class ChannelDiscoveryService {
             final String query, final OnChannelSearchResultsFound listener) {
         final Map<Jid, Account> localMucService = getLocalMucServices();
         Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
-        if (localMucService.size() == 0) {
+        if (localMucService.isEmpty()) {
             listener.onChannelSearchResultsFound(Collections.emptyList());
             return;
         }
@@ -216,39 +215,36 @@ public class ChannelDiscoveryService {
         }
         final AtomicInteger queriesInFlight = new AtomicInteger();
         final List<Room> rooms = new ArrayList<>();
-        for (Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
-            IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
+        for (final Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
+            Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
             queriesInFlight.incrementAndGet();
+            final var account = entry.getValue();
             service.sendIqPacket(
-                    entry.getValue(),
+                    account,
                     itemsRequest,
-                    (account, itemsResponse) -> {
-                        if (itemsResponse.getType() == IqPacket.TYPE.RESULT) {
+                    (itemsResponse) -> {
+                        if (itemsResponse.getType() == Iq.Type.RESULT) {
                             final List<Jid> items = IqParser.items(itemsResponse);
-                            for (Jid item : items) {
-                                IqPacket infoRequest =
+                            for (final Jid item : items) {
+                                final Iq infoRequest =
                                         service.getIqGenerator().queryDiscoInfo(item);
                                 queriesInFlight.incrementAndGet();
                                 service.sendIqPacket(
                                         account,
                                         infoRequest,
-                                        new OnIqPacketReceived() {
-                                            @Override
-                                            public void onIqPacketReceived(
-                                                    Account account, IqPacket infoResponse) {
-                                                if (infoResponse.getType()
-                                                        == IqPacket.TYPE.RESULT) {
-                                                    final Room room =
-                                                            IqParser.parseRoom(infoResponse);
-                                                    if (room != null) {
-                                                        rooms.add(room);
-                                                    }
-                                                    if (queriesInFlight.decrementAndGet() <= 0) {
-                                                        finishDiscoSearch(rooms, query, listener);
-                                                    }
-                                                } else {
-                                                    queriesInFlight.decrementAndGet();
+                                        infoResponse -> {
+                                            if (infoResponse.getType()
+                                                    == Iq.Type.RESULT) {
+                                                final Room room =
+                                                        IqParser.parseRoom(infoResponse);
+                                                if (room != null) {
+                                                    rooms.add(room);
                                                 }
+                                                if (queriesInFlight.decrementAndGet() <= 0) {
+                                                    finishDiscoSearch(rooms, query, listener);
+                                                }
+                                            } else {
+                                                queriesInFlight.decrementAndGet();
                                             }
                                         });
                             }

src/main/java/eu/siacs/conversations/services/MessageArchiveService.java 🔗

@@ -23,8 +23,8 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
 import eu.siacs.conversations.xmpp.mam.MamReference;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Message;
 
 public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 
@@ -81,7 +81,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             return false;
         }
 
-        public static Element findResult(MessagePacket packet) {
+        public static Element findResult(Message packet) {
             for (Version version : values()) {
                 Element result = packet.findChild("result", version.namespace);
                 if (result != null) {
@@ -234,17 +234,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
                 throw new IllegalStateException("Attempted to run MAM query for archived conversation");
             }
             Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString());
-            final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
-            this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> {
+            final Iq packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
+            this.mXmppConnectionService.sendIqPacket(account, packet, (p) -> {
                 final Element fin = p.findChild("fin", query.version.namespace);
-                if (p.getType() == IqPacket.TYPE.TIMEOUT) {
+                if (p.getType() == Iq.Type.TIMEOUT) {
                     synchronized (this.queries) {
                         this.queries.remove(query);
                         if (query.hasCallback()) {
                             query.callback(false);
                         }
                     }
-                } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) {
+                } else if (p.getType() == Iq.Type.RESULT && fin != null) {
                     final boolean running;
                     synchronized (this.queries) {
                         running = this.queries.contains(query);
@@ -254,10 +254,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
                     } else {
                         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring MAM iq result because query had been killed");
                     }
-                } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) {
+                } else if (p.getType() == Iq.Type.RESULT && query.isLegacy()) {
                     //do nothing
                 } else {
-                    Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString());
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": error executing mam: " + p.toString());
                     try {
                         finalizeQuery(query, true);
                     } catch (final IllegalStateException e) {
@@ -304,7 +304,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         }
     }
 
-    boolean inCatchup(Account account) {
+    public boolean inCatchup(Account account) {
         synchronized (this.queries) {
             for (Query query : queries) {
                 if (query.account == account && query.isCatchup() && query.getWith() == null) {

src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java 🔗

@@ -32,8 +32,9 @@ import eu.siacs.conversations.receiver.UnifiedPushDistributor;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Presence;
+
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.List;
@@ -82,7 +83,7 @@ public class UnifiedPushBroker {
     }
 
     private void sendDirectedPresence(final Account account, Jid to) {
-        final PresencePacket presence = new PresencePacket();
+        final var presence = new Presence();
         presence.setTo(to);
         service.sendPresencePacket(account, presence);
     }
@@ -146,7 +147,7 @@ public class UnifiedPushBroker {
                     UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
             final String hashedInstance =
                     UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
-            final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
+            final Iq registration = new Iq(Iq.Type.SET);
             registration.setTo(transport.transport);
             final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
             register.setAttribute("application", hashedApplication);
@@ -160,7 +161,7 @@ public class UnifiedPushBroker {
             this.service.sendIqPacket(
                     account,
                     registration,
-                    (a, response) -> processRegistration(transport, renewal, messenger, response));
+                    (response) -> processRegistration(transport, renewal, messenger, response));
         }
     }
 
@@ -168,8 +169,8 @@ public class UnifiedPushBroker {
             final Transport transport,
             final UnifiedPushDatabase.PushTarget renewal,
             final Messenger messenger,
-            final IqPacket response) {
-        if (response.getType() == IqPacket.TYPE.RESULT) {
+            final Iq response) {
+        if (response.getType() == Iq.Type.RESULT) {
             final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
             if (registered == null) {
                 return;

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -52,7 +52,6 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.app.RemoteInput;
 import androidx.core.content.ContextCompat;
-import androidx.core.util.Consumer;
 
 import com.google.common.base.Objects;
 import com.google.common.base.Optional;
@@ -92,6 +91,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
@@ -123,8 +123,6 @@ import eu.siacs.conversations.generator.PresenceGenerator;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.parser.AbstractParser;
 import eu.siacs.conversations.parser.IqParser;
-import eu.siacs.conversations.parser.MessageParser;
-import eu.siacs.conversations.parser.PresenceParser;
 import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.persistance.UnifiedPushDatabase;
@@ -141,7 +139,6 @@ import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.EasyOnboardingInvite;
-import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.QuickLoader;
@@ -160,11 +157,8 @@ import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnBindListener;
 import eu.siacs.conversations.xmpp.OnContactStatusChanged;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
-import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
-import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 import eu.siacs.conversations.xmpp.OnStatusChanged;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
@@ -178,9 +172,7 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 import eu.siacs.conversations.xmpp.mam.MamReference;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 import me.leolin.shortcutbadger.ShortcutBadger;
 
 public class XmppConnectionService extends Service {
@@ -225,12 +217,12 @@ public class XmppConnectionService extends Service {
     private final Set<String> mInProgressAvatarFetches = new HashSet<>();
     private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
     private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
-    private final OnIqPacketReceived mDefaultIqHandler = (account, packet) -> {
-        if (packet.getType() != IqPacket.TYPE.RESULT) {
-            Element error = packet.findChild("error");
+    private final Consumer<Iq> 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, account.getJid().asBareJid() + ": received iq error - " + text);
+                Log.d(Config.LOGTAG, "received iq error: " + text);
             }
         }
     };
@@ -247,9 +239,6 @@ public class XmppConnectionService extends Service {
     private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
     private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
     private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
-    private final OnMessagePacketReceived mMessageParser = new MessageParser(this);
-    private final OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
-    private final IqParser mIqParser = new IqParser(this);
     private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
     public OnContactStatusChanged onContactStatusChanged = (contact, online) -> {
         Conversation conversation = find(getConversations(), contact);
@@ -330,79 +319,6 @@ public class XmppConnectionService extends Service {
     public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
 
 
-    private final OnBindListener mOnBindListener = new OnBindListener() {
-
-        @Override
-        public void onBind(final Account account) {
-            synchronized (mInProgressAvatarFetches) {
-                for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
-                    final String KEY = iterator.next();
-                    if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
-                        iterator.remove();
-                    }
-                }
-            }
-            boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
-            boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0));
-            if (loggedInSuccessfully || gainedFeature) {
-                databaseBackend.updateAccount(account);
-            }
-
-            if (loggedInSuccessfully) {
-                if (!TextUtils.isEmpty(account.getDisplayName())) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing");
-                    publishDisplayName(account);
-                }
-            }
-
-            account.getRoster().clearPresences();
-            synchronized (account.inProgressConferenceJoins) {
-                account.inProgressConferenceJoins.clear();
-            }
-            synchronized (account.inProgressConferencePings) {
-                account.inProgressConferencePings.clear();
-            }
-            mJingleConnectionManager.notifyRebound(account);
-            mQuickConversationsService.considerSyncBackground(false);
-            fetchRosterFromServer(account);
-
-            final XmppConnection connection = account.getXmppConnection();
-
-            if (connection.getFeatures().bookmarks2()) {
-                fetchBookmarks2(account);
-            } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) {
-                fetchBookmarks(account);
-            }
-
-            if (connection.getFeatures().mds()) {
-                fetchMessageDisplayedSynchronization(account);
-            } else {
-                Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds");
-            }
-            final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval();
-            final boolean catchup = getMessageArchiveService().inCatchup(account);
-            final boolean trackOfflineMessageRetrieval;
-            if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) {
-                trackOfflineMessageRetrieval = false;
-                sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> {
-                    if (packet.getType() == IqPacket.TYPE.RESULT) {
-                        Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages");
-                    }
-                });
-            } else {
-                trackOfflineMessageRetrieval = true;
-            }
-            sendPresence(account);
-            account.getXmppConnection().trackOfflineMessageRetrieval(trackOfflineMessageRetrieval);
-            if (mPushManagementService.available(account)) {
-                mPushManagementService.registerPushTokenOnServer(account);
-            }
-            connectMultiModeConversations(account);
-            syncDirtyContacts(account);
-
-            unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account);
-        }
-    };
 
     private final AtomicLong mLastExpiryRun = new AtomicLong(0);
     private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
@@ -1636,12 +1552,8 @@ public class XmppConnectionService extends Service {
 
     public XmppConnection createConnection(final Account account) {
         final XmppConnection connection = new XmppConnection(account, this);
-        connection.setOnMessagePacketReceivedListener(this.mMessageParser);
         connection.setOnStatusChangedListener(this.statusListener);
-        connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
-        connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
         connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
-        connection.setOnBindListener(this.mOnBindListener);
         connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
         connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
         connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
@@ -1654,7 +1566,7 @@ public class XmppConnectionService extends Service {
 
     public void sendChatState(Conversation conversation) {
         if (sendChatStates()) {
-            MessagePacket packet = mMessageGenerator.generateChatState(conversation);
+            final var packet = mMessageGenerator.generateChatState(conversation);
             sendMessagePacket(conversation.getAccount(), packet);
         }
     }
@@ -1692,7 +1604,7 @@ public class XmppConnectionService extends Service {
             }
         }
 
-        MessagePacket packet = null;
+        im.conversations.android.xmpp.model.stanza.Message packet = null;
         final boolean addToConversation = !message.edited();
         boolean saveInDb = addToConversation;
         message.setStatus(Message.STATUS_WAITING);
@@ -1866,13 +1778,13 @@ public class XmppConnectionService extends Service {
             callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites));
             return;
         }
-        final IqPacket request = new IqPacket(IqPacket.TYPE.SET);
+        final Iq request = new Iq(Iq.Type.SET);
         request.setTo(jid);
         final Element command = request.addChild("command", Namespace.COMMANDS);
         command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
         command.setAttribute("action", "execute");
-        sendIqPacket(account, request, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        sendIqPacket(account, request, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 final Element resultCommand = response.findChild("command", Namespace.COMMANDS);
                 final Element x = resultCommand == null ? null : resultCommand.findChild("x", Namespace.DATA);
                 if (x != null) {
@@ -1887,7 +1799,7 @@ public class XmppConnectionService extends Service {
                 }
                 callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
                 Log.d(Config.LOGTAG, response.toString());
-            } else if (response.getType() == IqPacket.TYPE.ERROR) {
+            } else if (response.getType() == Iq.Type.ERROR) {
                 callback.inviteRequestFailed(IqParser.errorMessage(response));
             } else {
                 callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
@@ -1896,54 +1808,42 @@ public class XmppConnectionService extends Service {
 
     }
 
-    public void fetchRosterFromServer(final Account account) {
-        final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
-        if (!"".equals(account.getRosterVersion())) {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid()
-                    + ": fetching roster version " + account.getRosterVersion());
-        } else {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
-        }
-        iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion());
-        sendIqPacket(account, iqPacket, mIqParser);
-    }
-
     public void fetchBookmarks(final Account account) {
-        final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+        final Iq iqPacket = new Iq(Iq.Type.GET);
         final Element query = iqPacket.query("jabber:iq:private");
         query.addChild("storage", Namespace.BOOKMARKS);
-        final OnIqPacketReceived callback = (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        final Consumer<Iq> callback = (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 final Element query1 = response.query();
                 final Element storage = query1.findChild("storage", "storage:bookmarks");
                 Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
-                processBookmarksInitial(a, bookmarks, false);
+                processBookmarksInitial(account, bookmarks, false);
             } else {
-                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks");
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fetch bookmarks");
             }
         };
         sendIqPacket(account, iqPacket, callback);
     }
 
     public void fetchBookmarks2(final Account account) {
-        final IqPacket retrieve = mIqGenerator.retrieveBookmarks();
-        sendIqPacket(account, retrieve, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        final Iq retrieve = mIqGenerator.retrieveBookmarks();
+        sendIqPacket(account, retrieve, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
-                final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromPubsub(pubsub, a);
-                processBookmarksInitial(a, bookmarks, true);
+                final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromPubsub(pubsub, account);
+                processBookmarksInitial(account, bookmarks, true);
             }
         });
     }
 
-    private void fetchMessageDisplayedSynchronization(final Account account) {
+    public void fetchMessageDisplayedSynchronization(final Account account) {
         Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
         final var retrieve = mIqGenerator.retrieveMds();
         sendIqPacket(
                 account,
                 retrieve,
-                (a, response) -> {
-                    if (response.getType() != IqPacket.TYPE.RESULT) {
+                (response) -> {
+                    if (response.getType() != Iq.Type.RESULT) {
                         return;
                     }
                     final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
@@ -2096,11 +1996,11 @@ public class XmppConnectionService extends Service {
         account.removeBookmark(bookmark);
         final XmppConnection connection = account.getXmppConnection();
         if (connection.getFeatures().bookmarks2()) {
-            final IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
+            final Iq request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
             Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
-            sendIqPacket(account, request, (a, response) -> {
-                if (response.getType() == IqPacket.TYPE.ERROR) {
-                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition());
+            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()) {
@@ -2112,7 +2012,7 @@ public class XmppConnectionService extends Service {
 
     private void pushBookmarksPrivateXml(Account account) {
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
-        IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
+        final Iq iqPacket = new Iq(Iq.Type.SET);
         Element query = iqPacket.query("jabber:iq:private");
         Element storage = query.addChild("storage", "storage:bookmarks");
         for (final Bookmark bookmark : account.getBookmarks()) {
@@ -2137,9 +2037,9 @@ public class XmppConnectionService extends Service {
     }
 
     private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) {
-        final IqPacket packet = mIqGenerator.publishElement(node, element, id, options);
-        sendIqPacket(account, packet, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        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)) {
@@ -2610,6 +2510,10 @@ public class XmppConnectionService extends Service {
         return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
     }
 
+    public UnifiedPushBroker getUnifiedPushBroker() {
+        return this.unifiedPushBroker;
+    }
+
     private void provisionAccount(final String address, final String password) {
         final Jid jid = Jid.ofEscaped(address);
         final Account account = new Account(jid, password);
@@ -2708,12 +2612,12 @@ public class XmppConnectionService extends Service {
     }
 
     public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
-        final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
-        sendIqPacket(account, iq, (a, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.RESULT) {
-                a.setPassword(newPassword);
-                a.setOption(Account.OPTION_MAGIC_CREATE, false);
-                databaseBackend.updateAccount(a);
+        final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
+        sendIqPacket(account, iq, (packet) -> {
+            if (packet.getType() == Iq.Type.RESULT) {
+                account.setPassword(newPassword);
+                account.setOption(Account.OPTION_MAGIC_CREATE, false);
+                databaseBackend.updateAccount(account);
                 callback.onPasswordChangeSucceeded();
             } else {
                 callback.onPasswordChangeFailed();
@@ -2722,12 +2626,12 @@ public class XmppConnectionService extends Service {
     }
 
     public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
-        final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
+        final Iq iqPacket = new Iq(Iq.Type.SET);
         final Element query = iqPacket.addChild("query",Namespace.REGISTER);
         query.addChild("remove");
-        sendIqPacket(account, iqPacket, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
-                deleteAccount(a);
+        sendIqPacket(account, iqPacket, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
+                deleteAccount(account);
                 callback.accept(true);
             } else {
                 callback.accept(false);
@@ -3055,7 +2959,7 @@ public class XmppConnectionService extends Service {
         Log.d(Config.LOGTAG, "app switched into background");
     }
 
-    private void connectMultiModeConversations(Account account) {
+    public void connectMultiModeConversations(Account account) {
         List<Conversation> conversations = getConversations();
         for (Conversation conversation : conversations) {
             if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
@@ -3079,20 +2983,20 @@ public class XmppConnectionService extends Service {
             }
         }
         final Jid self = conversation.getMucOptions().getSelf().getFullJid();
-        final IqPacket ping = new IqPacket(IqPacket.TYPE.GET);
+        final Iq ping = new Iq(Iq.Type.GET);
         ping.setTo(self);
         ping.addChild("ping", Namespace.PING);
-        sendIqPacket(conversation.getAccount(), ping, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.ERROR) {
-                Element error = response.findChild("error");
+        sendIqPacket(conversation.getAccount(), ping, (response) -> {
+            if (response.getType() == Iq.Type.ERROR) {
+                final var error = response.getError();
                 if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) {
-                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error");
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error");
                 } else {
-                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin");
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin");
                     joinMuc(conversation);
                 }
-            } else if (response.getType() == IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back fine");
+            } else if (response.getType() == Iq.Type.RESULT) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back fine");
             }
             synchronized (account.inProgressConferencePings) {
                 account.inProgressConferencePings.remove(conversation);
@@ -3151,7 +3055,7 @@ public class XmppConnectionService extends Service {
 
                     final Jid joinJid = mucOptions.getSelf().getFullJid();
                     Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString());
-                    PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null);
+                    final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null);
                     packet.setTo(joinJid);
                     Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
                     if (conversation.getMucOptions().getPassword() != null) {
@@ -3240,16 +3144,16 @@ public class XmppConnectionService extends Service {
         final Account account = conversation.getAccount();
         final AxolotlService axolotlService = account.getAxolotlService();
         final String[] affiliations = {"member", "admin", "owner"};
-        OnIqPacketReceived callback = new OnIqPacketReceived() {
+        final Consumer<Iq> callback = new Consumer<Iq>() {
 
             private int i = 0;
             private boolean success = true;
 
             @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
+            public void accept(Iq response) {
                 final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
-                Element query = packet.query("http://jabber.org/protocol/muc#admin");
-                if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+                Element query = response.query("http://jabber.org/protocol/muc#admin");
+                if (response.getType() == Iq.Type.RESULT && query != null) {
                     for (Element child : query.getChildren()) {
                         if ("item".equals(child.getName())) {
                             MucOptions.User user = AbstractParser.parseItem(conversation, child);
@@ -3335,29 +3239,29 @@ public class XmppConnectionService extends Service {
     }
 
     private void deletePepNode(final Account account, final String node, final Runnable runnable) {
-        final IqPacket request = mIqGenerator.deleteNode(node);
-        sendIqPacket(account, request, (a, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node);
+        final Iq request = mIqGenerator.deleteNode(node);
+        sendIqPacket(account, request, (packet) -> {
+            if (packet.getType() == Iq.Type.RESULT) {
+                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully deleted pep node "+node);
                 if (runnable != null) {
                     runnable.run();
                 }
             } else {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet);
+                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to delete "+ packet);
             }
         });
     }
 
     private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
-        final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
-        sendIqPacket(account, retrieveVcard, (a, response) -> {
-            if (response.getType() != IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do");
+        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,a.getJid().asBareJid()+": no vCard set. nothing to do");
+                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": no vCard set. nothing to do");
                 return;
             }
             Element photo = vcard.findChild("PHOTO");
@@ -3365,12 +3269,12 @@ public class XmppConnectionService extends Service {
                 photo = vcard.addChild("PHOTO");
             }
             photo.clearChildren();
-            IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
-            publication.setTo(a.getJid().asBareJid());
+            final Iq publication = new Iq(Iq.Type.SET);
+            publication.setTo(account.getJid().asBareJid());
             publication.addChild(vcard);
-            sendIqPacket(account, publication, (a1, publicationResponse) -> {
-                if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
-                    Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar");
+            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());
@@ -3450,7 +3354,7 @@ public class XmppConnectionService extends Service {
                 }
             });
 
-            final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous());
+            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous());
             packet.setTo(joinJid);
             sendPresencePacket(account, packet);
         } else {
@@ -3610,39 +3514,37 @@ public class XmppConnectionService extends Service {
     }
 
     public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
-        IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
-        sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    final MucOptions mucOptions = conversation.getMucOptions();
-                    final Bookmark bookmark = conversation.getBookmark();
-                    final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
+        final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
+        final var account = conversation.getAccount();
+        sendIqPacket(account, request, response -> {
+            if (response.getType() == Iq.Type.RESULT) {
+                final MucOptions mucOptions = conversation.getMucOptions();
+                final Bookmark bookmark = conversation.getBookmark();
+                final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
 
-                    if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
-                        updateConversation(conversation);
-                    }
+                if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
+                    updateConversation(conversation);
+                }
 
-                    if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
-                        if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
-                            createBookmark(account, bookmark);
-                        }
+                if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
+                    if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
+                        createBookmark(account, bookmark);
                     }
+                }
 
 
-                    if (callback != null) {
-                        callback.onConferenceConfigurationFetched(conversation);
-                    }
+                if (callback != null) {
+                    callback.onConferenceConfigurationFetched(conversation);
+                }
 
 
-                    updateConversationUi();
-                } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch");
-                } else {
-                    if (callback != null) {
-                        callback.onFetchFailed(conversation, packet.getErrorCondition());
-                    }
+                updateConversationUi();
+            } else if (response.getType() == Iq.Type.TIMEOUT) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch");
+            } else {
+                if (callback != null) {
+                    callback.onFetchFailed(conversation, response.getErrorCondition());
                 }
             }
         });
@@ -3654,33 +3556,27 @@ public class XmppConnectionService extends Service {
 
     public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) {
         Log.d(Config.LOGTAG, "pushing node configuration");
-        sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
-                    Element configuration = pubsub == null ? null : pubsub.findChild("configure");
-                    Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA);
-                    if (x != null) {
-                        final Data data = Data.parse(x);
-                        data.submit(options);
-                        sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() {
-                            @Override
-                            public void onIqPacketReceived(Account account, IqPacket packet) {
-                                if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) {
-                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node);
-                                    callback.onPushSucceeded();
-                                } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
-                                    callback.onPushFailed();
-                                }
-                            }
-                        });
-                    } else if (callback != null) {
-                        callback.onPushFailed();
-                    }
-                } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
+        sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), responseToRequest -> {
+            if (responseToRequest.getType() == Iq.Type.RESULT) {
+                Element pubsub = responseToRequest.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
+                Element configuration = pubsub == null ? null : pubsub.findChild("configure");
+                Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA);
+                if (x != null) {
+                    final Data data = Data.parse(x);
+                    data.submit(options);
+                    sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), responseToPublish -> {
+                        if (responseToPublish.getType() == Iq.Type.RESULT && callback != null) {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node);
+                            callback.onPushSucceeded();
+                        } else if (responseToPublish.getType() == Iq.Type.ERROR && callback != null) {
+                            callback.onPushFailed();
+                        }
+                    });
+                } else if (callback != null) {
                     callback.onPushFailed();
                 }
+            } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
+                callback.onPushFailed();
             }
         });
     }
@@ -3700,50 +3596,45 @@ public class XmppConnectionService extends Service {
             options.putString("allow_private_messages", allow ? "1" : "0");
             options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
         }
-        final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+        final var account = conversation.getAccount();
+        final Iq request = new Iq(Iq.Type.GET);
         request.setTo(conversation.getJid().asBareJid());
         request.query("http://jabber.org/protocol/muc#owner");
-        sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    final Data data = Data.parse(packet.query().findChild("x", Namespace.DATA));
-                    data.submit(options);
-                    final IqPacket set = new IqPacket(IqPacket.TYPE.SET);
-                    set.setTo(conversation.getJid().asBareJid());
-                    set.query("http://jabber.org/protocol/muc#owner").addChild(data);
-                    sendIqPacket(account, set, new OnIqPacketReceived() {
-                        @Override
-                        public void onIqPacketReceived(Account account, IqPacket packet) {
-                            if (callback != null) {
-                                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                                    callback.onPushSucceeded();
-                                } else {
-                                    Log.d(Config.LOGTAG,"failed: "+packet.toString());
-                                    callback.onPushFailed();
-                                }
-                            }
-                        }
-                    });
-                } else {
+        sendIqPacket(account, request, response -> {
+            if (response.getType() == Iq.Type.RESULT) {
+                final Data data = Data.parse(response.query().findChild("x", Namespace.DATA));
+                data.submit(options);
+                final Iq set = new Iq(Iq.Type.SET);
+                set.setTo(conversation.getJid().asBareJid());
+                set.query("http://jabber.org/protocol/muc#owner").addChild(data);
+                sendIqPacket(account, set, packet -> {
                     if (callback != null) {
-                        callback.onPushFailed();
+                        if (packet.getType() == Iq.Type.RESULT) {
+                            callback.onPushSucceeded();
+                        } else {
+                            Log.d(Config.LOGTAG,"failed: "+packet.toString());
+                            callback.onPushFailed();
+                        }
                     }
+                });
+            } else {
+                if (callback != null) {
+                    callback.onPushFailed();
                 }
             }
         });
     }
 
     public void pushSubjectToConference(final Conversation conference, final String subject) {
-        MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
+        final var packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
         this.sendMessagePacket(conference.getAccount(), packet);
     }
 
     public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
         final Jid jid = user.asBareJid();
-        final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
-        sendIqPacket(conference.getAccount(), request, (account, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        final Iq request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
+        sendIqPacket(conference.getAccount(), request, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 conference.getMucOptions().changeAffiliation(jid, affiliation);
                 getAvatarService().clear(conference);
                 if (callback != null) {
@@ -3760,29 +3651,27 @@ public class XmppConnectionService extends Service {
     }
 
     public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
-        IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
-        sendIqPacket(conference.getAccount(), request, (account, packet) -> {
-            if (packet.getType() != IqPacket.TYPE.RESULT) {
+        final var account =conference.getAccount();
+        final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
+        sendIqPacket(account, request, (packet) -> {
+            if (packet.getType() != Iq.Type.RESULT) {
                 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
             }
         });
     }
 
     public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
-        IqPacket request = new IqPacket(IqPacket.TYPE.SET);
+        final Iq request = new Iq(Iq.Type.SET);
         request.setTo(conversation.getJid().asBareJid());
         request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
-        sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    if (callback != null) {
-                        callback.onRoomDestroySucceeded();
-                    }
-                } else if (packet.getType() == IqPacket.TYPE.ERROR) {
-                    if (callback != null) {
-                        callback.onRoomDestroyFailed();
-                    }
+        sendIqPacket(conversation.getAccount(), request, response -> {
+            if (response.getType() == Iq.Type.RESULT) {
+                if (callback != null) {
+                    callback.onRoomDestroySucceeded();
+                }
+            } else if (response.getType() == Iq.Type.ERROR) {
+                if (callback != null) {
+                    callback.onRoomDestroyFailed();
                 }
             }
         });
@@ -3832,7 +3721,7 @@ public class XmppConnectionService extends Service {
         updateConversationUi();
     }
 
-    protected void syncDirtyContacts(Account account) {
+    public void syncDirtyContacts(Account account) {
         for (Contact contact : account.getRoster().getContacts()) {
             if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
                 pushContactToServer(contact);
@@ -3868,7 +3757,7 @@ public class XmppConnectionService extends Service {
             final boolean sendUpdates = contact
                     .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
                     && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
-            final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+            final Iq iq = new Iq(Iq.Type.SET);
             iq.query(Namespace.ROSTER).addChild(contact.asElement());
             account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
             if (sendUpdates) {
@@ -3920,10 +3809,11 @@ public class XmppConnectionService extends Service {
     }
 
     private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
-        final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
-        sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> {
-            boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found");
-            if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) {
+        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");
@@ -3935,11 +3825,11 @@ public class XmppConnectionService extends Service {
                 photo.clearChildren();
                 photo.addChild("TYPE").setContent(avatar.type);
                 photo.addChild("BINVAL").setContent(avatar.image);
-                IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
+                final Iq publication = new Iq(Iq.Type.SET);
                 publication.setTo(conversation.getJid().asBareJid());
                 publication.addChild(vcard);
-                sendIqPacket(account, publication, (a1, publicationResponse) -> {
-                    if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
+                sendIqPacket(account, publication, (publicationResponse) -> {
+                    if (publicationResponse.getType() == Iq.Type.RESULT) {
                         callback.onAvatarPublicationSucceeded();
                     } else {
                         Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition());
@@ -3965,71 +3855,64 @@ public class XmppConnectionService extends Service {
 
     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);
-        IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options);
-        this.sendIqPacket(account, packet, new OnIqPacketReceived() {
-
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket result) {
-                if (result.getType() == IqPacket.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);
-                        }
+        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);
+                    @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 IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
-        sendIqPacket(account, packet, new OnIqPacketReceived() {
-            @Override
-            public void onIqPacketReceived(Account account, IqPacket result) {
-                if (result.getType() == IqPacket.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();
+        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);
                     }
-                } 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);
+                    @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);
                 }
             }
         });
@@ -4040,10 +3923,10 @@ public class XmppConnectionService extends Service {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
             return;
         }
-        IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
-        this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
+        this.sendIqPacket(account, packet, new Consumer<Iq>() {
 
-            private Avatar parseAvatar(IqPacket packet) {
+            private Avatar parseAvatar(Iq packet) {
                 Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
                 if (pubsub != null) {
                     Element items = pubsub.findChild("items");
@@ -4054,16 +3937,16 @@ public class XmppConnectionService extends Service {
                 return null;
             }
 
-            private boolean errorIsItemNotFound(IqPacket packet) {
+            private boolean errorIsItemNotFound(Iq packet) {
                 Element error = packet.findChild("error");
-                return packet.getType() == IqPacket.TYPE.ERROR
+                return packet.getType() == Iq.Type.ERROR
                         && error != null
                         && error.hasChild("item-not-found");
             }
 
             @Override
-            public void onIqPacketReceived(Account account, IqPacket packet) {
-                if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
+            public void accept(final Iq packet) {
+                if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
                     Avatar serverAvatar = parseAvatar(packet);
                     if (serverAvatar == null && account.getAvatar() != null) {
                         Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
@@ -4079,6 +3962,17 @@ public class XmppConnectionService extends Service {
         });
     }
 
+    public void cancelAvatarFetches(final Account account) {
+        synchronized (mInProgressAvatarFetches) {
+            for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
+                final String KEY = iterator.next();
+                if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
+                    iterator.remove();
+                }
+            }
+        }
+    }
+
     public void fetchAvatar(Account account, Avatar avatar) {
         fetchAvatar(account, avatar, null);
     }
@@ -4105,26 +3999,26 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
-        IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
-        sendIqPacket(account, packet, (a, result) -> {
+    private void fetchAvatarPep(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
+        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
+        sendIqPacket(account, packet, (result) -> {
             synchronized (mInProgressAvatarFetches) {
-                mInProgressAvatarFetches.remove(generateFetchKey(a, avatar));
+                mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
             }
-            final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed ";
-            if (result.getType() == IqPacket.TYPE.RESULT) {
-                avatar.image = mIqParser.avatarData(result);
+            final String ERROR = account.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed ";
+            if (result.getType() == Iq.Type.RESULT) {
+                avatar.image = IqParser.avatarData(result);
                 if (avatar.image != null) {
                     if (getFileBackend().save(avatar)) {
-                        if (a.getJid().asBareJid().equals(avatar.owner)) {
-                            if (a.setAvatar(avatar.getFilename())) {
-                                databaseBackend.updateAccount(a);
+                        if (account.getJid().asBareJid().equals(avatar.owner)) {
+                            if (account.setAvatar(avatar.getFilename())) {
+                                databaseBackend.updateAccount(account);
                             }
-                            getAvatarService().clear(a);
+                            getAvatarService().clear(account);
                             updateConversationUi();
                             updateAccountUi();
                         } else {
-                            final Contact contact = a.getRoster().getContact(avatar.owner);
+                            final Contact contact = account.getRoster().getContact(avatar.owner);
                             contact.setAvatar(avatar);
                             syncRoster(account);
                             getAvatarService().clear(contact);

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -494,14 +494,9 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     protected boolean isOptimizingBattery() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
-            return pm != null
-                    && !pm.isIgnoringBatteryOptimizations(getPackageName());
-        } else {
-            return false;
-        }
-    }
+        final PowerManager pm = getSystemService(PowerManager.class);
+        return !pm.isIgnoringBatteryOptimizations(getPackageName());
+}
 
     protected boolean isAffectedByDataSaver() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

src/main/java/eu/siacs/conversations/xml/Element.java 🔗

@@ -3,7 +3,9 @@ package eu.siacs.conversations.xml;
 import androidx.annotation.NonNull;
 
 import com.google.common.base.Optional;
+import com.google.common.base.Strings;
 import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
 
 import java.util.ArrayList;
 import java.util.Hashtable;
@@ -12,7 +14,7 @@ import java.util.List;
 import eu.siacs.conversations.utils.XmlHelper;
 import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.stanza.Message;
 
 public class Element {
     private final String name;
@@ -136,6 +138,10 @@ public class Element {
         return this;
     }
 
+    public void setAttribute(final String name, final boolean value) {
+        this.setAttribute(name, value ? "1" : "0");
+    }
+
     public void removeAttribute(final String name) {
         this.attributes.remove(name);
     }
@@ -153,6 +159,11 @@ public class Element {
         }
     }
 
+    public long getLongAttribute(final String name) {
+        final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name)));
+        return value == null ? 0 : value;
+    }
+
     public Optional<Integer> getOptionalIntAttribute(final String name) {
         final String value = getAttribute(name);
         if (value == null) {
@@ -167,7 +178,7 @@ public class Element {
             try {
                 return Jid.ofEscaped(jid);
             } catch (final IllegalArgumentException e) {
-                return InvalidJid.of(jid, this instanceof MessagePacket);
+                return InvalidJid.of(jid, this instanceof Message);
             }
         }
         return null;
@@ -180,7 +191,7 @@ public class Element {
     @NonNull
     public String toString() {
         final StringBuilder elementOutput = new StringBuilder();
-        if ((content == null) && (children.size() == 0)) {
+        if (content == null && children.isEmpty()) {
             final Tag emptyTag = Tag.empty(name);
             emptyTag.setAttributes(this.attributes);
             elementOutput.append(emptyTag);

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -1,8 +1,29 @@
 package eu.siacs.conversations.xml;
 
 public final class Namespace {
+    public static final String ADDRESSING = "http://jabber.org/protocol/address";
+    public static final String AXOLOTL = "eu.siacs.conversations.axolotl";
+    public static final String PGP_SIGNED = "jabber:x:signed";
+    public static final String PGP_ENCRYPTED = "jabber:x:encrypted";
+    public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles";
+    public static final String AXOLOTL_DEVICE_LIST = AXOLOTL + ".devicelist";
+    public static final String HINTS = "urn:xmpp:hints";
+    public static final String MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2";
+    public static final String VERSION = "jabber:iq:version";
+    public static final String LAST_MESSAGE_CORRECTION = "urn:xmpp:message-correct:0";
+    public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
+    public static final String CHAT_MARKERS = "urn:xmpp:chat-markers:0";
+    public static final String CHAT_STATES = "http://jabber.org/protocol/chatstates";
+    public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts";
+    public static final String REACTIONS = "urn:xmpp:reactions:0";
+    public static final String VCARD_TEMP = "vcard-temp";
+    public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update";
+    public static final String DELAY = "urn:xmpp:delay";
+    public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0";
     public static final String STREAMS = "http://etherx.jabber.org/streams";
+    public static final String STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas";
     public static final String JABBER_CLIENT = "jabber:client";
+    public static final String FORWARD = "urn:xmpp:forward:0";
     public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
     public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
     public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2";
@@ -23,12 +44,15 @@ public final class Namespace {
     public static final String FAST = "urn:xmpp:fast:0";
     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";
+    public static final String MUC = "http://jabber.org/protocol/muc";
     public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
     public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max";
     public static final String PUBSUB_ERROR = PUBSUB + "#errors";
     public static final String PUBSUB_OWNER = PUBSUB + "#owner";
     public static final String NICK = "http://jabber.org/protocol/nick";
-    public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
+    public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL =
+            "http://jabber.org/protocol/offline";
     public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
     public static final String BIND2 = "urn:xmpp:bind:0";
     public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3";
@@ -38,7 +62,7 @@ public final class Namespace {
     public static final String BOOKMARKS = "storage:bookmarks";
     public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";
     public static final String AVATAR_DATA = "urn:xmpp:avatar:data";
-    public static final String AVATAR_METADATA =  "urn:xmpp:avatar:metadata";
+    public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata";
     public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0";
     public static final String JINGLE = "urn:xmpp:jingle:1";
     public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1";
@@ -48,7 +72,8 @@ public final class Namespace {
     public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1";
     public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1";
     public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1";
-    public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1";
+    public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL =
+            "urn:xmpp:jingle:transports:webrtc-datachannel:1";
     public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1";
     public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1";
 
@@ -57,9 +82,12 @@ public final class Namespace {
     public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
     public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
     public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video";
-    public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
-    public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
-    public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0";
+    public static final String JINGLE_RTP_HEADER_EXTENSIONS =
+            "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
+    public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION =
+            "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
+    public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES =
+            "urn:xmpp:jingle:apps:rtp:ssma:0";
     public static final String IBB = "http://jabber.org/protocol/ibb";
     public static final String PING = "urn:xmpp:ping";
     public static final String PUSH = "urn:xmpp:push:0";
@@ -70,8 +98,10 @@ public final class Namespace {
     public static final String INVITE = "urn:xmpp:invite";
     public static final String PARS = "urn:xmpp:pars:0";
     public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
-    public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
-    public static final String JINGLE_TRANSPORT_ICE_OPTION = "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option";
+    public static final String OMEMO_DTLS_SRTP_VERIFICATION =
+            "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
+    public static final String JINGLE_TRANSPORT_ICE_OPTION =
+            "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option";
     public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
     public static final String REPORTING = "urn:xmpp:reporting:1";
     public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
@@ -79,4 +109,7 @@ public final class Namespace {
     public static final String HASHES = "urn:xmpp:hashes:2";
     public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
     public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
+
+    public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
+    public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
 }

src/main/java/eu/siacs/conversations/xml/TagWriter.java 🔗

@@ -10,13 +10,14 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import im.conversations.android.xmpp.model.StreamElement;
 
 public class TagWriter {
 
     private OutputStreamWriter outputStream;
     private boolean finished = false;
-    private final LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
+
+    private final LinkedBlockingQueue<StreamElement> writeQueue = new LinkedBlockingQueue<>();
     private CountDownLatch stanzaWriterCountDownLatch = null;
 
     private final Thread asyncStanzaWriter = new Thread() {
@@ -25,13 +26,13 @@ public class TagWriter {
         public void run() {
             stanzaWriterCountDownLatch = new CountDownLatch(1);
             while (!isInterrupted()) {
-                if (finished && writeQueue.size() == 0) {
+                if (finished && writeQueue.isEmpty()) {
                     break;
                 }
                 try {
-                    AbstractStanza output = writeQueue.take();
+                    final var output = writeQueue.take();
                     outputStream.write(output.toString());
-                    if (writeQueue.size() == 0) {
+                    if (writeQueue.isEmpty()) {
                         outputStream.flush();
                     }
                 } catch (Exception e) {
@@ -74,7 +75,7 @@ public class TagWriter {
         }
     }
 
-    public synchronized void writeElement(Element element) throws IOException {
+    public synchronized void writeElement(final StreamElement element) throws IOException {
         if (outputStream == null) {
             throw new IOException("output stream was null");
         }
@@ -82,7 +83,7 @@ public class TagWriter {
         outputStream.flush();
     }
 
-    public void writeStanzaAsync(AbstractStanza stanza) {
+    public void writeStanzaAsync(StreamElement stanza) {
         if (finished) {
             Log.d(Config.LOGTAG, "attempting to write stanza to finished TagWriter");
         } else {

src/main/java/eu/siacs/conversations/xml/XmlReader.java 🔗

@@ -3,6 +3,12 @@ package eu.siacs.conversations.xml;
 import android.util.Log;
 import android.util.Xml;
 
+import eu.siacs.conversations.Config;
+
+import im.conversations.android.xmpp.ExtensionFactory;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.StreamElement;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -11,8 +17,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 
-import eu.siacs.conversations.Config;
-
 public class XmlReader implements Closeable {
 	private final XmlPullParser parser;
 	private InputStream is;
@@ -87,8 +91,21 @@ public class XmlReader implements Closeable {
 		return null;
 	}
 
-	public Element readElement(Tag currentTag) throws IOException {
-		Element element = new Element(currentTag.getName());
+	public <T extends StreamElement> T readElement(final Tag current, final Class<T> clazz)
+			throws IOException {
+		final Element element = readElement(current);
+		if (clazz.isInstance(element)) {
+			return clazz.cast(element);
+		}
+		throw new IOException(
+				String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName()));
+	}
+
+	public Element readElement(final Tag currentTag) throws IOException {
+		final var attributes = currentTag.getAttributes();
+		final var namespace = attributes.get("xmlns");
+		final var name = currentTag.getName();
+		final Element element = ExtensionFactory.create(name, namespace);
 		element.setAttributes(currentTag.getAttributes());
 		Tag nextTag = this.readTag();
 		if (nextTag == null) {

src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java 🔗

@@ -31,7 +31,7 @@ package eu.siacs.conversations.xmpp;
 
 import androidx.annotation.NonNull;
 
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
+import im.conversations.android.xmpp.model.stanza.Stanza;
 
 public class InvalidJid implements Jid {
 
@@ -137,10 +137,10 @@ public class InvalidJid implements Jid {
 	}
 
 	public static boolean isValid(Jid jid) {
-		return !(jid != null && jid instanceof InvalidJid);
+		return !(jid instanceof InvalidJid);
 	}
 
-	public static boolean hasValidFrom(AbstractStanza stanza) {
+	public static boolean hasValidFrom(Stanza stanza) {
 		final String from = stanza.getAttribute("from");
 		if (from == null) {
 			return false;

src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java 🔗

@@ -1,8 +0,0 @@
-package eu.siacs.conversations.xmpp;
-
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-
-public interface OnIqPacketReceived extends PacketReceived {
-	void onIqPacketReceived(Account account, IqPacket packet);
-}

src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java 🔗

@@ -1,8 +1,7 @@
 package eu.siacs.conversations.xmpp;
 
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.stanza.Message;
 
-public interface OnMessagePacketReceived extends PacketReceived {
-	void onMessagePacketReceived(Account account, MessagePacket packet);
+public interface OnMessagePacketReceived {
+	void onMessagePacketReceived(Message packet);
 }

src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java 🔗

@@ -1,8 +0,0 @@
-package eu.siacs.conversations.xmpp;
-
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
-
-public interface OnPresencePacketReceived extends PacketReceived {
-	void onPresencePacketReceived(Account account, PresencePacket packet);
-}

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -36,6 +36,9 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.generator.IqGenerator;
 import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.parser.MessageParser;
+import eu.siacs.conversations.parser.PresenceParser;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.MemorizingTrustManager;
 import eu.siacs.conversations.services.MessageArchiveService;
@@ -58,18 +61,37 @@ import eu.siacs.conversations.xml.XmlReader;
 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.jingle.stanzas.JinglePacket;
-import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
-import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
-import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
-import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
-import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
-import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
-import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
-import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
+import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
+import im.conversations.android.xmpp.model.StreamElement;
+import im.conversations.android.xmpp.model.bind2.Bind;
+import im.conversations.android.xmpp.model.bind2.Bound;
+import im.conversations.android.xmpp.model.csi.Active;
+import im.conversations.android.xmpp.model.csi.Inactive;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.fast.Fast;
+import im.conversations.android.xmpp.model.fast.RequestToken;
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.sasl.Auth;
+import im.conversations.android.xmpp.model.sasl.Mechanisms;
+import im.conversations.android.xmpp.model.sasl.Response;
+import im.conversations.android.xmpp.model.sasl.Success;
+import im.conversations.android.xmpp.model.sasl2.Authenticate;
+import im.conversations.android.xmpp.model.sasl2.Authentication;
+import im.conversations.android.xmpp.model.sm.Ack;
+import im.conversations.android.xmpp.model.sm.Enable;
+import im.conversations.android.xmpp.model.sm.Enabled;
+import im.conversations.android.xmpp.model.sm.Failed;
+import im.conversations.android.xmpp.model.sm.Request;
+import im.conversations.android.xmpp.model.sm.Resume;
+import im.conversations.android.xmpp.model.sm.Resumed;
+import im.conversations.android.xmpp.model.sm.StreamManagement;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import im.conversations.android.xmpp.model.stanza.Stanza;
+import im.conversations.android.xmpp.model.tls.Proceed;
+import im.conversations.android.xmpp.model.tls.StartTls;
+import im.conversations.android.xmpp.processor.BindProcessor;
 
 import okhttp3.HttpUrl;
 
@@ -104,6 +126,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 
 import javax.net.ssl.KeyManager;
@@ -116,46 +139,12 @@ import javax.net.ssl.X509TrustManager;
 
 public class XmppConnection implements Runnable {
 
-    private static final int PACKET_IQ = 0;
-    private static final int PACKET_MESSAGE = 1;
-    private static final int PACKET_PRESENCE = 2;
-    public final OnIqPacketReceived registrationResponseListener =
-            (account, packet) -> {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    account.setOption(Account.OPTION_REGISTER, false);
-                    Log.d(
-                            Config.LOGTAG,
-                            account.getJid().asBareJid()
-                                    + ": successfully registered new account on server");
-                    throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
-                } else {
-                    final List<String> PASSWORD_TOO_WEAK_MSGS =
-                            Arrays.asList(
-                                    "The password is too weak", "Please use a longer password.");
-                    Element error = packet.findChild("error");
-                    Account.State state = Account.State.REGISTRATION_FAILED;
-                    if (error != null) {
-                        if (error.hasChild("conflict")) {
-                            state = Account.State.REGISTRATION_CONFLICT;
-                        } else if (error.hasChild("resource-constraint")
-                                && "wait".equals(error.getAttribute("type"))) {
-                            state = Account.State.REGISTRATION_PLEASE_WAIT;
-                        } else if (error.hasChild("not-acceptable")
-                                && PASSWORD_TOO_WEAK_MSGS.contains(
-                                        error.findChildContent("text"))) {
-                            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
-                        }
-                    }
-                    throw new StateChangingError(state);
-                }
-            };
     protected final Account account;
     private final Features features = new Features(this);
     private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
     private final HashMap<String, Jid> commands = new HashMap<>();
-    private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
-    private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks =
-            new Hashtable<>();
+    private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
+    private final Hashtable<String, Pair<Iq, Consumer<Iq>>> packetCallbacks = new Hashtable<>();
     private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
             new HashSet<>();
     private final AppSettings appSettings;
@@ -168,8 +157,8 @@ public class XmppConnection implements Runnable {
     private boolean quickStartInProgress = false;
     private boolean isBound = false;
     private boolean offlineMessagesRetrieved = false;
-    private Element streamFeatures;
-    private Element boundStreamFeatures;
+    private im.conversations.android.xmpp.model.streams.Features streamFeatures;
+    private im.conversations.android.xmpp.model.streams.Features boundStreamFeatures;
     private StreamId streamId = null;
     private int stanzasReceived = 0;
     private int stanzasSent = 0;
@@ -186,12 +175,13 @@ public class XmppConnection implements Runnable {
     private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
     private boolean mInteractive = false;
     private int attempt = 0;
-    private OnPresencePacketReceived presenceListener = null;
     private OnJinglePacketReceived jingleListener = null;
-    private OnIqPacketReceived unregisteredIqListener = null;
-    private OnMessagePacketReceived messageListener = null;
+
+    private final Consumer<Presence> presenceListener;
+    private final Consumer<Iq> unregisteredIqListener;
+    private final Consumer<im.conversations.android.xmpp.model.stanza.Message> messageListener;
     private OnStatusChanged statusListener = null;
-    private OnBindListener bindListener = null;
+    private final Runnable bindListener;
     private OnMessageAcknowledged acknowledgedListener = null;
     private LoginInfo loginInfo;
     private HashedToken.Mechanism hashTokenRequest;
@@ -206,6 +196,10 @@ public class XmppConnection implements Runnable {
         this.account = account;
         this.mXmppConnectionService = service;
         this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
+        this.presenceListener = new PresenceParser(service, account);
+        this.unregisteredIqListener = new IqParser(service, account);
+        this.messageListener = new MessageParser(service, account);
+        this.bindListener = new BindProcessor(service, account);
     }
 
     private static void fixResource(final Context context, final Account account) {
@@ -606,7 +600,7 @@ public class XmppConnection implements Runnable {
             } else if (nextTag.isStart("features", Namespace.STREAMS)) {
                 processStreamFeatures(nextTag);
             } else if (nextTag.isStart("proceed", Namespace.TLS)) {
-                switchOverToTls();
+                switchOverToTls(nextTag);
             } else if (nextTag.isStart("failure", Namespace.TLS)) {
                 throw new StateChangingException(Account.State.TLS_ERROR);
             } else if (account.isOptionSet(Account.OPTION_REGISTER)
@@ -632,10 +626,10 @@ public class XmppConnection implements Runnable {
                 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
             } else if (this.streamId != null
                     && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
-                final Element resumed = tagReader.readElement(nextTag);
+                final Resumed resumed = tagReader.readElement(nextTag, Resumed.class);
                 processResumed(resumed);
             } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
-                final Element failed = tagReader.readElement(nextTag);
+                final Failed failed = tagReader.readElement(nextTag, Failed.class);
                 processFailed(failed, true);
             } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
                 processIq(nextTag);
@@ -651,7 +645,7 @@ public class XmppConnection implements Runnable {
             } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
                 processPresence(nextTag);
             } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
-                final Element enabled = tagReader.readElement(nextTag);
+                final var enabled = tagReader.readElement(nextTag, Enabled.class);
                 processEnabled(enabled);
             } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
                 tagReader.readElement(nextTag);
@@ -662,7 +656,7 @@ public class XmppConnection implements Runnable {
                                     + ": acknowledging stanza #"
                                     + this.stanzasReceived);
                 }
-                final AckPacket ack = new AckPacket(this.stanzasReceived);
+                final Ack ack = new Ack(this.stanzasReceived);
                 tagWriter.writeStanzaAsync(ack);
             } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
                 boolean accountUiNeedsRefresh = false;
@@ -689,11 +683,11 @@ public class XmppConnection implements Runnable {
                 if (accountUiNeedsRefresh) {
                     mXmppConnectionService.updateAccountUi();
                 }
-                final Element ack = tagReader.readElement(nextTag);
+                final var ack = tagReader.readElement(nextTag, Ack.class);
                 lastPacketReceived = SystemClock.elapsedRealtime();
                 final boolean acknowledgedMessages;
                 synchronized (this.mStanzaQueue) {
-                    final Optional<Integer> serverSequence = ack.getOptionalIntAttribute("h");
+                    final Optional<Integer> serverSequence = ack.getHandled();
                     if (serverSequence.isPresent()) {
                         acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get());
                     } else {
@@ -729,11 +723,11 @@ public class XmppConnection implements Runnable {
         } catch (final IllegalArgumentException e) {
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
-        final Element response;
+        final StreamElement response;
         if (version == SaslMechanism.Version.SASL) {
-            response = new Element("response", Namespace.SASL);
+            response = new Response();
         } else if (version == SaslMechanism.Version.SASL_2) {
-            response = new Element("response", Namespace.SASL_2);
+            response = new im.conversations.android.xmpp.model.sasl2.Response();
         } else {
             throw new AssertionError("Missing implementation for " + version);
         }
@@ -753,26 +747,23 @@ public class XmppConnection implements Runnable {
         tagWriter.writeElement(response);
     }
 
-    private boolean processSuccess(final Element success)
+    private boolean processSuccess(final Element element)
             throws IOException, XmlPullParserException {
-        final SaslMechanism.Version version;
-        try {
-            version = SaslMechanism.Version.of(success);
-        } catch (final IllegalArgumentException e) {
-            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
-        }
         final LoginInfo currentLoginInfo = this.loginInfo;
         final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
         if (currentLoginInfo == null || currentSaslMechanism == null) {
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
+        final SaslMechanism.Version version;
         final String challenge;
-        if (version == SaslMechanism.Version.SASL) {
+        if (element instanceof Success success) {
             challenge = success.getContent();
-        } else if (version == SaslMechanism.Version.SASL_2) {
+            version = SaslMechanism.Version.SASL;
+        } else if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
             challenge = success.findChildContent("additional-data");
+            version = SaslMechanism.Version.SASL_2;
         } else {
-            throw new AssertionError("Missing implementation for " + version);
+            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
         try {
             currentLoginInfo.success(challenge, sslSocketOrNull(socket));
@@ -786,47 +777,24 @@ public class XmppConnection implements Runnable {
         if (SaslMechanism.pin(currentSaslMechanism)) {
             account.setPinnedMechanism(currentSaslMechanism);
         }
-        if (version == SaslMechanism.Version.SASL_2) {
-            final String authorizationIdentifier =
-                    success.findChildContent("authorization-identifier");
-            final Jid authorizationJid;
-            try {
-                authorizationJid =
-                        Strings.isNullOrEmpty(authorizationIdentifier)
-                                ? null
-                                : Jid.ofEscaped(authorizationIdentifier);
-            } catch (final IllegalArgumentException e) {
-                Log.d(
-                        Config.LOGTAG,
-                        account.getJid().asBareJid()
-                                + ": SASL 2.0 authorization identifier was not a valid jid");
-                throw new StateChangingException(Account.State.BIND_FAILURE);
-            }
-            if (authorizationJid == null) {
-                throw new StateChangingException(Account.State.BIND_FAILURE);
-            }
+        if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) {
+            final var authorizationJid = success.getAuthorizationIdentifier();
+            checkAssignedDomainOrThrow(authorizationJid);
             Log.d(
                     Config.LOGTAG,
                     account.getJid().asBareJid()
                             + ": SASL 2.0 authorization identifier was "
                             + authorizationJid);
-            if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) {
-                Log.d(
-                        Config.LOGTAG,
-                        account.getJid().asBareJid()
-                                + ": server tried to re-assign domain to "
-                                + authorizationJid.getDomain());
-                throw new StateChangingError(Account.State.BIND_FAILURE);
-            }
+            // TODO this should only happen when we used Bind 2
             if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) {
                 Log.d(
                         Config.LOGTAG,
                         account.getJid().asBareJid()
                                 + ": jid changed during SASL 2.0. updating database");
             }
-            final Element bound = success.findChild("bound", Namespace.BIND2);
-            final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT);
-            final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT);
+            final Bound bound = success.getExtension(Bound.class);
+            final Resumed resumed = success.getExtension(Resumed.class);
+            final Failed failed = success.getExtension(Failed.class);
             final Element tokenWrapper = success.findChild("token", Namespace.FAST);
             final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token");
             if (bound != null && resumed != null) {
@@ -853,8 +821,7 @@ public class XmppConnection implements Runnable {
                 this.isBound = true;
                 processNopStreamFeatures();
                 this.boundStreamFeatures = this.streamFeatures;
-                final Element streamManagementEnabled =
-                        bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
+                final Enabled streamManagementEnabled = bound.getExtension(Enabled.class);
                 final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
                 final boolean waitForDisco;
                 if (streamManagementEnabled != null) {
@@ -939,7 +906,7 @@ public class XmppConnection implements Runnable {
 
     private void resetOutboundStanzaQueue() {
         synchronized (this.mStanzaQueue) {
-            final ImmutableList.Builder<AbstractAcknowledgeableStanza> intermediateStanzasBuilder =
+            final ImmutableList.Builder<Stanza> intermediateStanzasBuilder =
                     new ImmutableList.Builder<>();
             if (Config.EXTENDED_SM_LOGGING) {
                 Log.d(
@@ -949,7 +916,7 @@ public class XmppConnection implements Runnable {
                                 + this.stanzasSentBeforeAuthentication);
             }
             for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) {
-                final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i);
+                final Stanza stanza = this.mStanzaQueue.get(i);
                 if (stanza != null) {
                     intermediateStanzasBuilder.add(stanza);
                 }
@@ -973,7 +940,9 @@ public class XmppConnection implements Runnable {
     private void processNopStreamFeatures() throws IOException {
         final Tag tag = tagReader.readTag();
         if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
-            this.streamFeatures = tagReader.readElement(tag);
+            this.streamFeatures =
+                    tagReader.readElement(
+                            tag, im.conversations.android.xmpp.model.streams.Features.class);
             Log.d(
                     Config.LOGTAG,
                     account.getJid().asBareJid()
@@ -1043,22 +1012,8 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private void processEnabled(final Element enabled) {
-        final String id;
-        if (enabled.getAttributeAsBoolean("resume")) {
-            id = enabled.getAttribute("id");
-        } else {
-            id = null;
-        }
-        final String locationAttribute = enabled.getAttribute("location");
-        final Resolver.Result currentResolverResult = this.currentResolverResult;
-        final Resolver.Result location;
-        if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
-            location = null;
-        } else {
-            location = currentResolverResult.seeOtherHost(locationAttribute);
-        }
-        final StreamId streamId = id == null ? null : new StreamId(id, location);
+    private void processEnabled(final Enabled enabled) {
+        final StreamId streamId = getStreamId(enabled);
         if (streamId == null) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
         } else {
@@ -1071,16 +1026,30 @@ public class XmppConnection implements Runnable {
         this.streamId = streamId;
         this.stanzasReceived = 0;
         this.inSmacksSession = true;
-        final RequestPacket r = new RequestPacket();
+        final var r = new Request();
         tagWriter.writeStanzaAsync(r);
     }
 
-    private void processResumed(final Element resumed) throws StateChangingException {
+    @Nullable
+    private StreamId getStreamId(final Enabled enabled) {
+        final Optional<String> id = enabled.getResumeId();
+        final String locationAttribute = enabled.getLocation();
+        final Resolver.Result currentResolverResult = this.currentResolverResult;
+        final Resolver.Result location;
+        if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
+            location = null;
+        } else {
+            location = currentResolverResult.seeOtherHost(locationAttribute);
+        }
+        return id.isPresent() ? new StreamId(id.get(), location) : null;
+    }
+
+    private void processResumed(final Resumed resumed) throws StateChangingException {
         this.inSmacksSession = true;
         this.isBound = true;
-        this.tagWriter.writeStanzaAsync(new RequestPacket());
+        this.tagWriter.writeStanzaAsync(new Request());
         lastPacketReceived = SystemClock.elapsedRealtime();
-        final Optional<Integer> h = resumed.getOptionalIntAttribute("h");
+        final Optional<Integer> h = resumed.getHandled();
         final int serverCount;
         if (h.isPresent()) {
             serverCount = h.get();
@@ -1088,7 +1057,7 @@ public class XmppConnection implements Runnable {
             resetStreamId();
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
-        final ArrayList<AbstractAcknowledgeableStanza> failedStanzas = new ArrayList<>();
+        final ArrayList<Stanza> failedStanzas = new ArrayList<>();
         final boolean acknowledgedMessages;
         synchronized (this.mStanzaQueue) {
             if (serverCount < stanzasSent) {
@@ -1111,8 +1080,8 @@ public class XmppConnection implements Runnable {
         Log.d(
                 Config.LOGTAG,
                 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
-        for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
-            if (packet instanceof MessagePacket message) {
+        for (final Stanza packet : failedStanzas) {
+            if (packet instanceof im.conversations.android.xmpp.model.stanza.Message message) {
                 mXmppConnectionService.markMessage(
                         account,
                         message.getTo().asBareJid(),
@@ -1131,8 +1100,8 @@ public class XmppConnection implements Runnable {
         changeStatus(Account.State.ONLINE);
     }
 
-    private void processFailed(final Element failed, final boolean sendBindRequest) {
-        final Optional<Integer> serverCount = failed.getOptionalIntAttribute("h");
+    private void processFailed(final Failed failed, final boolean sendBindRequest) {
+        final Optional<Integer> serverCount = failed.getHandled();
         if (serverCount.isPresent()) {
             Log.d(
                     Config.LOGTAG,
@@ -1179,8 +1148,9 @@ public class XmppConnection implements Runnable {
                                     + ": server acknowledged stanza #"
                                     + mStanzaQueue.keyAt(i));
                 }
-                final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
-                if (stanza instanceof MessagePacket packet && acknowledgedListener != null) {
+                final Stanza stanza = mStanzaQueue.valueAt(i);
+                if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet
+                        && acknowledgedListener != null) {
                     final String id = packet.getId();
                     final Jid to = packet.getTo();
                     if (id != null && to != null) {
@@ -1195,29 +1165,9 @@ public class XmppConnection implements Runnable {
         return acknowledgedMessages;
     }
 
-    private @NonNull Element processPacket(final Tag currentTag, final int packetType)
+    private <S extends Stanza> @NonNull S processPacket(final Tag currentTag, final Class<S> clazz)
             throws IOException {
-        final Element element =
-                switch (packetType) {
-                    case PACKET_IQ -> new IqPacket();
-                    case PACKET_MESSAGE -> new MessagePacket();
-                    case PACKET_PRESENCE -> new PresencePacket();
-                    default -> throw new AssertionError("Should never encounter invalid type");
-                };
-        element.setAttributes(currentTag.getAttributes());
-        Tag nextTag = tagReader.readTag();
-        if (nextTag == null) {
-            throw new IOException("interrupted mid tag");
-        }
-        while (!nextTag.isEnd(element.getName())) {
-            if (!nextTag.isNo()) {
-                element.addChild(tagReader.readElement(nextTag));
-            }
-            nextTag = tagReader.readTag();
-            if (nextTag == null) {
-                throw new IOException("interrupted mid tag");
-            }
-        }
+        final S stanza = tagReader.readElement(currentTag, clazz);
         if (stanzasReceived == Integer.MAX_VALUE) {
             resetStreamId();
             throw new IOException("time to restart the session. cant handle >2 billion pcks");
@@ -1229,25 +1179,19 @@ public class XmppConnection implements Runnable {
                     Config.LOGTAG,
                     account.getJid().asBareJid()
                             + ": not counting stanza("
-                            + element.getClass().getSimpleName()
+                            + stanza.getClass().getSimpleName()
                             + "). Not in smacks session.");
         }
         lastPacketReceived = SystemClock.elapsedRealtime();
         if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) {
-            Log.d(Config.LOGTAG, "[background stanza] " + element);
-        }
-        if (element instanceof IqPacket
-                && (((IqPacket) element).getType() == IqPacket.TYPE.SET)
-                && element.hasChild("jingle", Namespace.JINGLE)) {
-            return JinglePacket.upgrade((IqPacket) element);
-        } else {
-            return element;
+            Log.d(Config.LOGTAG, "[background stanza] " + stanza);
         }
+        return stanza;
     }
 
     private void processIq(final Tag currentTag) throws IOException {
-        final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
-        if (!packet.valid()) {
+        final Iq packet = processPacket(currentTag, Iq.class);
+        if (packet.isInvalid()) {
             Log.e(
                     Config.LOGTAG,
                     "encountered invalid iq from='"
@@ -1263,12 +1207,12 @@ public class XmppConnection implements Runnable {
                     account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
             return;
         }
-        if (packet instanceof JinglePacket jinglePacket && isBound) {
+        if (packet.hasExtension(Jingle.class) && packet.getType() == Iq.Type.SET && isBound) {
             if (this.jingleListener != null) {
-                this.jingleListener.onJinglePacketReceived(account, jinglePacket);
+                this.jingleListener.onJinglePacketReceived(account, packet);
             }
         } else {
-            final OnIqPacketReceived callback = getIqPacketReceivedCallback(packet);
+            final var callback = getIqPacketReceivedCallback(packet);
             if (callback == null) {
                 Log.d(
                         Config.LOGTAG,
@@ -1278,17 +1222,17 @@ public class XmppConnection implements Runnable {
                 return;
             }
             try {
-                callback.onIqPacketReceived(account, packet);
+                callback.accept(packet);
             } catch (final StateChangingError error) {
                 throw new StateChangingException(error.state);
             }
         }
     }
 
-    private OnIqPacketReceived getIqPacketReceivedCallback(final IqPacket stanza)
+    private Consumer<Iq> getIqPacketReceivedCallback(final Iq stanza)
             throws StateChangingException {
         final boolean isRequest =
-                stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET;
+                stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET;
         if (isRequest) {
             if (isBound) {
                 return this.unregisteredIqListener;
@@ -1328,8 +1272,9 @@ public class XmppConnection implements Runnable {
     }
 
     private void processMessage(final Tag currentTag) throws IOException {
-        final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
-        if (!packet.valid()) {
+        final var packet =
+                processPacket(currentTag, im.conversations.android.xmpp.model.stanza.Message.class);
+        if (packet.isInvalid()) {
             Log.e(
                     Config.LOGTAG,
                     "encountered invalid message from='"
@@ -1346,12 +1291,12 @@ public class XmppConnection implements Runnable {
                             + "Not processing message. Thread was interrupted");
             return;
         }
-        this.messageListener.onMessagePacketReceived(account, packet);
+        this.messageListener.accept(packet);
     }
 
     private void processPresence(final Tag currentTag) throws IOException {
-        final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
-        if (!packet.valid()) {
+        final var packet = processPacket(currentTag, Presence.class);
+        if (packet.isInvalid()) {
             Log.e(
                     Config.LOGTAG,
                     "encountered invalid presence from='"
@@ -1368,17 +1313,15 @@ public class XmppConnection implements Runnable {
                             + "Not processing presence. Thread was interrupted");
             return;
         }
-        this.presenceListener.onPresencePacketReceived(account, packet);
+        this.presenceListener.accept(packet);
     }
 
     private void sendStartTLS() throws IOException {
-        final Tag startTLS = Tag.empty("starttls");
-        startTLS.setAttribute("xmlns", Namespace.TLS);
-        tagWriter.writeTag(startTLS);
+        tagWriter.writeElement(new StartTls());
     }
 
-    private void switchOverToTls() throws XmlPullParserException, IOException {
-        tagReader.readTag();
+    private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
+        tagReader.readElement(currentTag, Proceed.class);
         final Socket socket = this.socket;
         final SSLSocket sslSocket = upgradeSocketToTls(socket);
         this.socket = sslSocket;
@@ -1439,11 +1382,13 @@ public class XmppConnection implements Runnable {
     }
 
     private void processStreamFeatures(final Tag currentTag) throws IOException {
-        this.streamFeatures = tagReader.readElement(currentTag);
+        this.streamFeatures =
+                tagReader.readElement(
+                        currentTag, im.conversations.android.xmpp.model.streams.Features.class);
         final boolean isSecure = isSecure();
         final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
         if (this.quickStartInProgress) {
-            if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
+            if (this.streamFeatures.hasStreamFeature(Authentication.class)) {
                 Log.d(
                         Config.LOGTAG,
                         account.getJid().asBareJid()
@@ -1452,8 +1397,7 @@ public class XmppConnection implements Runnable {
                 if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
                     return;
                 }
-                if (isFastTokenAvailable(
-                        this.streamFeatures.findChild("authentication", Namespace.SASL_2))) {
+                if (isFastTokenAvailable(this.streamFeatures.getExtension(Authentication.class))) {
                     Log.d(
                             Config.LOGTAG,
                             account.getJid().asBareJid()
@@ -1471,8 +1415,7 @@ public class XmppConnection implements Runnable {
             mXmppConnectionService.databaseBackend.updateAccount(account);
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
-        if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
-                && !features.encryptionEnabled) {
+        if (this.streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) {
             sendStartTLS();
         } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
                 && account.isOptionSet(Account.OPTION_REGISTER)) {
@@ -1489,15 +1432,15 @@ public class XmppConnection implements Runnable {
         } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
                 && account.isOptionSet(Account.OPTION_REGISTER)) {
             throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
-        } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)
+        } else if (this.streamFeatures.hasStreamFeature(Authentication.class)
                 && shouldAuthenticate
                 && isSecure) {
             authenticate(SaslMechanism.Version.SASL_2);
-        } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL)
+        } else if (this.streamFeatures.hasStreamFeature(Mechanisms.class)
                 && shouldAuthenticate
                 && isSecure) {
             authenticate(SaslMechanism.Version.SASL);
-        } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
+        } else if (this.streamFeatures.streamManagement()
                 && isSecure
                 && LoginInfo.isSuccess(loginInfo)
                 && streamId != null
@@ -1509,7 +1452,7 @@ public class XmppConnection implements Runnable {
                                 + ": resuming after stanza #"
                                 + stanzasReceived);
             }
-            final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
+            final var resume = new Resume(this.streamId.id, stanzasReceived);
             this.mSmCatchupMessageCounter.set(0);
             this.mWaitingForSmCatchup.set(true);
             this.tagWriter.writeStanzaAsync(resume);
@@ -1537,9 +1480,9 @@ public class XmppConnection implements Runnable {
 
     private void authenticate() throws IOException {
         final boolean isSecure = isSecure();
-        if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
+        if (isSecure && this.streamFeatures.hasStreamFeature(Authentication.class)) {
             authenticate(SaslMechanism.Version.SASL_2);
-        } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
+        } else if (isSecure && this.streamFeatures.hasStreamFeature(Mechanisms.class)) {
             authenticate(SaslMechanism.Version.SASL);
         } else {
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
@@ -1551,13 +1494,13 @@ public class XmppConnection implements Runnable {
     }
 
     private void authenticate(final SaslMechanism.Version version) throws IOException {
-        final Element authElement;
+        final AuthenticationStreamFeature authElement;
         if (version == SaslMechanism.Version.SASL) {
-            authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL);
+            authElement = this.streamFeatures.getExtension(Mechanisms.class);
         } else {
-            authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
+            authElement = this.streamFeatures.getExtension(Authentication.class);
         }
-        final Collection<String> mechanisms = SaslMechanism.mechanisms(authElement);
+        final Collection<String> mechanisms = authElement.getMechanismNames();
         final Element cbElement =
                 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
         final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
@@ -1569,26 +1512,27 @@ public class XmppConnection implements Runnable {
         final String firstMessage =
                 saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
         final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
-        final Element authenticate;
+        final StreamElement authenticate;
         if (version == SaslMechanism.Version.SASL) {
-            authenticate = new Element("auth", Namespace.SASL);
+            authenticate = new Auth();
             if (!Strings.isNullOrEmpty(firstMessage)) {
                 authenticate.setContent(firstMessage);
             }
             quickStartAvailable = false;
             this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
         } else if (version == SaslMechanism.Version.SASL_2) {
-            final Element inline = authElement.findChild("inline", Namespace.SASL_2);
-            final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
+            final Authentication authentication = (Authentication) authElement;
+            final var inline = authentication.getInline();
+            final boolean sm = inline != null && inline.hasExtension(StreamManagement.class);
             final HashedToken.Mechanism hashTokenRequest;
             if (usingFast) {
                 hashTokenRequest = null;
-            } else {
-                final Element fast =
-                        inline == null ? null : inline.findChild("fast", Namespace.FAST);
-                final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
+            } else if (inline != null) {
                 hashTokenRequest =
-                        HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
+                        HashedToken.Mechanism.best(
+                                inline.getFastMechanisms(), SSLSockets.version(this.socket));
+            } else {
+                hashTokenRequest = null;
             }
             final Collection<String> bindFeatures = Bind2.features(inline);
             quickStartAvailable =
@@ -1633,9 +1577,9 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private static boolean isFastTokenAvailable(final Element authentication) {
-        final Element inline = authentication == null ? null : authentication.findChild("inline");
-        return inline != null && inline.hasChild("fast", Namespace.FAST);
+    private static boolean isFastTokenAvailable(final Authentication authentication) {
+        final var inline = authentication == null ? null : authentication.getInline();
+        return inline != null && inline.hasExtension(Fast.class);
     }
 
     private void validate(
@@ -1649,7 +1593,7 @@ public class XmppConnection implements Runnable {
                             + mechanisms);
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
-        validateRequireChannelBinding(saslMechanism);
+        checkRequireChannelBinding(saslMechanism);
         if (SaslMechanism.hashedToken(saslMechanism)) {
             return;
         }
@@ -1668,7 +1612,7 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private void validateRequireChannelBinding(@NonNull final SaslMechanism mechanism)
+    private void checkRequireChannelBinding(@NonNull final SaslMechanism mechanism)
             throws StateChangingException {
         if (appSettings.isRequireChannelBinding()) {
             if (mechanism instanceof ChannelBindingMechanism) {
@@ -1679,19 +1623,44 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private Element generateAuthenticationRequest(
+    private void checkAssignedDomainOrThrow(final Jid jid) throws StateChangingException {
+        if (jid == null) {
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": bind response is missing jid");
+            throw new StateChangingException(Account.State.BIND_FAILURE);
+        }
+        final var current = this.account.getJid().getDomain();
+        if (jid.getDomain().equals(current)) {
+            return;
+        }
+        Log.d(
+                Config.LOGTAG,
+                account.getJid().asBareJid()
+                        + ": server tried to re-assign domain to "
+                        + jid.getDomain());
+        throw new StateChangingException(Account.State.BIND_FAILURE);
+    }
+
+    private void checkAssignedDomain(final Jid jid) {
+        try {
+            checkAssignedDomainOrThrow(jid);
+        } catch (final StateChangingException e) {
+            throw new StateChangingError(e.state);
+        }
+    }
+
+    private Authenticate generateAuthenticationRequest(
             final String firstMessage, final boolean usingFast) {
         return generateAuthenticationRequest(
                 firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
     }
 
-    private Element generateAuthenticationRequest(
+    private Authenticate generateAuthenticationRequest(
             final String firstMessage,
             final boolean usingFast,
             final HashedToken.Mechanism hashedTokenRequest,
             final Collection<String> bind,
             final boolean inlineStreamManagement) {
-        final Element authenticate = new Element("authenticate", Namespace.SASL_2);
+        final var authenticate = new Authenticate();
         if (!Strings.isNullOrEmpty(firstMessage)) {
             authenticate.addChild("initial-response").setContent(firstMessage);
         }
@@ -1712,31 +1681,29 @@ public class XmppConnection implements Runnable {
             authenticate.addChild(generateBindRequest(bind));
         }
         if (inlineStreamManagement && streamId != null) {
-            final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
+            final var resume = new Resume(this.streamId.id, stanzasReceived);
             this.mSmCatchupMessageCounter.set(0);
             this.mWaitingForSmCatchup.set(true);
-            authenticate.addChild(resume);
+            authenticate.addExtension(resume);
         }
         if (hashedTokenRequest != null) {
-            authenticate
-                    .addChild("request-token", Namespace.FAST)
-                    .setAttribute("mechanism", hashedTokenRequest.name());
+            authenticate.addExtension(new RequestToken(hashedTokenRequest));
         }
         if (usingFast) {
-            authenticate.addChild("fast", Namespace.FAST);
+            authenticate.addExtension(new Fast());
         }
         return authenticate;
     }
 
-    private Element generateBindRequest(final Collection<String> bindFeatures) {
+    private Bind generateBindRequest(final Collection<String> bindFeatures) {
         Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures);
-        final Element bind = new Element("bind", Namespace.BIND2);
+        final var bind = new Bind();
         bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
         if (bindFeatures.contains(Namespace.CARBONS)) {
-            bind.addChild("enable", Namespace.CARBONS);
+            bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable());
         }
         if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) {
-            bind.addChild(new EnablePacket());
+            bind.addExtension(new Enable());
         }
         return bind;
     }
@@ -1744,12 +1711,12 @@ public class XmppConnection implements Runnable {
     private void register() {
         final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
         if (preAuth != null && features.invite()) {
-            final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET);
+            final Iq preAuthRequest = new Iq(Iq.Type.SET);
             preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
             sendUnmodifiedIqPacket(
                     preAuthRequest,
-                    (account, response) -> {
-                        if (response.getType() == IqPacket.TYPE.RESULT) {
+                    (response) -> {
+                        if (response.getType() == Iq.Type.RESULT) {
                             sendRegistryRequest();
                         } else {
                             final String error = response.getErrorCondition();
@@ -1768,21 +1735,21 @@ public class XmppConnection implements Runnable {
     }
 
     private void sendRegistryRequest() {
-        final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
+        final Iq register = new Iq(Iq.Type.GET);
         register.query(Namespace.REGISTER);
         register.setTo(account.getDomain());
         sendUnmodifiedIqPacket(
                 register,
-                (account, packet) -> {
-                    if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+                (packet) -> {
+                    if (packet.getType() == Iq.Type.TIMEOUT) {
                         return;
                     }
-                    if (packet.getType() == IqPacket.TYPE.ERROR) {
+                    if (packet.getType() == Iq.Type.ERROR) {
                         throw new StateChangingError(Account.State.REGISTRATION_FAILED);
                     }
                     final Element query = packet.query(Namespace.REGISTER);
                     if (query.hasChild("username") && (query.hasChild("password"))) {
-                        final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET);
+                        final Iq register1 = new Iq(Iq.Type.SET);
                         final Element username =
                                 new Element("username").setContent(account.getUsername());
                         final Element password =
@@ -1790,7 +1757,7 @@ public class XmppConnection implements Runnable {
                         register1.query(Namespace.REGISTER).addChild(username);
                         register1.query().addChild(password);
                         register1.setFrom(account.getJid().asBareJid());
-                        sendUnmodifiedIqPacket(register1, registrationResponseListener, true);
+                        sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true);
                     } else if (query.hasChild("x", Namespace.DATA)) {
                         final Data data = Data.parse(query.findChild("x", Namespace.DATA));
                         final Element blob = query.findChild("data", "urn:xmpp:bob");
@@ -1858,6 +1825,45 @@ public class XmppConnection implements Runnable {
                 true);
     }
 
+    public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
+        final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
+        this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
+    }
+
+    private void processRegistrationResponse(final Iq response) {
+        if (response.getType() == Iq.Type.RESULT) {
+            account.setOption(Account.OPTION_REGISTER, false);
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": successfully registered new account on server");
+            throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
+        } else {
+            final Account.State state = getRegistrationFailedState(response);
+            throw new StateChangingError(state);
+        }
+    }
+
+    @NonNull
+    private static Account.State getRegistrationFailedState(final Iq response) {
+        final List<String> PASSWORD_TOO_WEAK_MESSAGES =
+                Arrays.asList("The password is too weak", "Please use a longer password.");
+        final var error = response.getError();
+        final var condition = error == null ? null : error.getCondition();
+        final Account.State state;
+        if (condition instanceof Condition.Conflict) {
+            state = Account.State.REGISTRATION_CONFLICT;
+        } else if (condition instanceof Condition.ResourceConstraint) {
+            state = Account.State.REGISTRATION_PLEASE_WAIT;
+        } else if (condition instanceof Condition.NotAcceptable
+                && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
+            state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
+        } else {
+            state = Account.State.REGISTRATION_FAILED;
+        }
+        return state;
+    }
+
     private void setAccountCreationFailed(final String url) {
         final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
         if (httpUrl != null && httpUrl.isHttps()) {
@@ -1905,61 +1911,36 @@ public class XmppConnection implements Runnable {
         } else {
             fixResource(mXmppConnectionService, account);
         }
-        final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+        final Iq iq = new Iq(Iq.Type.SET);
         final String resource =
                 Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource();
-        iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource);
+        iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource);
         this.sendUnmodifiedIqPacket(
                 iq,
-                (account, packet) -> {
-                    if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
+                (packet) -> {
+                    if (packet.getType() == Iq.Type.TIMEOUT) {
                         return;
                     }
-                    final Element bind = packet.findChild("bind");
-                    if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) {
+                    final var bind =
+                            packet.getExtension(
+                                    im.conversations.android.xmpp.model.bind.Bind.class);
+                    if (bind != null && packet.getType() == Iq.Type.RESULT) {
                         isBound = true;
-                        final Element jid = bind.findChild("jid");
-                        if (jid != null && jid.getContent() != null) {
-                            try {
-                                Jid assignedJid = Jid.ofEscaped(jid.getContent());
-                                if (!account.getJid().getDomain().equals(assignedJid.getDomain())) {
-                                    Log.d(
-                                            Config.LOGTAG,
-                                            account.getJid().asBareJid()
-                                                    + ": server tried to re-assign domain to "
-                                                    + assignedJid.getDomain());
-                                    throw new StateChangingError(Account.State.BIND_FAILURE);
-                                }
-                                if (account.setJid(assignedJid)) {
-                                    Log.d(
-                                            Config.LOGTAG,
-                                            account.getJid().asBareJid()
-                                                    + ": jid changed during bind. updating database");
-                                    mXmppConnectionService.databaseBackend.updateAccount(account);
-                                }
-                                if (streamFeatures.hasChild("session")
-                                        && !streamFeatures
-                                                .findChild("session")
-                                                .hasChild("optional")) {
-                                    sendStartSession();
-                                } else {
-                                    final boolean waitForDisco = enableStreamManagement();
-                                    sendPostBindInitialization(waitForDisco, false);
-                                }
-                                return;
-                            } catch (final IllegalArgumentException e) {
-                                Log.d(
-                                        Config.LOGTAG,
-                                        account.getJid().asBareJid()
-                                                + ": server reported invalid jid ("
-                                                + jid.getContent()
-                                                + ") on bind");
-                            }
-                        } else {
+                        final Jid assignedJid = bind.getJid();
+                        checkAssignedDomain(assignedJid);
+                        if (account.setJid(assignedJid)) {
                             Log.d(
                                     Config.LOGTAG,
-                                    account.getJid()
-                                            + ": disconnecting because of bind failure. (no jid)");
+                                    account.getJid().asBareJid()
+                                            + ": jid changed during bind. updating database");
+                            mXmppConnectionService.databaseBackend.updateAccount(account);
+                        }
+                        if (streamFeatures.hasChild("session")
+                                && !streamFeatures.findChild("session").hasChild("optional")) {
+                            sendStartSession();
+                        } else {
+                            final boolean waitForDisco = enableStreamManagement();
+                            sendPostBindInitialization(waitForDisco, false);
                         }
                     } else {
                         Log.d(

src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java 🔗

@@ -8,7 +8,8 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import java.util.List;
 import java.util.Map;
@@ -47,8 +48,9 @@ public abstract class AbstractContentMap<
         return ImmutableList.copyOf(contents.keySet());
     }
 
-    JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
-        final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
+    Iq toJinglePacket(final Jingle.Action action, final String sessionId) {
+        final Iq iq = new Iq(Iq.Type.SET);
+        final var jinglePacket = iq.addExtension(new Jingle(action, sessionId));
         for (final Map.Entry<String, DescriptionTransport<D, T>> entry : this.contents.entrySet()) {
             final DescriptionTransport<D, T> descriptionTransport = entry.getValue();
             final Content content =
@@ -65,7 +67,7 @@ public abstract class AbstractContentMap<
         if (this.group != null) {
             jinglePacket.addGroup(this.group);
         }
-        return jinglePacket;
+        return iq;
     }
 
     void requireContentDescriptions() {

src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java 🔗

@@ -19,9 +19,9 @@ import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -184,10 +184,10 @@ public abstract class AbstractJingleConnection {
         return TERMINATED.contains(this.state);
     }
 
-    abstract void deliverPacket(JinglePacket jinglePacket);
+    abstract void deliverPacket(Iq jinglePacket);
 
     protected void receiveOutOfOrderAction(
-            final JinglePacket jinglePacket, final JinglePacket.Action action) {
+            final Iq jinglePacket, final Jingle.Action action) {
         Log.d(
                 Config.LOGTAG,
                 String.format(
@@ -205,7 +205,7 @@ public abstract class AbstractJingleConnection {
         }
     }
 
-    protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
+    protected void terminateWithOutOfOrder(final Iq jinglePacket) {
         Log.d(
                 Config.LOGTAG,
                 id.account.getJid().asBareJid() + ": terminating session with out-of-order");
@@ -235,37 +235,38 @@ public abstract class AbstractJingleConnection {
         if (previous != State.NULL && trigger != null) {
             trigger.accept(target);
         }
-        final JinglePacket jinglePacket =
-                new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+        final var iq = new Iq(Iq.Type.SET);
+        final var jinglePacket =
+                iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
         jinglePacket.setReason(reason, text);
-        send(jinglePacket);
+        send(iq);
         finish();
     }
 
-    protected void send(final JinglePacket jinglePacket) {
+    protected void send(final Iq jinglePacket) {
         jinglePacket.setTo(id.with);
         xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
     }
 
-    protected void respondOk(final JinglePacket jinglePacket) {
+    protected void respondOk(final Iq jinglePacket) {
         xmppConnectionService.sendIqPacket(
-                id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
+                id.account, jinglePacket.generateResponse(Iq.Type.RESULT), null);
     }
 
-    protected void respondWithTieBreak(final JinglePacket jinglePacket) {
+    protected void respondWithTieBreak(final Iq jinglePacket) {
         respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
     }
 
-    protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
+    protected void respondWithOutOfOrder(final Iq jinglePacket) {
         respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
     }
 
-    protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
+    protected void respondWithItemNotFound(final Iq jinglePacket) {
         respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
     }
 
     private void respondWithJingleError(
-            final IqPacket original,
+            final Iq original,
             String jingleCondition,
             String condition,
             String conditionType) {
@@ -273,18 +274,18 @@ public abstract class AbstractJingleConnection {
                 id.account, original, jingleCondition, condition, conditionType);
     }
 
-    private synchronized void handleIqResponse(final Account account, final IqPacket response) {
-        if (response.getType() == IqPacket.TYPE.ERROR) {
+    private synchronized void handleIqResponse(final Iq response) {
+        if (response.getType() == Iq.Type.ERROR) {
             handleIqErrorResponse(response);
             return;
         }
-        if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+        if (response.getType() == Iq.Type.TIMEOUT) {
             handleIqTimeoutResponse(response);
         }
     }
 
-    protected void handleIqErrorResponse(final IqPacket response) {
-        Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
+    protected void handleIqErrorResponse(final Iq response) {
+        Preconditions.checkArgument(response.getType() == Iq.Type.ERROR);
         final String errorCondition = response.getErrorCondition();
         Log.d(
                 Config.LOGTAG,
@@ -316,8 +317,8 @@ public abstract class AbstractJingleConnection {
         this.finish();
     }
 
-    protected void handleIqTimeoutResponse(final IqPacket response) {
-        Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
+    protected void handleIqTimeoutResponse(final Iq response) {
+        Preconditions.checkArgument(response.getType() == Iq.Type.TIMEOUT);
         Log.d(
                 Config.LOGTAG,
                 id.account.getJid().asBareJid()
@@ -361,8 +362,8 @@ public abstract class AbstractJingleConnection {
             this.sessionId = sessionId;
         }
 
-        public static Id of(Account account, JinglePacket jinglePacket) {
-            return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId());
+        public static Id of(Account account, Iq iq, final Jingle jingle) {
+            return new Id(account, iq.getFrom(), jingle.getSessionId());
         }
 
         public static Id of(Account account, Jid with, final String sessionId) {

src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java 🔗

@@ -13,11 +13,10 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
-import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+import im.conversations.android.xmpp.model.jingle.Jingle;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -39,7 +38,7 @@ public class FileTransferContentMap
         super(group, contents);
     }
 
-    public static FileTransferContentMap of(final JinglePacket jinglePacket) {
+    public static FileTransferContentMap of(final Jingle jinglePacket) {
         final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
                 contents = of(jinglePacket.getJingleContents());
         return new FileTransferContentMap(jinglePacket.getGroup(), contents);

src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java 🔗

@@ -10,7 +10,7 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.IP;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import org.webrtc.PeerConnection;
 
@@ -20,9 +20,9 @@ import java.util.List;
 
 public final class IceServers {
 
-    public static List<PeerConnection.IceServer> parse(final IqPacket response) {
+    public static List<PeerConnection.IceServer> parse(final Iq response) {
         ImmutableList.Builder<PeerConnection.IceServer> listBuilder = new ImmutableList.Builder<>();
-        if (response.getType() == IqPacket.TYPE.RESULT) {
+        if (response.getType() == Iq.Type.RESULT) {
             final Element services =
                     response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
             final List<Element> children =

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -34,14 +34,13 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
 import eu.siacs.conversations.xmpp.jingle.transports.Transport;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import java.lang.ref.WeakReference;
 import java.security.SecureRandom;
@@ -77,9 +76,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE);
     }
 
-    public void deliverPacket(final Account account, final JinglePacket packet) {
-        final String sessionId = packet.getSessionId();
-        final JinglePacket.Action action = packet.getAction();
+    public void deliverPacket(final Account account, final Iq packet) {
+        final var jingle = packet.getExtension(Jingle.class);
+        Preconditions.checkNotNull(jingle,"Passed iq packet w/o jingle extension to Connection Manager");
+        final String sessionId = jingle.getSessionId();
+        final Jingle.Action action = jingle.getAction();
         if (sessionId == null) {
             respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel");
             return;
@@ -88,13 +89,13 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             respondWithJingleError(account, packet, null, "bad-request", "cancel");
             return;
         }
-        final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet);
+        final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet, jingle);
         final AbstractJingleConnection existingJingleConnection = connections.get(id);
         if (existingJingleConnection != null) {
             existingJingleConnection.deliverPacket(packet);
-        } else if (action == JinglePacket.Action.SESSION_INITIATE) {
+        } else if (action == Jingle.Action.SESSION_INITIATE) {
             final Jid from = packet.getFrom();
-            final Content content = packet.getJingleContent();
+            final Content content = jingle.getJingleContent();
             final String descriptionNamespace =
                     content == null ? null : content.getDescriptionNamespace();
             final AbstractJingleConnection connection;
@@ -162,14 +163,14 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     private void sendSessionTerminate(
-            final Account account, final IqPacket request, final AbstractJingleConnection.Id id) {
+            final Account account, final Iq request, final AbstractJingleConnection.Id id) {
         mXmppConnectionService.sendIqPacket(
-                account, request.generateResponse(IqPacket.TYPE.RESULT), null);
-        final JinglePacket sessionTermination =
-                new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
-        sessionTermination.setTo(id.with);
+                account, request.generateResponse(Iq.Type.RESULT), null);
+        final var iq = new Iq(Iq.Type.SET);
+        iq.setTo(id.with);
+        final var sessionTermination = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
         sessionTermination.setReason(Reason.BUSY, null);
-        mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
+        mXmppConnectionService.sendIqPacket(account, iq, null);
     }
 
     private boolean isUsingClearNet(final Account account) {
@@ -263,11 +264,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     void respondWithJingleError(
             final Account account,
-            final IqPacket original,
+            final Iq original,
             final String jingleCondition,
             final String condition,
             final String conditionType) {
-        final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR);
+        final Iq response = original.generateResponse(Iq.Type.ERROR);
         final Element error = response.addChild("error");
         error.setAttribute("type", conditionType);
         error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
@@ -438,7 +439,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     final int activeDevices = account.activeDevicesWithRtpCapability();
                     Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices);
                     if (activeDevices == 0) {
-                        final MessagePacket reject =
+                        final var reject =
                                 mXmppConnectionService
                                         .getMessageGenerator()
                                         .sessionReject(from, sessionId);
@@ -492,10 +493,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     if (remoteMsgId == null) {
                         return;
                     }
-                    final MessagePacket errorMessage = new MessagePacket();
+                    final var errorMessage =
+                            new im.conversations.android.xmpp.model.stanza.Message();
                     errorMessage.setTo(from);
                     errorMessage.setId(remoteMsgId);
-                    errorMessage.setType(MessagePacket.TYPE_ERROR);
+                    errorMessage.setType(im.conversations.android.xmpp.model.stanza.Message.Type.ERROR);
                     final Element error = errorMessage.addChild("error");
                     error.setAttribute("code", "404");
                     error.setAttribute("type", "cancel");
@@ -720,7 +722,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     rtpSessionProposal.sessionId,
                     RtpEndUserState.RETRACTED);
         }
-        final MessagePacket messagePacket =
+        final var messagePacket =
                 mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
         writeLogMissedOutgoing(
                 account,
@@ -790,7 +792,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING);
             mXmppConnectionService.notifyJingleRtpConnectionUpdate(
                     account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE);
-            final MessagePacket messagePacket =
+            final var messagePacket =
                     mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
             mXmppConnectionService.sendMessagePacket(account, messagePacket);
             return proposal;
@@ -800,7 +802,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     public void sendJingleMessageFinish(
             final Contact contact, final String sessionId, final Reason reason) {
         final var account = contact.getAccount();
-        final MessagePacket messagePacket =
+        final var messagePacket =
                 mXmppConnectionService
                         .getMessageGenerator()
                         .sessionFinish(contact.getJid(), sessionId, reason);
@@ -842,7 +844,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         return false;
     }
 
-    public void deliverIbbPacket(final Account account, final IqPacket packet) {
+    public void deliverIbbPacket(final Account account, final Iq packet) {
         final String sid;
         final Element payload;
         final InbandBytestreamsTransport.PacketType packetType;
@@ -868,7 +870,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     Config.LOGTAG,
                     account.getJid().asBareJid() + ": unable to deliver ibb packet. missing sid");
             account.getXmppConnection()
-                    .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
+                    .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null);
             return;
         }
         for (final AbstractJingleConnection connection : this.connections.values()) {
@@ -879,11 +881,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                         if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) {
                             account.getXmppConnection()
                                     .sendIqPacket(
-                                            packet.generateResponse(IqPacket.TYPE.RESULT), null);
+                                            packet.generateResponse(Iq.Type.RESULT), null);
                         } else {
                             account.getXmppConnection()
                                     .sendIqPacket(
-                                            packet.generateResponse(IqPacket.TYPE.ERROR), null);
+                                            packet.generateResponse(Iq.Type.ERROR), null);
                         }
                         return;
                     }
@@ -894,7 +896,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 Config.LOGTAG,
                 account.getJid().asBareJid() + ": unable to deliver ibb packet with sid=" + sid);
         account.getXmppConnection()
-                .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
+                .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null);
     }
 
     public void notifyRebound(final Account account) {
@@ -945,7 +947,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                             account.getJid().asBareJid()
                                     + ": resending session proposal to "
                                     + proposal.with);
-                    final MessagePacket messagePacket =
+                    final var messagePacket =
                             mXmppConnectionService.getMessageGenerator().sessionProposal(proposal);
                     mXmppConnectionService.sendMessagePacket(account, messagePacket);
                 }

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java 🔗

@@ -31,7 +31,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
@@ -39,7 +38,9 @@ import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
 import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
 import eu.siacs.conversations.xmpp.jingle.transports.Transport;
 import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import org.bouncycastle.crypto.engines.AESEngine;
 import org.bouncycastle.crypto.io.CipherInputStream;
@@ -112,22 +113,23 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
     }
 
     @Override
-    void deliverPacket(final JinglePacket jinglePacket) {
-        switch (jinglePacket.getAction()) {
-            case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
-            case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
-            case SESSION_INFO -> receiveSessionInfo(jinglePacket);
-            case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
-            case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket);
-            case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
-            case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket);
+    void deliverPacket(final Iq iq) {
+        final var jingle = iq.getExtension(Jingle.class);
+        switch (jingle.getAction()) {
+            case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle);
+            case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle);
+            case SESSION_INFO -> receiveSessionInfo(iq, jingle);
+            case SESSION_TERMINATE -> receiveSessionTerminate(iq, jingle);
+            case TRANSPORT_ACCEPT -> receiveTransportAccept(iq, jingle);
+            case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle);
+            case TRANSPORT_REPLACE -> receiveTransportReplace(iq, jingle);
             default -> {
-                respondOk(jinglePacket);
+                respondOk(iq);
                 Log.d(
                         Config.LOGTAG,
                         String.format(
                                 "%s: received unhandled jingle action %s",
-                                id.account.getJid().asBareJid(), jinglePacket.getAction()));
+                                id.account.getJid().asBareJid(), jingle.getAction()));
             }
         }
     }
@@ -203,33 +205,34 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         if (transition(
                 State.SESSION_INITIALIZED,
                 () -> this.initiatorFileTransferContentMap = contentMap)) {
-            final var jinglePacket =
-                    contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+            final var iq =
+                    contentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId);
+            final var jingle = iq.getExtension(Jingle.class);
             if (xmppAxolotlMessage != null) {
                 this.transportSecurity =
                         new TransportSecurity(
                                 xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV());
-                final var contents = jinglePacket.getJingleContents();
+                final var contents = jingle.getJingleContents();
                 final var rawContent =
                         contents.get(Iterables.getOnlyElement(contentMap.contents.keySet()));
                 if (rawContent != null) {
                     rawContent.setSecurity(xmppAxolotlMessage);
                 }
             }
-            jinglePacket.setTo(id.with);
+            iq.setTo(id.with);
             xmppConnectionService.sendIqPacket(
                     id.account,
-                    jinglePacket,
-                    (a, response) -> {
-                        if (response.getType() == IqPacket.TYPE.RESULT) {
+                    iq,
+                    (response) -> {
+                        if (response.getType() == Iq.Type.RESULT) {
                             xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
                             return;
                         }
-                        if (response.getType() == IqPacket.TYPE.ERROR) {
+                        if (response.getType() == Iq.Type.ERROR) {
                             handleIqErrorResponse(response);
                             return;
                         }
-                        if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                        if (response.getType() == Iq.Type.TIMEOUT) {
                             handleIqTimeoutResponse(response);
                         }
                     });
@@ -237,15 +240,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveSessionAccept(final JinglePacket jinglePacket) {
+    private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) {
         Log.d(Config.LOGTAG, "receive file transfer session accept");
         if (isResponder()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT);
             return;
         }
         final FileTransferContentMap contentMap;
         try {
-            contentMap = FileTransferContentMap.of(jinglePacket);
+            contentMap = FileTransferContentMap.of(jingle);
             contentMap.requireOnlyFileTransferDescription();
         } catch (final RuntimeException e) {
             Log.d(
@@ -261,7 +264,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
     }
 
     private void receiveSessionAccept(
-            final JinglePacket jinglePacket, final FileTransferContentMap contentMap) {
+            final Iq jinglePacket, final FileTransferContentMap contentMap) {
         if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) {
             respondOk(jinglePacket);
             final var transport = this.transport;
@@ -280,7 +283,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             Log.d(
                     Config.LOGTAG,
                     id.account.getJid().asBareJid() + ": receive out of order session-accept");
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT);
         }
     }
 
@@ -309,16 +312,16 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveSessionInitiate(final JinglePacket jinglePacket) {
+    private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) {
         if (isInitiator()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE);
             return;
         }
         Log.d(Config.LOGTAG, "receive session initiate " + jinglePacket);
         final FileTransferContentMap contentMap;
         final FileTransferDescription.File file;
         try {
-            contentMap = FileTransferContentMap.of(jinglePacket);
+            contentMap = FileTransferContentMap.of(jingle);
             contentMap.requireContentDescriptions();
             file = contentMap.requireOnlyFile();
             // TODO check is offer
@@ -332,7 +335,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             return;
         }
         final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
-        final var contents = jinglePacket.getJingleContents();
+        final var contents = jingle.getJingleContents();
         final var rawContent = contents.get(Iterables.getOnlyElement(contentMap.contents.keySet()));
         final var security =
                 rawContent == null ? null : rawContent.getSecurity(jinglePacket.getFrom());
@@ -349,7 +352,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
     }
 
     private void receiveSessionInitiate(
-            final JinglePacket jinglePacket,
+            final Iq jinglePacket,
             final FileTransferContentMap contentMap,
             final FileTransferDescription.File file,
             final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage) {
@@ -396,7 +399,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             Log.d(
                     Config.LOGTAG,
                     id.account.getJid().asBareJid() + ": receive out of order session-initiate");
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE);
         }
     }
 
@@ -453,9 +456,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
 
     private void sendSessionAccept(final FileTransferContentMap contentMap) {
         setLocalContentMap(contentMap);
-        final var jinglePacket =
-                contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
-        send(jinglePacket);
+        final var iq =
+                contentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId);
+        send(iq);
         // this needs to come after session-accept or else our candidate-error might arrive first
         this.transport.connect();
         this.transport.readyToSentAdditionalCandidates();
@@ -541,9 +544,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
     }
 
-    private void receiveSessionInfo(final JinglePacket jinglePacket) {
+    private void receiveSessionInfo(final Iq jinglePacket, final Jingle jingle) {
         respondOk(jinglePacket);
-        final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket);
+        final var sessionInfo = FileTransferDescription.getSessionInfo(jingle);
         if (sessionInfo instanceof FileTransferDescription.Checksum checksum) {
             receiveSessionInfoChecksum(checksum);
         } else if (sessionInfo instanceof FileTransferDescription.Received received) {
@@ -559,9 +562,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         Log.d(Config.LOGTAG, "peer confirmed received " + received);
     }
 
-    private void receiveSessionTerminate(final JinglePacket jinglePacket) {
+    private void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) {
         respondOk(jinglePacket);
-        final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
+        final Jingle.ReasonWrapper wrapper = jingle.getReason();
         final State previous = this.state;
         Log.d(
                 Config.LOGTAG,
@@ -590,15 +593,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         finish();
     }
 
-    private void receiveTransportAccept(final JinglePacket jinglePacket) {
+    private void receiveTransportAccept(final Iq jinglePacket, final Jingle jingle) {
         if (isResponder()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_ACCEPT);
             return;
         }
         Log.d(Config.LOGTAG, "receive transport accept " + jinglePacket);
         final GenericTransportInfo transportInfo;
         try {
-            transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+            transportInfo = FileTransferContentMap.of(jingle).requireOnlyTransportInfo();
         } catch (final RuntimeException e) {
             Log.d(
                     Config.LOGTAG,
@@ -610,15 +613,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             return;
         }
         if (isInState(State.SESSION_ACCEPTED)) {
-            final var group = jinglePacket.getGroup();
+            final var group = jingle.getGroup();
             receiveTransportAccept(jinglePacket, new Transport.TransportInfo(transportInfo, group));
         } else {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_ACCEPT);
         }
     }
 
     private void receiveTransportAccept(
-            final JinglePacket jinglePacket, final Transport.TransportInfo transportInfo) {
+            final Iq jinglePacket, final Transport.TransportInfo transportInfo) {
         final FileTransferContentMap remoteContentMap =
                 getRemoteContentMap().withTransport(transportInfo);
         setRemoteContentMap(remoteContentMap);
@@ -637,11 +640,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveTransportInfo(final JinglePacket jinglePacket) {
+    private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) {
         final FileTransferContentMap contentMap;
         final GenericTransportInfo transportInfo;
         try {
-            contentMap = FileTransferContentMap.of(jinglePacket);
+            contentMap = FileTransferContentMap.of(jingle);
             transportInfo = contentMap.requireOnlyTransportInfo();
         } catch (final RuntimeException e) {
             Log.d(
@@ -725,14 +728,14 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveTransportReplace(final JinglePacket jinglePacket) {
+    private void receiveTransportReplace(final Iq jinglePacket, final Jingle jingle) {
         if (isInitiator()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_REPLACE);
             return;
         }
         final GenericTransportInfo transportInfo;
         try {
-            transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+            transportInfo = FileTransferContentMap.of(jingle).requireOnlyTransportInfo();
         } catch (final RuntimeException e) {
             Log.d(
                     Config.LOGTAG,
@@ -746,12 +749,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
         if (isInState(State.SESSION_ACCEPTED)) {
             receiveTransportReplace(jinglePacket, transportInfo);
         } else {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_REPLACE);
         }
     }
 
     private void receiveTransportReplace(
-            final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) {
+            final Iq jinglePacket, final GenericTransportInfo transportInfo) {
         respondOk(jinglePacket);
         final Transport currentTransport = this.transport;
         if (currentTransport != null) {
@@ -796,11 +799,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
 
     private void sendTransportAccept(final FileTransferContentMap contentMap) {
         setLocalContentMap(contentMap);
-        final var jinglePacket =
+        final var iq =
                 contentMap
                         .transportInfo()
-                        .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId);
-        send(jinglePacket);
+                        .toJinglePacket(Jingle.Action.TRANSPORT_ACCEPT, id.sessionId);
+        send(iq);
         transport.connect();
     }
 
@@ -982,11 +985,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
     }
 
     private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) {
-        final var jinglePacket =
-                new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId);
-        jinglePacket.addJingleChild(sessionInfo.asElement());
-        jinglePacket.setTo(this.id.with);
-        send(jinglePacket);
+        final var iq = new Iq(Iq.Type.SET);
+        final var jinglePacket = iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId));
+        jinglePacket.addChild(sessionInfo.asElement());
+        send(iq);
     }
 
     @Override
@@ -1039,11 +1041,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
 
     private void sendTransportReplace(final FileTransferContentMap contentMap) {
         setLocalContentMap(contentMap);
-        final var jinglePacket =
+        final var iq =
                 contentMap
                         .transportInfo()
-                        .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId);
-        send(jinglePacket);
+                        .toJinglePacket(Jingle.Action.TRANSPORT_REPLACE, id.sessionId);
+        send(iq);
     }
 
     @Override
@@ -1068,9 +1070,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                             + contentName);
             return;
         }
-        final JinglePacket jinglePacket =
-                transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        send(jinglePacket);
+        final Iq iq =
+                transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        send(iq);
     }
 
     @Override
@@ -1081,12 +1083,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             Log.e(Config.LOGTAG, "local content map is null on candidate used");
             return;
         }
-        final var jinglePacket =
+        final var iq =
                 contentMap
                         .candidateUsed(streamId, candidate.cid)
-                        .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        Log.d(Config.LOGTAG, "sending candidate used " + jinglePacket);
-        send(jinglePacket);
+                        .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        Log.d(Config.LOGTAG, "sending candidate used " + iq);
+        send(iq);
     }
 
     @Override
@@ -1096,12 +1098,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             Log.e(Config.LOGTAG, "local content map is null on candidate used");
             return;
         }
-        final var jinglePacket =
+        final var iq =
                 contentMap
                         .candidateError(streamId)
-                        .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        Log.d(Config.LOGTAG, "sending candidate error " + jinglePacket);
-        send(jinglePacket);
+                        .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        Log.d(Config.LOGTAG, "sending candidate error " + iq);
+        send(iq);
     }
 
     @Override
@@ -1111,11 +1113,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             Log.e(Config.LOGTAG, "local content map is null on candidate used");
             return;
         }
-        final var jinglePacket =
+        final var iq =
                 contentMap
                         .proxyActivated(streamId, candidate.cid)
-                        .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        send(jinglePacket);
+                        .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        send(iq);
     }
 
     @Override
@@ -1251,10 +1253,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                         message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED);
             }
             terminateTransport();
-            final JinglePacket jinglePacket =
-                    new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
-            jinglePacket.setReason(reason, "User requested to stop file transfer");
-            send(jinglePacket);
+            final Iq iq = new Iq(Iq.Type.SET);
+            final var jingle = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId));
+            jingle.setReason(reason, "User requested to stop file transfer");
+            send(iq);
             finish();
             return true;
         } else {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -43,13 +43,13 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+import im.conversations.android.xmpp.model.jingle.Jingle;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import org.webrtc.EglBase;
 import org.webrtc.IceCandidate;
@@ -139,24 +139,25 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     @Override
-    synchronized void deliverPacket(final JinglePacket jinglePacket) {
-        switch (jinglePacket.getAction()) {
-            case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
-            case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
-            case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
-            case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
-            case CONTENT_ADD -> receiveContentAdd(jinglePacket);
-            case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket);
-            case CONTENT_REJECT -> receiveContentReject(jinglePacket);
-            case CONTENT_REMOVE -> receiveContentRemove(jinglePacket);
-            case CONTENT_MODIFY -> receiveContentModify(jinglePacket);
+    synchronized void deliverPacket(final Iq iq) {
+        final var jingle = iq.getExtension(Jingle.class);
+        switch (jingle.getAction()) {
+            case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle);
+            case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle);
+            case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle);
+            case SESSION_TERMINATE -> receiveSessionTerminate(iq);
+            case CONTENT_ADD -> receiveContentAdd(iq, jingle);
+            case CONTENT_ACCEPT -> receiveContentAccept(iq);
+            case CONTENT_REJECT -> receiveContentReject(iq, jingle);
+            case CONTENT_REMOVE -> receiveContentRemove(iq, jingle);
+            case CONTENT_MODIFY -> receiveContentModify(iq, jingle);
             default -> {
-                respondOk(jinglePacket);
+                respondOk(iq);
                 Log.d(
                         Config.LOGTAG,
                         String.format(
                                 "%s: received unhandled jingle action %s",
-                                id.account.getJid().asBareJid(), jinglePacket.getAction()));
+                                id.account.getJid().asBareJid(), jingle.getAction()));
             }
         }
     }
@@ -183,9 +184,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveSessionTerminate(final JinglePacket jinglePacket) {
+    private void receiveSessionTerminate(final Iq jinglePacket) {
         respondOk(jinglePacket);
-        final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
+        final var jingle = jinglePacket.getExtension(Jingle.class);
+        final Jingle.ReasonWrapper wrapper = jingle.getReason();
         final State previous = this.state;
         Log.d(
                 Config.LOGTAG,
@@ -214,7 +216,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         finish();
     }
 
-    private void receiveTransportInfo(final JinglePacket jinglePacket) {
+    private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) {
         // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to
         // INITIALIZED only after transport-info has been received
         if (isInState(
@@ -225,7 +227,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                 State.SESSION_ACCEPTED)) {
             final RtpContentMap contentMap;
             try {
-                contentMap = RtpContentMap.of(jinglePacket);
+                contentMap = RtpContentMap.of(jingle);
             } catch (final IllegalArgumentException | NullPointerException e) {
                 Log.d(
                         Config.LOGTAG,
@@ -255,7 +257,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void receiveTransportInfo(
-            final JinglePacket jinglePacket, final RtpContentMap contentMap) {
+            final Iq jinglePacket, final RtpContentMap contentMap) {
         final Set<Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>>
                 candidates = contentMap.contents.entrySet();
         final RtpContentMap remote = getRemoteContentMap();
@@ -294,17 +296,17 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveContentAdd(final JinglePacket jinglePacket) {
+    private void receiveContentAdd(final Iq iq, final Jingle jingle) {
         final RtpContentMap modification;
         try {
-            modification = RtpContentMap.of(jinglePacket);
+            modification = RtpContentMap.of(jingle);
             modification.requireContentDescriptions();
         } catch (final RuntimeException e) {
             Log.d(
                     Config.LOGTAG,
                     id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
                     Throwables.getRootCause(e));
-            respondOk(jinglePacket);
+            respondOk(iq);
             webRTCWrapper.close();
             sendSessionTerminate(Reason.of(e), e.getMessage());
             return;
@@ -320,12 +322,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
                     new FutureCallback<>() {
                         @Override
                         public void onSuccess(final RtpContentMap rtpContentMap) {
-                            receiveContentAdd(jinglePacket, rtpContentMap);
+                            receiveContentAdd(iq, rtpContentMap);
                         }
 
                         @Override
                         public void onFailure(@NonNull Throwable throwable) {
-                            respondOk(jinglePacket);
+                            respondOk(iq);
                             final Throwable rootCause = Throwables.getRootCause(throwable);
                             Log.d(
                                     Config.LOGTAG,
@@ -339,12 +341,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
                     },
                     MoreExecutors.directExecutor());
         } else {
-            terminateWithOutOfOrder(jinglePacket);
+            terminateWithOutOfOrder(iq);
         }
     }
 
     private void receiveContentAdd(
-            final JinglePacket jinglePacket, final RtpContentMap modification) {
+            final Iq jinglePacket, final RtpContentMap modification) {
         final RtpContentMap remote = getRemoteContentMap();
         if (!Collections.disjoint(modification.getNames(), remote.getNames())) {
             respondOk(jinglePacket);
@@ -396,10 +398,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveContentAccept(final JinglePacket jinglePacket) {
+    private void receiveContentAccept(final Iq jinglePacket) {
+        final var jingle = jinglePacket.getExtension(Jingle.class);
         final RtpContentMap receivedContentAccept;
         try {
-            receivedContentAccept = RtpContentMap.of(jinglePacket);
+            receivedContentAccept = RtpContentMap.of(jingle);
             receivedContentAccept.requireContentDescriptions();
         } catch (final RuntimeException e) {
             Log.d(
@@ -484,14 +487,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
         updateEndUserState();
     }
 
-    private void receiveContentModify(final JinglePacket jinglePacket) {
+    private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) {
         if (this.state != State.SESSION_ACCEPTED) {
             terminateWithOutOfOrder(jinglePacket);
             return;
         }
         final Map<String, Content.Senders> modification =
                 Maps.transformEntries(
-                        jinglePacket.getJingleContents(), (key, value) -> value.getSenders());
+                        jingle.getJingleContents(), (key, value) -> value.getSenders());
         final boolean isInitiator = isInitiator();
         final RtpContentMap currentOutgoing = this.outgoingContentAdd;
         final RtpContentMap remoteContentMap = this.getRemoteContentMap();
@@ -594,10 +597,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
         return candidateBuilder.build();
     }
 
-    private void receiveContentReject(final JinglePacket jinglePacket) {
+    private void receiveContentReject(final Iq jinglePacket, final Jingle jingle) {
         final RtpContentMap receivedContentReject;
         try {
-            receivedContentReject = RtpContentMap.of(jinglePacket);
+            receivedContentReject = RtpContentMap.of(jingle);
         } catch (final RuntimeException e) {
             Log.d(
                     Config.LOGTAG,
@@ -650,10 +653,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
                         + summary);
     }
 
-    private void receiveContentRemove(final JinglePacket jinglePacket) {
+    private void receiveContentRemove(final Iq jinglePacket, final Jingle jingle) {
         final RtpContentMap receivedContentRemove;
         try {
-            receivedContentRemove = RtpContentMap.of(jinglePacket);
+            receivedContentRemove = RtpContentMap.of(jingle);
             receivedContentRemove.requireContentDescriptions();
         } catch (final RuntimeException e) {
             Log.d(
@@ -687,8 +690,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
                     String.format(
                             "%s only supports %s as a means to retract a not yet accepted %s",
                             BuildConfig.APP_NAME,
-                            JinglePacket.Action.CONTENT_REMOVE,
-                            JinglePacket.Action.CONTENT_ADD));
+                            Jingle.Action.CONTENT_REMOVE,
+                            Jingle.Action.CONTENT_ADD));
         }
     }
 
@@ -713,10 +716,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
             return;
         }
         this.outgoingContentAdd = null;
-        final JinglePacket retract =
+        final Iq retract =
                 outgoingContentAdd
                         .toStub()
-                        .toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId);
+                        .toJinglePacket(Jingle.Action.CONTENT_REMOVE, id.sessionId);
         this.send(retract);
         Log.d(
                 Config.LOGTAG,
@@ -772,16 +775,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
                         "content addition is receive only. we want to upgrade to 'both'");
                 final RtpContentMap modifiedSenders =
                         incomingContentAdd.modifiedSenders(Content.Senders.BOTH);
-                final JinglePacket proposedContentModification =
+                final Iq proposedContentModification =
                         modifiedSenders
                                 .toStub()
-                                .toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId);
+                                .toJinglePacket(Jingle.Action.CONTENT_MODIFY, id.sessionId);
                 proposedContentModification.setTo(id.with);
                 xmppConnectionService.sendIqPacket(
                         id.account,
                         proposedContentModification,
-                        (account, response) -> {
-                            if (response.getType() == IqPacket.TYPE.RESULT) {
+                        (response) -> {
+                            if (response.getType() == Iq.Type.RESULT) {
                                 Log.d(
                                         Config.LOGTAG,
                                         id.account.getJid().asBareJid()
@@ -875,7 +878,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
                         @Override
                         public void onFailure(@NonNull final Throwable throwable) {
-                            failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable);
+                            failureToPerformAction(Jingle.Action.CONTENT_ACCEPT, throwable);
                         }
                     },
                     MoreExecutors.directExecutor());
@@ -887,9 +890,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void sendContentAccept(final RtpContentMap contentAcceptMap) {
-        final JinglePacket jinglePacket =
-                contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId);
-        send(jinglePacket);
+        final Iq iq =
+                contentAcceptMap.toJinglePacket(Jingle.Action.CONTENT_ACCEPT, id.sessionId);
+        send(iq);
     }
 
     public synchronized void rejectContentAdd() {
@@ -903,20 +906,20 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void rejectContentAdd(final RtpContentMap contentMap) {
-        final JinglePacket jinglePacket =
+        final Iq iq =
                 contentMap
                         .toStub()
-                        .toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId);
+                        .toJinglePacket(Jingle.Action.CONTENT_REJECT, id.sessionId);
         Log.d(
                 Config.LOGTAG,
                 id.getAccount().getJid().asBareJid()
                         + ": rejecting content "
                         + ContentAddition.summary(contentMap));
-        send(jinglePacket);
+        send(iq);
     }
 
     private boolean checkForIceRestart(
-            final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
+            final Iq jinglePacket, final RtpContentMap rtpContentMap) {
         final RtpContentMap existing = getRemoteContentMap();
         final Set<IceUdpTransportInfo.Credentials> existingCredentials;
         final IceUdpTransportInfo.Credentials newCredentials;
@@ -995,7 +998,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private boolean applyIceRestart(
-            final JinglePacket jinglePacket,
+            final Iq jinglePacket,
             final RtpContentMap restartContentMap,
             final boolean isOffer)
             throws ExecutionException, InterruptedException {
@@ -1096,7 +1099,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private ListenableFuture<RtpContentMap> receiveRtpContentMap(
-            final JinglePacket jinglePacket, final boolean expectVerification) {
+            final Jingle jinglePacket, final boolean expectVerification) {
         try {
             return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification);
         } catch (final Exception e) {
@@ -1139,12 +1142,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveSessionInitiate(final JinglePacket jinglePacket) {
+    private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) {
         if (isInitiator()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE);
             return;
         }
-        final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
+        final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jingle, false);
         Futures.addCallback(
                 future,
                 new FutureCallback<>() {
@@ -1163,7 +1166,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void receiveSessionInitiate(
-            final JinglePacket jinglePacket, final RtpContentMap contentMap) {
+            final Iq jinglePacket, final RtpContentMap contentMap) {
         try {
             contentMap.requireContentDescriptions();
             contentMap.requireDTLSFingerprint(true);
@@ -1223,13 +1226,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
         }
     }
 
-    private void receiveSessionAccept(final JinglePacket jinglePacket) {
+    private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) {
         if (isResponder()) {
-            receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+            receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT);
             return;
         }
         final ListenableFuture<RtpContentMap> future =
-                receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
+                receiveRtpContentMap(jingle, this.omemoVerification.hasFingerprint());
         Futures.addCallback(
                 future,
                 new FutureCallback<>() {
@@ -1254,7 +1257,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void receiveSessionAccept(
-            final JinglePacket jinglePacket, final RtpContentMap contentMap) {
+            final Iq jinglePacket, final RtpContentMap contentMap) {
         try {
             contentMap.requireContentDescriptions();
             contentMap.requireDTLSFingerprint();
@@ -1399,7 +1402,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void failureToPerformAction(
-            final JinglePacket.Action action, final Throwable throwable) {
+            final Jingle.Action action, final Throwable throwable) {
         if (isTerminated()) {
             return;
         }
@@ -1470,8 +1473,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
             return;
         }
         transitionOrThrow(State.SESSION_ACCEPTED);
-        final JinglePacket sessionAccept =
-                rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
+        final Iq sessionAccept =
+                rtpContentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId);
         send(sessionAccept);
     }
 
@@ -1939,8 +1942,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
             return;
         }
         this.transitionOrThrow(targetState);
-        final JinglePacket sessionInitiate =
-                rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+        final Iq sessionInitiate =
+                rtpContentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId);
         send(sessionInitiate);
     }
 
@@ -2008,9 +2011,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
                             + contentName);
             return;
         }
-        final JinglePacket jinglePacket =
-                transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        send(jinglePacket);
+        final Iq iq =
+                transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        send(iq);
     }
 
     public RtpEndUserState getEndUserState() {
@@ -2364,8 +2367,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private void sendJingleMessage(final String action, final Jid to) {
-        final MessagePacket messagePacket = new MessagePacket();
-        messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those
+        final var messagePacket = new im.conversations.android.xmpp.model.stanza.Message();
+        messagePacket.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); // we want to carbon copy those
         messagePacket.setTo(to);
         final Element intent =
                 messagePacket
@@ -2386,7 +2389,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void sendJingleMessageFinish(final Reason reason) {
         final var account = id.getAccount();
-        final MessagePacket messagePacket =
+        final var messagePacket =
                 xmppConnectionService
                         .getMessageGenerator()
                         .sessionFinish(id.with, id.sessionId, reason);
@@ -2545,34 +2548,34 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void initiateIceRestart(final RtpContentMap rtpContentMap) {
         final RtpContentMap transportInfo = rtpContentMap.transportInfo();
-        final JinglePacket jinglePacket =
-                transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
-        Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
-        jinglePacket.setTo(id.with);
+        final Iq iq =
+                transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
+        Log.d(Config.LOGTAG, "initiating ice restart: " + iq);
+        iq.setTo(id.with);
         xmppConnectionService.sendIqPacket(
                 id.account,
-                jinglePacket,
-                (account, response) -> {
-                    if (response.getType() == IqPacket.TYPE.RESULT) {
+                iq,
+                (response) -> {
+                    if (response.getType() == Iq.Type.RESULT) {
                         Log.d(Config.LOGTAG, "received success to our ice restart");
                         setLocalContentMap(rtpContentMap);
                         webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
                         return;
                     }
-                    if (response.getType() == IqPacket.TYPE.ERROR) {
+                    if (response.getType() == Iq.Type.ERROR) {
                         if (isTieBreak(response)) {
                             Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
                             return;
                         }
                         handleIqErrorResponse(response);
                     }
-                    if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
                         handleIqTimeoutResponse(response);
                     }
                 });
     }
 
-    private boolean isTieBreak(final IqPacket response) {
+    private boolean isTieBreak(final Iq response) {
         final Element error = response.findChild("error");
         return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS);
     }
@@ -2593,7 +2596,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
                     @Override
                     public void onFailure(@NonNull Throwable throwable) {
-                        failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable);
+                        failureToPerformAction(Jingle.Action.CONTENT_ADD, throwable);
                     }
                 },
                 MoreExecutors.directExecutor());
@@ -2601,21 +2604,21 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void sendContentAdd(final RtpContentMap contentAdd) {
 
-        final JinglePacket jinglePacket =
-                contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId);
-        jinglePacket.setTo(id.with);
+        final Iq iq =
+                contentAdd.toJinglePacket(Jingle.Action.CONTENT_ADD, id.sessionId);
+        iq.setTo(id.with);
         xmppConnectionService.sendIqPacket(
                 id.account,
-                jinglePacket,
-                (connection, response) -> {
-                    if (response.getType() == IqPacket.TYPE.RESULT) {
+                iq,
+                (response) -> {
+                    if (response.getType() == Iq.Type.RESULT) {
                         Log.d(
                                 Config.LOGTAG,
                                 id.getAccount().getJid().asBareJid()
                                         + ": received ACK to our content-add");
                         return;
                     }
-                    if (response.getType() == IqPacket.TYPE.ERROR) {
+                    if (response.getType() == Iq.Type.ERROR) {
                         if (isTieBreak(response)) {
                             this.outgoingContentAdd = null;
                             Log.d(Config.LOGTAG, "received tie-break as result of our content-add");
@@ -2623,7 +2626,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                         }
                         handleIqErrorResponse(response);
                     }
-                    if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
                         handleIqTimeoutResponse(response);
                     }
                 });
@@ -2821,13 +2824,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
         if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
-            final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+            final Iq request = new Iq(Iq.Type.GET);
             request.setTo(id.account.getDomain());
             request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
             xmppConnectionService.sendIqPacket(
                     id.account,
                     request,
-                    (account, response) -> {
+                    (response) -> {
                         final var iceServers = IceServers.parse(response);
                         if (iceServers.isEmpty()) {
                             Log.w(

src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java 🔗

@@ -1,9 +1,8 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.PacketReceived;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
-public interface OnJinglePacketReceived extends PacketReceived {
-	void onJinglePacketReceived(Account account, JinglePacket packet);
+public interface OnJinglePacketReceived {
+	void onJinglePacketReceived(Account account, Iq packet);
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java 🔗

@@ -18,9 +18,9 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
 import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
-import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import im.conversations.android.xmpp.model.jingle.Jingle;
 
 import java.util.Collection;
 import java.util.LinkedHashMap;
@@ -38,7 +38,7 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
         super(group, contents);
     }
 
-    public static RtpContentMap of(final JinglePacket jinglePacket) {
+    public static RtpContentMap of(final Jingle jinglePacket) {
         final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
                 of(jinglePacket.getJingleContents());
         if (isOmemoVerified(contents)) {
@@ -52,7 +52,7 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
             Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
         final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
                 contents.values();
-        if (values.size() == 0) {
+        if (values.isEmpty()) {
             return false;
         }
         for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java 🔗

@@ -15,6 +15,7 @@ import com.google.common.primitives.Longs;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.jingle.Jingle;
 
 import java.util.List;
 
@@ -55,15 +56,11 @@ public class FileTransferDescription extends GenericDescription {
         return new File(size, name, mediaType, hashes);
     }
 
-    public static SessionInfo getSessionInfo(@NonNull final JinglePacket jinglePacket) {
-        Preconditions.checkNotNull(jinglePacket);
+    public static SessionInfo getSessionInfo(@NonNull final Jingle jingle) {
+        Preconditions.checkNotNull(jingle);
         Preconditions.checkArgument(
-                jinglePacket.getAction() == JinglePacket.Action.SESSION_INFO,
+                jingle.getAction() == Jingle.Action.SESSION_INFO,
                 "jingle packet is not a session-info");
-        final Element jingle = jinglePacket.findChild("jingle", Namespace.JINGLE);
-        if (jingle == null) {
-            return null;
-        }
         final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
         if (checksum != null) {
             final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java 🔗

@@ -16,7 +16,7 @@ import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -96,7 +96,7 @@ public class InbandBytestreamsTransport implements Transport {
     }
 
     private void openInBandTransport() {
-        final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
+        final var iqPacket = new Iq(Iq.Type.SET);
         iqPacket.setTo(with);
         final var open = iqPacket.addChild("open", Namespace.IBB);
         open.setAttribute("block-size", this.blockSize);
@@ -106,8 +106,8 @@ public class InbandBytestreamsTransport implements Transport {
         xmppConnection.sendIqPacket(iqPacket, this::receiveResponseToOpen);
     }
 
-    private void receiveResponseToOpen(final Account account, final IqPacket response) {
-        if (response.getType() == IqPacket.TYPE.RESULT) {
+    private void receiveResponseToOpen(final Iq response) {
+        if (response.getType() == Iq.Type.RESULT) {
             Log.d(Config.LOGTAG, "ibb open was accepted");
             this.transportCallback.onTransportEstablished();
             this.blockSenderThread.start();
@@ -284,7 +284,7 @@ public class InbandBytestreamsTransport implements Transport {
 
         private void sendIbbBlock(final int sequence, final byte[] block) {
             Log.d(Config.LOGTAG, "sending ibb block #" + sequence + " " + block.length + " bytes");
-            final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
+            final var iqPacket = new Iq(Iq.Type.SET);
             iqPacket.setTo(with);
             final var data = iqPacket.addChild("data", Namespace.IBB);
             data.setAttribute("sid", this.streamId);
@@ -292,8 +292,8 @@ public class InbandBytestreamsTransport implements Transport {
             data.setContent(BaseEncoding.base64().encode(block));
             this.xmppConnection.sendIqPacket(
                     iqPacket,
-                    (a, response) -> {
-                        if (response.getType() != IqPacket.TYPE.RESULT) {
+                    (response) -> {
+                        if (response.getType() != Iq.Type.RESULT) {
                             Log.d(
                                     Config.LOGTAG,
                                     "received iq error in response to data block #" + sequence);

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java 🔗

@@ -32,7 +32,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.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -250,7 +250,7 @@ public class SocksByteStreamsTransport implements Transport {
     private ListenableFuture<String> activateProxy(final Candidate candidate) {
         Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate);
         final SettableFuture<String> iqFuture = SettableFuture.create();
-        final IqPacket proxyActivation = new IqPacket(IqPacket.TYPE.SET);
+        final Iq proxyActivation = new Iq(Iq.Type.SET);
         proxyActivation.setTo(candidate.jid);
         final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS);
         query.setAttribute("sid", this.streamId);
@@ -258,17 +258,18 @@ public class SocksByteStreamsTransport implements Transport {
         activate.setContent(id.with.toEscapedString());
         xmppConnection.sendIqPacket(
                 proxyActivation,
-                (a, response) -> {
-                    if (response.getType() == IqPacket.TYPE.RESULT) {
+                (response) -> {
+                    if (response.getType() == Iq.Type.RESULT) {
                         Log.d(Config.LOGTAG, "our proxy has been activated");
                         transportCallback.onProxyActivated(this.streamId, candidate);
                         iqFuture.set(candidate.cid);
-                    } else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    } else if (response.getType() == Iq.Type.TIMEOUT) {
                         iqFuture.setException(new TimeoutException());
                     } else {
+                        final var account = id.account;
                         Log.d(
                                 Config.LOGTAG,
-                                a.getJid().asBareJid()
+                                account.getJid().asBareJid()
                                         + ": failed to activate proxy on "
                                         + candidate.jid);
                         iqFuture.setException(new IllegalStateException("Proxy activation failed"));
@@ -314,14 +315,14 @@ public class SocksByteStreamsTransport implements Transport {
             return Futures.immediateFailedFuture(
                     new IllegalStateException("No proxy/streamer found"));
         }
-        final IqPacket iqRequest = new IqPacket(IqPacket.TYPE.GET);
+        final Iq iqRequest = new Iq(Iq.Type.GET);
         iqRequest.setTo(streamer);
         iqRequest.query(Namespace.BYTE_STREAMS);
         final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
         xmppConnection.sendIqPacket(
                 iqRequest,
-                (a, response) -> {
-                    if (response.getType() == IqPacket.TYPE.RESULT) {
+                (response) -> {
+                    if (response.getType() == Iq.Type.RESULT) {
                         final Element query = response.findChild("query", Namespace.BYTE_STREAMS);
                         final Element streamHost =
                                 query == null
@@ -349,7 +350,7 @@ public class SocksByteStreamsTransport implements Transport {
                                         655360 + (initiator ? 0 : 15),
                                         CandidateType.PROXY));
 
-                    } else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    } else if (response.getType() == Iq.Type.TIMEOUT) {
                         candidateFuture.setException(new TimeoutException());
                     } else {
                         candidateFuture.setException(

src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java 🔗

@@ -22,7 +22,7 @@ import eu.siacs.conversations.xmpp.jingle.IceServers;
 import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import org.webrtc.CandidatePairChangeEvent;
 import org.webrtc.DataChannel;
@@ -234,14 +234,14 @@ public class WebRTCDataChannelTransport implements Transport {
         if (xmppConnection.getFeatures().externalServiceDiscovery()) {
             final SettableFuture<List<PeerConnection.IceServer>> iceServerFuture =
                     SettableFuture.create();
-            final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+            final Iq request = new Iq(Iq.Type.GET);
             request.setTo(this.account.getDomain());
             request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
             xmppConnection.sendIqPacket(
                     request,
-                    (account, response) -> {
+                    (response) -> {
                         final var iceServers = IceServers.parse(response);
-                        if (iceServers.size() == 0) {
+                        if (iceServers.isEmpty()) {
                             Log.w(
                                     Config.LOGTAG,
                                     account.getJid().asBareJid()

src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java 🔗

@@ -4,7 +4,7 @@ import android.os.Bundle;
 
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class PublishOptions {
 
@@ -37,8 +37,8 @@ public class PublishOptions {
         return options;
     }
 
-    public static boolean preconditionNotMet(IqPacket response) {
-        final Element error = response.getType() == IqPacket.TYPE.ERROR ? response.findChild("error") : null;
+    public static boolean preconditionNotMet(Iq response) {
+        final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
         return error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR);
     }
 

src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java 🔗

@@ -1,42 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas;
-
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.InvalidJid;
-
-abstract public class AbstractAcknowledgeableStanza extends AbstractStanza {
-
-    protected AbstractAcknowledgeableStanza(String name) {
-        super(name);
-    }
-
-
-    public String getId() {
-        return this.getAttribute("id");
-    }
-
-    public void setId(final String id) {
-        setAttribute("id", id);
-    }
-
-    private Element getErrorConditionElement() {
-        final Element error = findChild("error");
-        if (error == null) {
-            return null;
-        }
-        for (final Element element : error.getChildren()) {
-            if (!element.getName().equals("text")) {
-                return element;
-            }
-        }
-        return null;
-    }
-
-    public String getErrorCondition() {
-        final Element condition = getErrorConditionElement();
-        return condition == null ? null : condition.getName();
-    }
-
-    public boolean valid() {
-        return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo());
-    }
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java 🔗

@@ -1,53 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas;
-
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.Jid;
-
-public class AbstractStanza extends Element {
-
-	protected AbstractStanza(final String name) {
-		super(name);
-	}
-
-	public Jid getTo() {
-		return getAttributeAsJid("to");
-	}
-
-	public Jid getFrom() {
-		return getAttributeAsJid("from");
-	}
-
-	public void setTo(final Jid to) {
-		if (to != null) {
-			setAttribute("to", to);
-		}
-	}
-
-	public void setFrom(final Jid from) {
-		if (from != null) {
-			setAttribute("from", from);
-		}
-	}
-
-	public boolean fromServer(final Account account) {
-		final Jid from = getFrom();
-		return from == null
-			|| from.equals(account.getDomain())
-			|| from.equals(account.getJid().asBareJid())
-			|| from.equals(account.getJid());
-	}
-
-	public boolean toServer(final Account account) {
-		final Jid to = getTo();
-		return to == null
-			|| to.equals(account.getDomain())
-			|| to.equals(account.getJid().asBareJid())
-			|| to.equals(account.getJid());
-	}
-
-	public boolean fromAccount(final Account account) {
-		final Jid from = getFrom();
-		return from != null && from.asBareJid().equals(account.getJid().asBareJid());
-	}
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java 🔗

@@ -1,75 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas;
-
-import eu.siacs.conversations.xml.Element;
-
-public class IqPacket extends AbstractAcknowledgeableStanza {
-
-	public enum TYPE {
-		ERROR,
-		SET,
-		RESULT,
-		GET,
-		INVALID,
-		TIMEOUT
-	}
-
-	public IqPacket(final TYPE type) {
-		super("iq");
-		if (type != TYPE.INVALID) {
-			this.setAttribute("type", type.toString().toLowerCase());
-		}
-	}
-
-	public IqPacket() {
-		super("iq");
-	}
-
-	public Element query() {
-		Element query = findChild("query");
-		if (query == null) {
-			query = addChild("query");
-		}
-		return query;
-	}
-
-	public Element query(final String xmlns) {
-		final Element query = query();
-		query.setAttribute("xmlns", xmlns);
-		return query();
-	}
-
-	public TYPE getType() {
-		final String type = getAttribute("type");
-		if (type == null) {
-			return TYPE.INVALID;
-		}
-		switch (type) {
-			case "error":
-				return TYPE.ERROR;
-			case "result":
-				return TYPE.RESULT;
-			case "set":
-				return TYPE.SET;
-			case "get":
-				return TYPE.GET;
-			case "timeout":
-				return TYPE.TIMEOUT;
-			default:
-				return TYPE.INVALID;
-		}
-	}
-
-	public IqPacket generateResponse(final TYPE type) {
-		final IqPacket packet = new IqPacket(type);
-		packet.setTo(this.getFrom());
-		packet.setId(this.getId());
-		return packet;
-	}
-
-	@Override
-	public boolean valid() {
-		String id = getId();
-		return id != null && super.valid();
-	}
-
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java 🔗

@@ -1,100 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas;
-
-import android.util.Pair;
-
-import eu.siacs.conversations.parser.AbstractParser;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.LocalizedContent;
-
-public class MessagePacket extends AbstractAcknowledgeableStanza {
-	public static final int TYPE_CHAT = 0;
-	public static final int TYPE_NORMAL = 2;
-	public static final int TYPE_GROUPCHAT = 3;
-	public static final int TYPE_ERROR = 4;
-	public static final int TYPE_HEADLINE = 5;
-
-	public MessagePacket() {
-		super("message");
-	}
-
-	public LocalizedContent getBody() {
-		return findInternationalizedChildContentInDefaultNamespace("body");
-	}
-
-	public void setBody(String text) {
-		this.children.remove(findChild("body"));
-		Element body = new Element("body");
-		body.setContent(text);
-		this.children.add(0, body);
-	}
-
-	public void setAxolotlMessage(Element axolotlMessage) {
-		this.children.remove(findChild("body"));
-		this.children.add(0, axolotlMessage);
-	}
-
-	public void setType(int type) {
-		switch (type) {
-		case TYPE_CHAT:
-			this.setAttribute("type", "chat");
-			break;
-		case TYPE_GROUPCHAT:
-			this.setAttribute("type", "groupchat");
-			break;
-		case TYPE_NORMAL:
-			break;
-		case TYPE_ERROR:
-			this.setAttribute("type","error");
-			break;
-		default:
-			this.setAttribute("type", "chat");
-			break;
-		}
-	}
-
-	public int getType() {
-		String type = getAttribute("type");
-		if (type == null) {
-			return TYPE_NORMAL;
-		} else if (type.equals("normal")) {
-			return TYPE_NORMAL;
-		} else if (type.equals("chat")) {
-			return TYPE_CHAT;
-		} else if (type.equals("groupchat")) {
-			return TYPE_GROUPCHAT;
-		} else if (type.equals("error")) {
-			return TYPE_ERROR;
-		} else if (type.equals("headline")) {
-			return TYPE_HEADLINE;
-		} else {
-			return TYPE_NORMAL;
-		}
-	}
-
-	public Pair<MessagePacket,Long> getForwardedMessagePacket(String name, String namespace) {
-		Element wrapper = findChild(name, namespace);
-		if (wrapper == null) {
-			return null;
-		}
-		Element forwarded = wrapper.findChild("forwarded", "urn:xmpp:forward:0");
-		if (forwarded == null) {
-			return null;
-		}
-		MessagePacket packet = create(forwarded.findChild("message"));
-		if (packet == null) {
-			return null;
-		}
-		Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
-		return new Pair(packet,timestamp);
-	}
-
-	public static MessagePacket create(Element element) {
-		if (element == null) {
-			return null;
-		}
-		MessagePacket packet = new MessagePacket();
-		packet.setAttributes(element.getAttributes());
-		packet.setChildren(element.getChildren());
-		return packet;
-	}
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java 🔗

@@ -1,11 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.csi;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class ActivePacket extends AbstractStanza {
-	public ActivePacket() {
-		super("active");
-		setAttribute("xmlns", Namespace.CSI);
-	}
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java 🔗

@@ -1,11 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.csi;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class InactivePacket extends AbstractStanza {
-	public InactivePacket() {
-		super("inactive");
-		setAttribute("xmlns", Namespace.CSI);
-	}
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java 🔗

@@ -1,14 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.streammgmt;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class AckPacket extends AbstractStanza {
-
-	public AckPacket(final int sequence) {
-		super("a");
-		this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT);
-		this.setAttribute("h", Integer.toString(sequence));
-	}
-
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java 🔗

@@ -1,14 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.streammgmt;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class EnablePacket extends AbstractStanza {
-
-	public EnablePacket() {
-		super("enable");
-		this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT);
-		this.setAttribute("resume", "true");
-	}
-
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java 🔗

@@ -1,13 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.streammgmt;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class RequestPacket extends AbstractStanza {
-
-	public RequestPacket() {
-		super("r");
-		this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT);
-	}
-
-}

src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java 🔗

@@ -1,15 +0,0 @@
-package eu.siacs.conversations.xmpp.stanzas.streammgmt;
-
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
-
-public class ResumePacket extends AbstractStanza {
-
-	public ResumePacket(final String id, final int sequence) {
-		super("resume");
-		this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT);
-		this.setAttribute("previd", id);
-		this.setAttribute("h", Integer.toString(sequence));
-	}
-
-}

src/main/java/im/conversations/android/xmpp/Entity.java 🔗

@@ -0,0 +1,34 @@
+package im.conversations.android.xmpp;
+
+import org.jxmpp.jid.Jid;
+
+public abstract class Entity {
+
+    public final Jid address;
+
+    private Entity(final Jid address) {
+        this.address = address;
+    }
+
+    public static class DiscoItem extends Entity {
+
+        private DiscoItem(Jid address) {
+            super(address);
+        }
+    }
+
+    public static class Presence extends Entity {
+
+        private Presence(Jid address) {
+            super(address);
+        }
+    }
+
+    public static Presence presence(final Jid address) {
+        return new Presence(address);
+    }
+
+    public static DiscoItem discoItem(final Jid address) {
+        return new DiscoItem(address);
+    }
+}

src/main/java/im/conversations/android/xmpp/EntityCapabilities.java 🔗

@@ -0,0 +1,133 @@
+package im.conversations.android.xmpp;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.data.Field;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.Identity;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+public final class EntityCapabilities {
+    public static EntityCapsHash hash(final InfoQuery info) {
+        final StringBuilder s = new StringBuilder();
+        final List<Identity> orderedIdentities =
+                Ordering.from(
+                                (Comparator<Identity>)
+                                        (a, b) ->
+                                                ComparisonChain.start()
+                                                        .compare(
+                                                                blankNull(a.getCategory()),
+                                                                blankNull(b.getCategory()))
+                                                        .compare(
+                                                                blankNull(a.getType()),
+                                                                blankNull(b.getType()))
+                                                        .compare(
+                                                                blankNull(a.getLang()),
+                                                                blankNull(b.getLang()))
+                                                        .compare(
+                                                                blankNull(a.getIdentityName()),
+                                                                blankNull(b.getIdentityName()))
+                                                        .result())
+                        .sortedCopy(info.getIdentities());
+
+        for (final Identity id : orderedIdentities) {
+            s.append(blankNull(id.getCategory()))
+                    .append("/")
+                    .append(blankNull(id.getType()))
+                    .append("/")
+                    .append(blankNull(id.getLang()))
+                    .append("/")
+                    .append(blankNull(id.getIdentityName()))
+                    .append("<");
+        }
+
+        final List<String> features =
+                Ordering.natural()
+                        .sortedCopy(Collections2.transform(info.getFeatures(), Feature::getVar));
+        for (final String feature : features) {
+            s.append(clean(feature)).append("<");
+        }
+
+        final List<Data> extensions =
+                Ordering.from(Comparator.comparing(Data::getFormType))
+                        .sortedCopy(info.getExtensions(Data.class));
+
+        for (final Data extension : extensions) {
+            s.append(clean(extension.getFormType())).append("<");
+            final List<Field> fields =
+                    Ordering.from(
+                                    Comparator.comparing(
+                                            (Field lhs) -> Strings.nullToEmpty(lhs.getFieldName())))
+                            .sortedCopy(extension.getFields());
+            for (final Field field : fields) {
+                s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
+                final List<String> values = Ordering.natural().sortedCopy(field.getValues());
+                for (final String value : values) {
+                    s.append(blankNull(value)).append("<");
+                }
+            }
+        }
+        return new EntityCapsHash(
+                Hashing.sha1().hashString(s.toString(), StandardCharsets.UTF_8).asBytes());
+    }
+
+    private static String clean(String s) {
+        return s.replace("<", "&lt;");
+    }
+
+    private static String blankNull(String s) {
+        return s == null ? "" : clean(s);
+    }
+
+    public abstract static class Hash {
+        public final byte[] hash;
+
+        protected Hash(byte[] hash) {
+            this.hash = hash;
+        }
+
+        public String encoded() {
+            return BaseEncoding.base64().encode(hash);
+        }
+
+        public abstract String capabilityNode(final String node);
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Hash hash1 = (Hash) o;
+            return Arrays.equals(hash, hash1.hash);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(hash);
+        }
+    }
+
+    public static class EntityCapsHash extends Hash {
+
+        protected EntityCapsHash(byte[] hash) {
+            super(hash);
+        }
+
+        @Override
+        public String capabilityNode(String node) {
+            return String.format("%s#%s", node, encoded());
+        }
+
+        public static EntityCapsHash of(final String encoded) {
+            return new EntityCapsHash(BaseEncoding.base64().decode(encoded));
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java 🔗

@@ -0,0 +1,185 @@
+package im.conversations.android.xmpp;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Ordering;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Bytes;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.Hash;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.data.Field;
+import im.conversations.android.xmpp.model.data.Value;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.Identity;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Objects;
+
+public class EntityCapabilities2 {
+
+    private static final char UNIT_SEPARATOR = 0x1f;
+    private static final char RECORD_SEPARATOR = 0x1e;
+
+    private static final char GROUP_SEPARATOR = 0x1d;
+
+    private static final char FILE_SEPARATOR = 0x1c;
+
+    public static EntityCaps2Hash hash(final InfoQuery info) {
+        return hash(Hash.Algorithm.SHA_256, info);
+    }
+
+    public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) {
+        final String result = algorithm(info);
+        final var hashFunction = toHashFunction(algorithm);
+        return new EntityCaps2Hash(
+                algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes());
+    }
+
+    private static HashFunction toHashFunction(final Hash.Algorithm algorithm) {
+        switch (algorithm) {
+            case SHA_1:
+                return Hashing.sha1();
+            case SHA_256:
+                return Hashing.sha256();
+            case SHA_512:
+                return Hashing.sha512();
+            default:
+                throw new IllegalArgumentException("Unknown hash algorithm");
+        }
+    }
+
+    private static String asHex(final String message) {
+        return Joiner.on(' ')
+                .join(
+                        Collections2.transform(
+                                Bytes.asList(message.getBytes(StandardCharsets.UTF_8)),
+                                b -> String.format("%02x", b)));
+    }
+
+    private static String algorithm(final InfoQuery infoQuery) {
+        return features(infoQuery.getFeatures())
+                + identities(infoQuery.getIdentities())
+                + extensions(infoQuery.getExtensions(Data.class));
+    }
+
+    private static String identities(final Collection<Identity> identities) {
+        return Joiner.on("")
+                        .join(
+                                Ordering.natural()
+                                        .sortedCopy(
+                                                Collections2.transform(
+                                                        identities, EntityCapabilities2::identity)))
+                + FILE_SEPARATOR;
+    }
+
+    private static String identity(final Identity identity) {
+        return Strings.nullToEmpty(identity.getCategory())
+                + UNIT_SEPARATOR
+                + Strings.nullToEmpty(identity.getType())
+                + UNIT_SEPARATOR
+                + Strings.nullToEmpty(identity.getLang())
+                + UNIT_SEPARATOR
+                + Strings.nullToEmpty(identity.getIdentityName())
+                + UNIT_SEPARATOR
+                + RECORD_SEPARATOR;
+    }
+
+    private static String features(Collection<Feature> features) {
+        return Joiner.on("")
+                        .join(
+                                Ordering.natural()
+                                        .sortedCopy(
+                                                Collections2.transform(
+                                                        features, EntityCapabilities2::feature)))
+                + FILE_SEPARATOR;
+    }
+
+    private static String feature(final Feature feature) {
+        return Strings.nullToEmpty(feature.getVar()) + UNIT_SEPARATOR;
+    }
+
+    private static String value(final Value value) {
+        return Strings.nullToEmpty(value.getContent()) + UNIT_SEPARATOR;
+    }
+
+    private static String values(final Collection<Value> values) {
+        return Joiner.on("")
+                .join(
+                        Ordering.natural()
+                                .sortedCopy(
+                                        Collections2.transform(
+                                                values, EntityCapabilities2::value)));
+    }
+
+    private static String field(final Field field) {
+        return Strings.nullToEmpty(field.getFieldName())
+                + UNIT_SEPARATOR
+                + values(field.getExtensions(Value.class))
+                + RECORD_SEPARATOR;
+    }
+
+    private static String fields(final Collection<Field> fields) {
+        return Joiner.on("")
+                        .join(
+                                Ordering.natural()
+                                        .sortedCopy(
+                                                Collections2.transform(
+                                                        fields, EntityCapabilities2::field)))
+                + GROUP_SEPARATOR;
+    }
+
+    private static String extension(final Data data) {
+        return fields(data.getExtensions(Field.class));
+    }
+
+    private static String extensions(final Collection<Data> extensions) {
+        return Joiner.on("")
+                        .join(
+                                Ordering.natural()
+                                        .sortedCopy(
+                                                Collections2.transform(
+                                                        extensions,
+                                                        EntityCapabilities2::extension)))
+                + FILE_SEPARATOR;
+    }
+
+    public static class EntityCaps2Hash extends EntityCapabilities.Hash {
+
+        public final Hash.Algorithm algorithm;
+
+        protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) {
+            super(hash);
+            this.algorithm = algorithm;
+        }
+
+        public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) {
+            return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded));
+        }
+
+        @Override
+        public String capabilityNode(String node) {
+            return String.format(
+                    "%s#%s.%s", Namespace.ENTITY_CAPABILITIES_2, algorithm.toString(), encoded());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            if (!super.equals(o)) return false;
+            EntityCaps2Hash that = (EntityCaps2Hash) o;
+            return algorithm == that.algorithm;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(super.hashCode(), algorithm);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/ExtensionFactory.java 🔗

@@ -0,0 +1,78 @@
+package im.conversations.android.xmpp;
+
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+
+import eu.siacs.conversations.xml.Element;
+
+import im.conversations.android.xmpp.model.Extension;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+public final class ExtensionFactory {
+
+    public static Element create(final String name, final String namespace) {
+        final Class<? extends Extension> clazz = of(name, namespace);
+        if (clazz == null) {
+            return new Element(name, namespace);
+        }
+        final Constructor<? extends Element> constructor;
+        try {
+            constructor = clazz.getDeclaredConstructor();
+        } catch (final NoSuchMethodException e) {
+            throw new IllegalStateException(
+                    String.format("%s has no default constructor", clazz.getName()),e);
+        }
+        try {
+            return constructor.newInstance();
+        } catch (final IllegalAccessException
+                | InstantiationException
+                | InvocationTargetException e) {
+            throw new IllegalStateException(
+                    String.format("%s has inaccessible default constructor", clazz.getName()),e);
+        }
+    }
+
+    private static Class<? extends Extension> of(final String name, final String namespace) {
+        return Extensions.EXTENSION_CLASS_MAP.get(new Id(name, namespace));
+    }
+
+    public static Id id(final Class<? extends Extension> clazz) {
+        return Extensions.EXTENSION_CLASS_MAP.inverse().get(clazz);
+    }
+
+    private ExtensionFactory() {}
+
+    public static class Id {
+        public final String name;
+        public final String namespace;
+
+        public Id(String name, String namespace) {
+            this.name = name;
+            this.namespace = namespace;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Id id = (Id) o;
+            return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(name, namespace);
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("name", name)
+                    .add("namespace", namespace)
+                    .toString();
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/NodeConfiguration.java 🔗

@@ -0,0 +1,112 @@
+package im.conversations.android.xmpp;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class NodeConfiguration implements Map<String, Object> {
+
+    private static final String PERSIST_ITEMS = "pubsub#persist_items";
+    private static final String ACCESS_MODEL = "pubsub#access_model";
+    private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item";
+    private static final String MAX_ITEMS = "pubsub#max_items";
+    private static final String NOTIFY_DELETE = "pubsub#notify_delete";
+    private static final String NOTIFY_RETRACT = "pubsub#notify_retract";
+
+    public static final NodeConfiguration OPEN =
+            new NodeConfiguration(
+                    new ImmutableMap.Builder<String, Object>()
+                            .put(PERSIST_ITEMS, Boolean.TRUE)
+                            .put(ACCESS_MODEL, "open")
+                            .build());
+    public static final NodeConfiguration PRESENCE =
+            new NodeConfiguration(
+                    new ImmutableMap.Builder<String, Object>()
+                            .put(PERSIST_ITEMS, Boolean.TRUE)
+                            .put(ACCESS_MODEL, "presence")
+                            .build());
+    public static final NodeConfiguration WHITELIST_MAX_ITEMS =
+            new NodeConfiguration(
+                    new ImmutableMap.Builder<String, Object>()
+                            .put(PERSIST_ITEMS, Boolean.TRUE)
+                            .put(ACCESS_MODEL, "whitelist")
+                            .put(SEND_LAST_PUBLISHED_ITEM, "never")
+                            .put(MAX_ITEMS, "max")
+                            .put(NOTIFY_DELETE, Boolean.TRUE)
+                            .put(NOTIFY_RETRACT, Boolean.TRUE)
+                            .build());
+    private final Map<String, Object> delegate;
+
+    private NodeConfiguration(Map<String, Object> map) {
+        this.delegate = map;
+    }
+
+    @Override
+    public int size() {
+        return this.delegate.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.delegate.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(@Nullable Object o) {
+        return this.delegate.containsKey(o);
+    }
+
+    @Override
+    public boolean containsValue(@Nullable Object o) {
+        return this.delegate.containsValue(o);
+    }
+
+    @Nullable
+    @Override
+    public Object get(@Nullable Object o) {
+        return this.delegate.get(o);
+    }
+
+    @Nullable
+    @Override
+    public Object put(String s, Object o) {
+        return this.delegate.put(s, o);
+    }
+
+    @Nullable
+    @Override
+    public Object remove(@Nullable Object o) {
+        return this.delegate.remove(o);
+    }
+
+    @Override
+    public void putAll(@NonNull Map<? extends String, ?> map) {
+        this.delegate.putAll(map);
+    }
+
+    @Override
+    public void clear() {
+        this.delegate.clear();
+    }
+
+    @NonNull
+    @Override
+    public Set<String> keySet() {
+        return this.delegate.keySet();
+    }
+
+    @NonNull
+    @Override
+    public Collection<Object> values() {
+        return this.delegate.values();
+    }
+
+    @NonNull
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        return this.delegate.entrySet();
+    }
+}

src/main/java/im/conversations/android/xmpp/Page.java 🔗

@@ -0,0 +1,31 @@
+package im.conversations.android.xmpp;
+
+import androidx.annotation.NonNull;
+import com.google.common.base.MoreObjects;
+
+public class Page {
+
+    public final String first;
+    public final String last;
+    public final Integer count;
+
+    public Page(String first, String last, Integer count) {
+        this.first = first;
+        this.last = last;
+        this.count = count;
+    }
+
+    public static Page emptyWithCount(final String id, final Integer count) {
+        return new Page(id, id, count);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("first", first)
+                .add("last", last)
+                .add("count", count)
+                .toString();
+    }
+}

src/main/java/im/conversations/android/xmpp/Range.java 🔗

@@ -0,0 +1,40 @@
+package im.conversations.android.xmpp;
+
+import androidx.annotation.NonNull;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+
+public class Range {
+
+    public final Order order;
+    public final String id;
+
+    public Range(final Order order, final String id) {
+        this.order = order;
+        this.id = id;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Range range = (Range) o;
+        return order == range.order && Objects.equal(id, range.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(order, id);
+    }
+
+    public enum Order {
+        NORMAL,
+        REVERSE
+    }
+}

src/main/java/im/conversations/android/xmpp/Timestamps.java 🔗

@@ -0,0 +1,44 @@
+package im.conversations.android.xmpp;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public final class Timestamps {
+
+    private Timestamps() {
+        throw new IllegalStateException("Do not instantiate me");
+    }
+
+    public static long parse(final String input) throws ParseException {
+        if (input == null) {
+            throw new IllegalArgumentException("timestamp should not be null");
+        }
+        final String timestamp = input.replace("Z", "+0000");
+        final SimpleDateFormat simpleDateFormat =
+                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
+        final long milliseconds = getMilliseconds(timestamp);
+        final String formatted =
+                timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5);
+        final Date date = simpleDateFormat.parse(formatted);
+        if (date == null) {
+            throw new IllegalArgumentException("Date was null");
+        }
+        return date.getTime() + milliseconds;
+    }
+
+    private static long getMilliseconds(final String timestamp) {
+        if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') {
+            final String millis = timestamp.substring(19, timestamp.length() - 5);
+            try {
+                double fractions = Double.parseDouble("0" + millis);
+                return Math.round(1000 * fractions);
+            } catch (final NumberFormatException e) {
+                return 0;
+            }
+        } else {
+            return 0;
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model;
+
+import java.util.Collection;
+
+public abstract class AuthenticationStreamFeature extends StreamFeature{
+
+    public AuthenticationStreamFeature(final Class<? extends AuthenticationStreamFeature> clazz) {
+        super(clazz);
+    }
+
+    public abstract Collection<String> getMechanismNames();
+}

src/main/java/im/conversations/android/xmpp/model/ByteContent.java 🔗

@@ -0,0 +1,33 @@
+package im.conversations.android.xmpp.model;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.io.BaseEncoding;
+
+import eu.siacs.conversations.xml.Element;
+
+public interface ByteContent {
+
+    String getContent();
+
+    default byte[] asBytes() {
+        final var content = this.getContent();
+        if (Strings.isNullOrEmpty(content)) {
+            throw new IllegalStateException(
+                    String.format("%s element is lacking content", getClass().getName()));
+        }
+        final var contentCleaned = CharMatcher.whitespace().removeFrom(content);
+        if (BaseEncoding.base64().canDecode(contentCleaned)) {
+            return BaseEncoding.base64().decode(contentCleaned);
+        } else {
+            throw new IllegalStateException(
+                    String.format("%s element contains invalid base64", getClass().getName()));
+        }
+    }
+
+    default void setContent(final byte[] bytes) {
+        setContent(BaseEncoding.base64().encode(bytes));
+    }
+
+    Element setContent(final String content);
+}

src/main/java/im/conversations/android/xmpp/model/Extension.java 🔗

@@ -0,0 +1,62 @@
+package im.conversations.android.xmpp.model;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+
+import eu.siacs.conversations.xml.Element;
+
+import im.conversations.android.xmpp.ExtensionFactory;
+
+import java.util.Collection;
+
+public class Extension extends Element {
+
+    private Extension(final ExtensionFactory.Id id) {
+        super(id.name, id.namespace);
+    }
+
+    public Extension(final Class<? extends Extension> clazz) {
+        this(
+                Preconditions.checkNotNull(
+                        ExtensionFactory.id(clazz),
+                        String.format(
+                                "%s does not seem to be annotated with @XmlElement",
+                                clazz.getName())));
+        Preconditions.checkArgument(
+                getClass().equals(clazz), "clazz passed in constructor must match class");
+    }
+
+    public <E extends Extension> boolean hasExtension(final Class<E> clazz) {
+        return Iterables.any(this.children, clazz::isInstance);
+    }
+
+    public <E extends Extension> E getExtension(final Class<E> clazz) {
+        final var extension = Iterables.find(this.children, clazz::isInstance, null);
+        if (extension == null) {
+            return null;
+        }
+        return clazz.cast(extension);
+    }
+
+    public <E extends Extension> Collection<E> getExtensions(final Class<E> clazz) {
+        return Collections2.transform(
+                Collections2.filter(this.children, clazz::isInstance), clazz::cast);
+    }
+
+    public Collection<ExtensionFactory.Id> getExtensionIds() {
+        return Collections2.transform(
+                this.children, c -> new ExtensionFactory.Id(c.getName(), c.getNamespace()));
+    }
+
+    public <T extends Extension> T addExtension(T child) {
+        this.addChild(child);
+        return child;
+    }
+
+    public void addExtensions(final Collection<? extends Extension> extensions) {
+        for (final Extension extension : extensions) {
+            addExtension(extension);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/Hash.java 🔗

@@ -0,0 +1,46 @@
+package im.conversations.android.xmpp.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement(namespace = Namespace.HASHES)
+public class Hash extends Extension {
+    public Hash() {
+        super(Hash.class);
+    }
+
+    public Algorithm getAlgorithm() {
+        return Algorithm.tryParse(this.getAttribute("algo"));
+    }
+
+    public void setAlgorithm(final Algorithm algorithm) {
+        this.setAttribute("algo", algorithm.toString());
+    }
+
+    public enum Algorithm {
+        SHA_1,
+        SHA_256,
+        SHA_512;
+
+        public static Algorithm tryParse(@Nullable final String name) {
+            try {
+                return valueOf(
+                        CaseFormat.LOWER_HYPHEN.to(
+                                CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name)));
+            } catch (final IllegalArgumentException e) {
+                return null;
+            }
+        }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString());
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/avatar/Data.java 🔗

@@ -0,0 +1,14 @@
+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.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.AVATAR_DATA)
+public class Data extends Extension implements ByteContent {
+
+    public Data() {
+        super(Data.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/avatar/Info.java 🔗

@@ -0,0 +1,37 @@
+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;
+
+@XmlElement(namespace = Namespace.AVATAR_METADATA)
+public class Info extends Extension {
+
+    public Info() {
+        super(Info.class);
+    }
+
+    public long getHeight() {
+        return this.getLongAttribute("height");
+    }
+
+    public long getWidth() {
+        return this.getLongAttribute("width");
+    }
+
+    public long getBytes() {
+        return this.getLongAttribute("bytes");
+    }
+
+    public String getType() {
+        return this.getAttribute("type");
+    }
+
+    public String getUrl() {
+        return this.getAttribute("url");
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java 🔗

@@ -0,0 +1,13 @@
+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;
+
+@XmlElement(namespace = Namespace.AVATAR_METADATA)
+public class Metadata extends Extension {
+
+    public Metadata() {
+        super(Metadata.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java 🔗

@@ -0,0 +1,60 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+
+@XmlElement
+public class Bundle extends Extension {
+
+    public Bundle() {
+        super(Bundle.class);
+    }
+
+    public SignedPreKey getSignedPreKey() {
+        return this.getExtension(SignedPreKey.class);
+    }
+
+    public SignedPreKeySignature getSignedPreKeySignature() {
+        return this.getExtension(SignedPreKeySignature.class);
+    }
+
+    public IdentityKey getIdentityKey() {
+        return this.getExtension(IdentityKey.class);
+    }
+
+    public PreKey getRandomPreKey() {
+        final var preKeys = this.getExtension(PreKeys.class);
+        final Collection<PreKey> preKeyList =
+                preKeys == null ? Collections.emptyList() : preKeys.getExtensions(PreKey.class);
+        return Iterables.get(preKeyList, (int) (preKeyList.size() * Math.random()), null);
+    }
+
+    public void setIdentityKey(final ECPublicKey ecPublicKey) {
+        final var identityKey = this.addExtension(new IdentityKey());
+        identityKey.setContent(ecPublicKey);
+    }
+
+    public void setSignedPreKey(
+            final int id, final ECPublicKey ecPublicKey, final byte[] signature) {
+        final var signedPreKey = this.addExtension(new SignedPreKey());
+        signedPreKey.setId(id);
+        signedPreKey.setContent(ecPublicKey);
+        final var signedPreKeySignature = this.addExtension(new SignedPreKeySignature());
+        signedPreKeySignature.setContent(signature);
+    }
+
+    public void addPreKeys(final List<PreKeyRecord> preKeyRecords) {
+        final var preKeys = this.addExtension(new PreKeys());
+        for (final PreKeyRecord preKeyRecord : preKeyRecords) {
+            final var preKey = preKeys.addExtension(new PreKey());
+            preKey.setId(preKeyRecord.getId());
+            preKey.setContent(preKeyRecord.getKeyPair().getPublicKey());
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java 🔗

@@ -0,0 +1,22 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Device extends Extension {
+
+    public Device() {
+        super(Device.class);
+    }
+
+    public Integer getDeviceId() {
+        return Ints.tryParse(Strings.nullToEmpty(this.getAttribute("id")));
+    }
+
+    public void setDeviceId(int deviceId) {
+        this.setAttribute("id", deviceId);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java 🔗

@@ -0,0 +1,35 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+
+@XmlElement(name = "list")
+public class DeviceList extends Extension {
+
+    public DeviceList() {
+        super(DeviceList.class);
+    }
+
+    public Collection<Device> getDevices() {
+        return this.getExtensions(Device.class);
+    }
+
+    public Set<Integer> getDeviceIds() {
+        return ImmutableSet.copyOf(
+                Collections2.filter(
+                        Collections2.transform(getDevices(), Device::getDeviceId),
+                        Objects::nonNull));
+    }
+
+    public void setDeviceIds(Collection<Integer> deviceIds) {
+        for (final Integer deviceId : deviceIds) {
+            final var device = this.addExtension(new Device());
+            device.setDeviceId(deviceId);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java 🔗

@@ -0,0 +1,23 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.xmpp.model.ByteContent;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.ecc.Curve;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
+
+public interface ECPublicKeyContent extends ByteContent {
+
+    default ECPublicKey asECPublicKey() {
+        try {
+            return Curve.decodePoint(asBytes(), 0);
+        } catch (InvalidKeyException e) {
+            throw new IllegalStateException(
+                    String.format("%s does not contain a valid ECPublicKey", getClass().getName()),
+                    e);
+        }
+    }
+
+    default void setContent(final ECPublicKey ecPublicKey) {
+        setContent(ecPublicKey.serialize());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java 🔗

@@ -0,0 +1,24 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Encrypted extends Extension {
+
+    public Encrypted() {
+        super(Encrypted.class);
+    }
+
+    public boolean hasPayload() {
+        return hasExtension(Payload.class);
+    }
+
+    public Header getHeader() {
+        return getExtension(Header.class);
+    }
+
+    public Payload getPayload() {
+        return getExtension(Payload.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java 🔗

@@ -0,0 +1,45 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Header extends Extension {
+
+    public Header() {
+        super(Header.class);
+    }
+
+    public void addIv(byte[] iv) {
+        this.addExtension(new IV()).setContent(iv);
+    }
+
+    public void setSourceDevice(long sourceDeviceId) {
+        this.setAttribute("sid", sourceDeviceId);
+    }
+
+    public Optional<Integer> getSourceDevice() {
+        return getOptionalIntAttribute("sid");
+    }
+
+    public Collection<Key> getKeys() {
+        return this.getExtensions(Key.class);
+    }
+
+    public Key getKey(final int deviceId) {
+        return Iterables.find(
+                getKeys(), key -> Objects.equals(key.getRemoteDeviceId(), deviceId), null);
+    }
+
+    public byte[] getIv() {
+        final IV iv = this.getExtension(IV.class);
+        if (iv == null) {
+            throw new IllegalStateException("No IV in header");
+        }
+        return iv.asBytes();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "iv")
+public class IV extends Extension implements ByteContent {
+
+    public IV() {
+        super(IV.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "identityKey")
+public class IdentityKey extends Extension implements ECPublicKeyContent {
+
+    public IdentityKey() {
+        super(IdentityKey.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java 🔗

@@ -0,0 +1,29 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Key extends Extension implements ByteContent {
+
+    public Key() {
+        super(Key.class);
+    }
+
+    public void setIsPreKey(boolean isPreKey) {
+        this.setAttribute("prekey", isPreKey);
+    }
+
+    public boolean isPreKey() {
+        return this.getAttributeAsBoolean("prekey");
+    }
+
+    public void setRemoteDeviceId(final int remoteDeviceId) {
+        this.setAttribute("rid", remoteDeviceId);
+    }
+
+    public Integer getRemoteDeviceId() {
+        return getOptionalIntAttribute("rid").orNull();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Payload extends Extension implements ByteContent {
+
+    public Payload() {
+        super(Payload.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.primitives.Ints;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "preKeyPublic")
+public class PreKey extends Extension implements ECPublicKeyContent {
+
+    public PreKey() {
+        super(PreKey.class);
+    }
+
+    public int getId() {
+        return Ints.saturatedCast(this.getLongAttribute("preKeyId"));
+    }
+
+    public void setId(int id) {
+        this.setAttribute("preKeyId", id);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "prekeys")
+public class PreKeys extends Extension {
+
+    public PreKeys() {
+        super(PreKeys.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import com.google.common.primitives.Ints;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "signedPreKeyPublic")
+public class SignedPreKey extends Extension implements ECPublicKeyContent {
+
+    public SignedPreKey() {
+        super(SignedPreKey.class);
+    }
+
+    public int getId() {
+        return Ints.saturatedCast(this.getLongAttribute("signedPreKeyId"));
+    }
+
+    public void setId(final int id) {
+        this.setAttribute("signedPreKeyId", id);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.axolotl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "signedPreKeySignature")
+public class SignedPreKeySignature extends Extension implements ByteContent {
+
+    public SignedPreKeySignature() {
+        super(SignedPreKeySignature.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind/Bind.java 🔗

@@ -0,0 +1,34 @@
+package im.conversations.android.xmpp.model.bind;
+
+import com.google.common.base.Strings;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Bind extends Extension {
+
+    public Bind() {
+        super(Bind.class);
+    }
+
+    public void setResource(final String resource) {
+        this.addExtension(new Resource(resource));
+    }
+
+    public eu.siacs.conversations.xmpp.Jid getJid() {
+        final var jidExtension = this.getExtension(Jid.class);
+        if (jidExtension == null) {
+            return null;
+        }
+        final var content = jidExtension.getContent();
+        if (Strings.isNullOrEmpty(content)) {
+            return null;
+        }
+        try {
+            return eu.siacs.conversations.xmpp.Jid.ofEscaped(content);
+        } catch (final IllegalArgumentException e) {
+            return null;
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind/Jid.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.bind;
+
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Jid extends Extension {
+
+    public Jid() {
+        super(Jid.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind/Resource.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.bind;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Resource extends Extension {
+    public Resource() {
+        super(Resource.class);
+    }
+
+    public Resource(final String resource) {
+        this();
+        this.setContent(resource);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java 🔗

@@ -0,0 +1,24 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Bind extends Extension {
+
+    public Bind() {
+        super(Bind.class);
+    }
+
+    public Inline getInline() {
+        return this.getExtension(Inline.class);
+    }
+
+    public Collection<Feature> getInlineFeatures() {
+        final var inline = getInline();
+        return inline == null ? Collections.emptyList() : inline.getExtensions(Feature.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Bound extends Extension {
+    public Bound() {
+        super(Bound.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Feature extends Extension {
+
+    public Feature() {
+        super(Feature.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.bind2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Inline extends Extension {
+
+    public Inline() {
+        super(Inline.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/blocking/Block.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.blocking;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Block extends Extension {
+
+    public Block() {
+        super(Block.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/blocking/Item.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.blocking;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Item extends Extension {
+
+    public Item() {
+        super(Item.class);
+    }
+
+    public Jid getJid() {
+        return getAttributeAsJid("jid");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java 🔗

@@ -0,0 +1,32 @@
+package im.conversations.android.xmpp.model.bookmark;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Conference extends Extension {
+
+    public Conference() {
+        super(Conference.class);
+    }
+
+    public boolean isAutoJoin() {
+        return this.getAttributeAsBoolean("autojoin");
+    }
+
+    public String getConferenceName() {
+        return this.getAttribute("name");
+    }
+
+    public void setAutoJoin(boolean autoJoin) {
+        setAttribute("autojoin", autoJoin);
+    }
+
+    public Nick getNick() {
+        return this.getExtension(Nick.class);
+    }
+
+    public Extensions getExtensions() {
+        return this.getExtension(Extensions.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.bookmark;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Nick extends Extension {
+
+    public Nick() {
+        super(Nick.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java 🔗

@@ -0,0 +1,43 @@
+package im.conversations.android.xmpp.model.capabilties;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.io.BaseEncoding;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.Hash;
+
+@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES_2)
+public class Capabilities extends Extension {
+
+    public Capabilities() {
+        super(Capabilities.class);
+    }
+
+    public EntityCapabilities2.EntityCaps2Hash getHash() {
+        final Optional<Hash> sha256Hash =
+                Iterables.tryFind(
+                        getExtensions(Hash.class), h -> h.getAlgorithm() == Hash.Algorithm.SHA_256);
+        if (sha256Hash.isPresent()) {
+            final String content = sha256Hash.get().getContent();
+            if (Strings.isNullOrEmpty(content)) {
+                return null;
+            }
+            if (BaseEncoding.base64().canDecode(content)) {
+                return EntityCapabilities2.EntityCaps2Hash.of(Hash.Algorithm.SHA_256, content);
+            }
+        }
+        return null;
+    }
+
+    public void setHash(final EntityCapabilities2.EntityCaps2Hash caps2Hash) {
+        final Hash hash = new Hash();
+        hash.setAlgorithm(caps2Hash.algorithm);
+        hash.setContent(caps2Hash.encoded());
+        this.addExtension(hash);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java 🔗

@@ -0,0 +1,39 @@
+package im.conversations.android.xmpp.model.capabilties;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import im.conversations.android.xmpp.model.Extension;
+
+public interface EntityCapabilities {
+
+    <E extends Extension> E getExtension(final Class<E> clazz);
+
+    default NodeHash getCapabilities() {
+        final String node;
+        final im.conversations.android.xmpp.EntityCapabilities.Hash hash;
+        final var capabilities = this.getExtension(Capabilities.class);
+        final var legacyCapabilities = this.getExtension(LegacyCapabilities.class);
+        if (capabilities != null) {
+            node = null;
+            hash = capabilities.getHash();
+        } else if (legacyCapabilities != null) {
+            node = legacyCapabilities.getNode();
+            hash = legacyCapabilities.getHash();
+        } else {
+            return null;
+        }
+        return hash == null ? null : new NodeHash(node, hash);
+    }
+
+    class NodeHash {
+        public final String node;
+        public final im.conversations.android.xmpp.EntityCapabilities.Hash hash;
+
+        private NodeHash(
+                @Nullable String node,
+                @NonNull final im.conversations.android.xmpp.EntityCapabilities.Hash hash) {
+            this.node = node;
+            this.hash = hash;
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java 🔗

@@ -0,0 +1,45 @@
+package im.conversations.android.xmpp.model.capabilties;
+
+import com.google.common.base.Strings;
+import com.google.common.io.BaseEncoding;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.EntityCapabilities;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES)
+public class LegacyCapabilities extends Extension {
+
+    private static final String HASH_ALGORITHM = "sha-1";
+
+    public LegacyCapabilities() {
+        super(LegacyCapabilities.class);
+    }
+
+    public String getNode() {
+        return this.getAttribute("node");
+    }
+
+    public EntityCapabilities.EntityCapsHash getHash() {
+        final String hash = getAttribute("hash");
+        final String ver = getAttribute("ver");
+        if (Strings.isNullOrEmpty(ver) || Strings.isNullOrEmpty(hash)) {
+            return null;
+        }
+        if (HASH_ALGORITHM.equals(hash) && BaseEncoding.base64().canDecode(ver)) {
+            return EntityCapabilities.EntityCapsHash.of(ver);
+        } else {
+            return null;
+        }
+    }
+
+    public void setNode(final String node) {
+        this.setAttribute("node", node);
+    }
+
+    public void setHash(final EntityCapabilities.EntityCapsHash hash) {
+        this.setAttribute("hash", HASH_ALGORITHM);
+        this.setAttribute("ver", hash.encoded());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.carbons;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Enable extends Extension {
+
+    public Enable() {
+        super(Enable.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/carbons/Received.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.carbons;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.forward.Forwarded;
+
+@XmlElement
+public class Received extends Extension {
+
+    public Received() {
+        super(Received.class);
+    }
+
+    public Forwarded getForwarded() {
+        return this.getExtension(Forwarded.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.carbons;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.forward.Forwarded;
+
+@XmlElement
+public class Sent extends Extension {
+
+    public Sent() {
+        super(Sent.class);
+    }
+
+    public Forwarded getForwarded() {
+        return this.getExtension(Forwarded.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/correction/Replace.java 🔗

@@ -0,0 +1,24 @@
+package im.conversations.android.xmpp.model.correction;
+
+import androidx.annotation.NonNull;
+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;
+
+@XmlElement(namespace = Namespace.LAST_MESSAGE_CORRECTION)
+public class Replace extends Extension {
+
+    public Replace() {
+        super(Replace.class);
+    }
+
+    public String getId() {
+        return Strings.emptyToNull(this.getAttribute("id"));
+    }
+
+    public void setId(@NonNull final String id) {
+        this.setAttribute("id", id);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/csi/Active.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.csi;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Active extends StreamElement {
+
+    public Active() {
+        super(Active.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.csi;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement(name = "csi")
+public class ClientStateIndication extends StreamFeature {
+
+    public ClientStateIndication() {
+        super(ClientStateIndication.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.csi;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Inactive extends StreamElement {
+
+    public Inactive() {
+        super(Inactive.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/data/Data.java 🔗

@@ -0,0 +1,110 @@
+package im.conversations.android.xmpp.model.data;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Map;
+
+@XmlElement(name = "x")
+public class Data extends Extension {
+
+    private static final String FORM_TYPE = "FORM_TYPE";
+    private static final String FIELD_TYPE_HIDDEN = "hidden";
+    private static final String FORM_TYPE_SUBMIT = "submit";
+
+    public Data() {
+        super(Data.class);
+    }
+
+    public String getFormType() {
+        final var fields = this.getExtensions(Field.class);
+        final var formTypeField = Iterables.find(fields, f -> FORM_TYPE.equals(f.getFieldName()));
+        return Iterables.getFirst(formTypeField.getValues(), null);
+    }
+
+    public Collection<Field> getFields() {
+        return Collections2.filter(
+                this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName()));
+    }
+
+    private void addField(final String name, final Object value) {
+        addField(name, value, null);
+    }
+
+    private void addField(final String name, final Object value, final String type) {
+        if (value == null) {
+            throw new IllegalArgumentException("Null values are not supported on data fields");
+        }
+        final var field = this.addExtension(new Field());
+        field.setFieldName(name);
+        if (type != null) {
+            field.setType(type);
+        }
+        if (value instanceof Collection) {
+            for (final Object subValue : (Collection<?>) value) {
+                if (subValue instanceof String) {
+                    final var valueExtension = field.addExtension(new Value());
+                    valueExtension.setContent((String) subValue);
+                } else {
+                    throw new IllegalArgumentException(
+                            String.format(
+                                    "%s is not a supported field value",
+                                    subValue.getClass().getSimpleName()));
+                }
+            }
+        } else {
+            final var valueExtension = field.addExtension(new Value());
+            if (value instanceof String) {
+                valueExtension.setContent((String) value);
+            } else if (value instanceof Integer) {
+                valueExtension.setContent(String.valueOf(value));
+            } else if (value instanceof Boolean) {
+                valueExtension.setContent(Boolean.TRUE.equals(value) ? "1" : "0");
+            } else {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "%s is not a supported field value",
+                                value.getClass().getSimpleName()));
+            }
+        }
+    }
+
+    private void setFormType(final String formType) {
+        this.addField(FORM_TYPE, formType, FIELD_TYPE_HIDDEN);
+    }
+
+    public static Data of(final String formType, final Map<String, Object> values) {
+        final var data = new Data();
+        data.setType(FORM_TYPE_SUBMIT);
+        data.setFormType(formType);
+        for (final Map.Entry<String, Object> entry : values.entrySet()) {
+            data.addField(entry.getKey(), entry.getValue());
+        }
+        return data;
+    }
+
+    public Data submit(final Map<String, Object> values) {
+        final String formType = this.getFormType();
+        final var submit = new Data();
+        submit.setType(FORM_TYPE_SUBMIT);
+        if (formType != null) {
+            submit.setFormType(formType);
+        }
+        for (final Field existingField : this.getFields()) {
+            final var fieldName = existingField.getFieldName();
+            final Object submittedValue = values.get(fieldName);
+            if (submittedValue != null) {
+                submit.addField(fieldName, submittedValue);
+            } else {
+                submit.addField(fieldName, existingField.getValues());
+            }
+        }
+        return submit;
+    }
+
+    private void setType(final String type) {
+        this.setAttribute("type", type);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/data/Field.java 🔗

@@ -0,0 +1,29 @@
+package im.conversations.android.xmpp.model.data;
+import eu.siacs.conversations.xml.Element;
+import com.google.common.collect.Collections2;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+
+@XmlElement
+public class Field extends Extension {
+    public Field() {
+        super(Field.class);
+    }
+
+    public String getFieldName() {
+        return getAttribute("var");
+    }
+
+    public Collection<String> getValues() {
+        return Collections2.transform(getExtensions(Value.class), Element::getContent);
+    }
+
+    public void setFieldName(String name) {
+        this.setAttribute("var", name);
+    }
+
+    public void setType(String type) {
+        this.setAttribute("type", type);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/data/Option.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.data;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Option extends Extension {
+
+    public Option() {
+        super(Option.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/data/Value.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.data;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Value extends Extension {
+
+    public Value() {
+        super(Value.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/delay/Delay.java 🔗

@@ -0,0 +1,30 @@
+package im.conversations.android.xmpp.model.delay;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.Timestamps;
+import im.conversations.android.xmpp.model.Extension;
+import java.text.ParseException;
+import java.time.Instant;
+
+@XmlElement(namespace = Namespace.DELAY)
+public class Delay extends Extension {
+
+    public Delay() {
+        super(Delay.class);
+    }
+
+    public Instant getStamp() {
+        final var stamp = this.getAttribute("stamp");
+        if (Strings.isNullOrEmpty(stamp)) {
+            return null;
+        }
+        try {
+            return Instant.ofEpochMilli(Timestamps.parse(stamp));
+        } catch (final IllegalArgumentException | ParseException e) {
+            return null;
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java 🔗

@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp.model.disco.info;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Feature extends Extension {
+    public Feature() {
+        super(Feature.class);
+    }
+
+    public String getVar() {
+        return this.getAttribute("var");
+    }
+
+    public void setVar(final String feature) {
+        this.setAttribute("var", feature);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java 🔗

@@ -0,0 +1,39 @@
+package im.conversations.android.xmpp.model.disco.info;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Identity extends Extension {
+    public Identity() {
+        super(Identity.class);
+    }
+
+    public String getCategory() {
+        return this.getAttribute("category");
+    }
+
+    public String getType() {
+        return this.getAttribute("type");
+    }
+
+    public String getLang() {
+        return this.getAttribute("xml:lang");
+    }
+
+    public String getIdentityName() {
+        return this.getAttribute("name");
+    }
+
+    public void setIdentityName(final String name) {
+        this.setAttribute("name", name);
+    }
+
+    public void setType(final String type) {
+        this.setAttribute("type", type);
+    }
+
+    public void setCategory(final String category) {
+        this.setAttribute("category", category);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java 🔗

@@ -0,0 +1,38 @@
+package im.conversations.android.xmpp.model.disco.info;
+
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+
+@XmlElement(name = "query")
+public class InfoQuery extends Extension {
+
+    public InfoQuery() {
+        super(InfoQuery.class);
+    }
+
+    public void setNode(final String node) {
+        this.setAttribute("node", node);
+    }
+
+    public String getNode() {
+        return this.getAttribute("node");
+    }
+
+    public Collection<Feature> getFeatures() {
+        return this.getExtensions(Feature.class);
+    }
+
+    public boolean hasFeature(final String feature) {
+        return Iterables.any(getFeatures(), f -> feature.equals(f.getVar()));
+    }
+
+    public Collection<Identity> getIdentities() {
+        return this.getExtensions(Identity.class);
+    }
+
+    public boolean hasIdentityWithCategory(final String category) {
+        return Iterables.any(getIdentities(), i -> category.equals(i.getCategory()));
+    }
+}

src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java 🔗

@@ -0,0 +1,22 @@
+package im.conversations.android.xmpp.model.disco.items;
+
+import androidx.annotation.Nullable;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Item extends Extension {
+    public Item() {
+        super(Item.class);
+    }
+
+    public Jid getJid() {
+        return getAttributeAsJid("jid");
+    }
+
+    public @Nullable String getNode() {
+        return this.getAttribute("node");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java 🔗

@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp.model.disco.items;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "query")
+public class ItemsQuery extends Extension {
+    public ItemsQuery() {
+        super(ItemsQuery.class);
+    }
+
+    public void setNode(final String node) {
+        this.setAttribute("node", node);
+    }
+
+    public String getNode() {
+        return this.getAttribute("node");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/error/Condition.java 🔗

@@ -0,0 +1,188 @@
+package im.conversations.android.xmpp.model.error;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class Condition extends Extension {
+
+    private Condition(Class<? extends Condition> clazz) {
+        super(clazz);
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class BadRequest extends Condition {
+
+        public BadRequest() {
+            super(BadRequest.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class Conflict extends Condition {
+
+        public Conflict() {
+            super(Conflict.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class FeatureNotImplemented extends Condition {
+
+        public FeatureNotImplemented() {
+            super(FeatureNotImplemented.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class Forbidden extends Condition {
+
+        public Forbidden() {
+            super(Forbidden.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class Gone extends Condition {
+
+        public Gone() {
+            super(Gone.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class InternalServerError extends Condition {
+
+        public InternalServerError() {
+            super(InternalServerError.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class ItemNotFound extends Condition {
+
+        public ItemNotFound() {
+            super(ItemNotFound.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class JidMalformed extends Condition {
+
+        public JidMalformed() {
+            super(JidMalformed.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class NotAcceptable extends Condition {
+
+        public NotAcceptable() {
+            super(NotAcceptable.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class NotAllowed extends Condition {
+
+        public NotAllowed() {
+            super(NotAllowed.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class NotAuthorized extends Condition {
+
+        public NotAuthorized() {
+            super(NotAuthorized.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class PaymentRequired extends Condition {
+
+        public PaymentRequired() {
+            super(PaymentRequired.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class RecipientUnavailable extends Condition {
+
+        public RecipientUnavailable() {
+            super(RecipientUnavailable.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class Redirect extends Condition {
+
+        public Redirect() {
+            super(Redirect.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class RegistrationRequired extends Condition {
+
+        public RegistrationRequired() {
+            super(RegistrationRequired.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class RemoteServerNotFound extends Condition {
+
+        public RemoteServerNotFound() {
+            super(RemoteServerNotFound.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class RemoteServerTimeout extends Condition {
+
+        public RemoteServerTimeout() {
+            super(RemoteServerTimeout.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class ResourceConstraint extends Condition {
+
+        public ResourceConstraint() {
+            super(ResourceConstraint.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class ServiceUnavailable extends Condition {
+
+        public ServiceUnavailable() {
+            super(ServiceUnavailable.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class SubscriptionRequired extends Condition {
+
+        public SubscriptionRequired() {
+            super(SubscriptionRequired.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class UndefinedCondition extends Condition {
+
+        public UndefinedCondition() {
+            super(UndefinedCondition.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.STANZAS)
+    public static class UnexpectedRequest extends Condition {
+
+        public UnexpectedRequest() {
+            super(UnexpectedRequest.class);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/error/Error.java 🔗

@@ -0,0 +1,55 @@
+package im.conversations.android.xmpp.model.error;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Locale;
+import eu.siacs.conversations.xml.Namespace;
+
+@XmlElement(namespace = Namespace.JABBER_CLIENT)
+public class Error extends Extension {
+
+    public Error() {
+        super(Error.class);
+    }
+
+    public Condition getCondition() {
+        return this.getExtension(Condition.class);
+    }
+
+    public void setCondition(final Condition condition) {
+        this.addExtension(condition);
+    }
+
+    public Text getText() {
+        return this.getExtension(Text.class);
+    }
+
+    public String getTextAsString() {
+        final var text = getText();
+        return text == null ? null : text.getContent();
+    }
+
+    public void setType(final Type type) {
+        this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
+    }
+
+    public void addExtensions(final Extension[] extensions) {
+        for (final Extension extension : extensions) {
+            this.addExtension(extension);
+        }
+    }
+
+    public enum Type {
+        MODIFY,
+        CANCEL,
+        AUTH,
+        WAIT
+    }
+
+    public static class Extension extends im.conversations.android.xmpp.model.Extension {
+
+        public Extension(Class<? extends im.conversations.android.xmpp.model.Extension> clazz) {
+            super(clazz);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/error/Text.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.error;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.STANZAS)
+public class Text extends Extension {
+
+    public Text() {
+        super(Text.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/fast/Fast.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.fast;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Fast extends Extension {
+    public Fast() {
+        super(Fast.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.fast;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Mechanism extends Extension {
+    public Mechanism() {
+        super(Mechanism.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.fast;
+
+import eu.siacs.conversations.crypto.sasl.HashedToken;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class RequestToken extends Extension {
+    public RequestToken() {
+        super(RequestToken.class);
+    }
+
+    public RequestToken(final HashedToken.Mechanism mechanism) {
+        this();
+        this.setAttribute("mechanism", mechanism.name());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/fast/Token.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.fast;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Token extends Extension {
+
+    public Token() {
+        super(Token.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.forward;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.stanza.Message;
+
+@XmlElement(namespace = Namespace.FORWARD)
+public class Forwarded extends Extension {
+
+    public Forwarded() {
+        super(Forwarded.class);
+    }
+
+    public Message getMessage() {
+        return this.getExtension(Message.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/hints/Store.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.hints;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Store extends Extension {
+
+    public Store() {
+        super(Store.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Body.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Body extends Extension {
+
+    public Body() {
+        super(Body.class);
+    }
+
+    public Body(final String content) {
+        this();
+        setContent(content);
+    }
+
+    public String getLang() {
+        return this.getAttribute("xml:lang");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Priority extends Extension {
+
+    public Priority() {
+        super(Priority.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Show.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Show extends Extension {
+    public Show() {
+        super(Show.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Status.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Status extends Extension {
+
+
+    public Status() {
+        super(Status.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Subject extends Extension {
+
+    public Subject() {
+        super(Subject.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.jabber;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Thread extends Extension {
+
+    public Thread() {
+        super(Thread.class);
+    }
+}

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java → src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java 🔗

@@ -1,4 +1,4 @@
-package eu.siacs.conversations.xmpp.jingle.stanzas;
+package im.conversations.android.xmpp.model.jingle;
 
 import androidx.annotation.NonNull;
 
@@ -10,66 +10,38 @@ import com.google.common.collect.ImmutableMap;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 
-import java.util.Map;
-
-public class JinglePacket extends IqPacket {
-
-    private JinglePacket() {
-        super();
-    }
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
 
-    public JinglePacket(final Action action, final String sessionId) {
-        super(TYPE.SET);
-        final Element jingle = addChild("jingle", Namespace.JINGLE);
-        jingle.setAttribute("sid", sessionId);
-        jingle.setAttribute("action", action.toString());
-    }
+import java.util.Map;
 
-    public static JinglePacket upgrade(final IqPacket iqPacket) {
-        Preconditions.checkArgument(iqPacket.hasChild("jingle", Namespace.JINGLE));
-        Preconditions.checkArgument(iqPacket.getType() == TYPE.SET);
-        final JinglePacket jinglePacket = new JinglePacket();
-        jinglePacket.setAttributes(iqPacket.getAttributes());
-        jinglePacket.setChildren(iqPacket.getChildren());
-        return jinglePacket;
-    }
+@XmlElement
+public class Jingle extends Extension {
 
-    // TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
-    public Content getJingleContent() {
-        final Element content = getJingleChild("content");
-        return content == null ? null : Content.upgrade(content);
+    public Jingle() {
+        super(Jingle.class);
     }
 
-    public Group getGroup() {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        final Element group = jingle.findChild("group", Namespace.JINGLE_APPS_GROUPING);
-        return group == null ? null : Group.upgrade(group);
+    public Jingle(final Action action, final String sessionId) {
+        this();
+        this.setAttribute("sid", sessionId);
+        this.setAttribute("action", action.toString());
     }
 
-    public void addGroup(final Group group) {
-        this.addJingleChild(group);
-    }
-
-    public Map<String, Content> getJingleContents() {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        ImmutableMap.Builder<String, Content> builder = new ImmutableMap.Builder<>();
-        for (final Element child : jingle.getChildren()) {
-            if ("content".equals(child.getName())) {
-                final Content content = Content.upgrade(child);
-                builder.put(content.getContentName(), content);
-            }
-        }
-        return builder.build();
+    public String getSessionId() {
+        return this.getAttribute("sid");
     }
 
-    public void addJingleContent(final Content content) { // take content interface
-        addJingleChild(content);
+    public Action getAction() {
+        return Action.of(this.getAttribute("action"));
     }
 
     public ReasonWrapper getReason() {
-        final Element reasonElement = getJingleChild("reason");
+        final Element reasonElement = this.findChild("reason");
         if (reasonElement == null) {
             return new ReasonWrapper(Reason.UNKNOWN, null);
         }
@@ -86,8 +58,7 @@ public class JinglePacket extends IqPacket {
     }
 
     public void setReason(final Reason reason, final String text) {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        final Element reasonElement = jingle.addChild("reason");
+        final Element reasonElement = this.addChild("reason");
         reasonElement.addChild(reason.toString());
         if (!Strings.isNullOrEmpty(text)) {
             reasonElement.addChild("text").setContent(text);
@@ -97,31 +68,44 @@ public class JinglePacket extends IqPacket {
     // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
     public void setInitiator(final Jid initiator) {
         Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID");
-        findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator);
+        this.setAttribute("initiator", initiator);
     }
 
     // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
-    public void setResponder(Jid responder) {
+    public void setResponder(final Jid responder) {
         Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID");
-        findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder);
+        this.setAttribute("responder", responder);
     }
 
-    public Element getJingleChild(final String name) {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        return jingle == null ? null : jingle.findChild(name);
+    public Group getGroup() {
+        final Element group = this.findChild("group", Namespace.JINGLE_APPS_GROUPING);
+        return group == null ? null : Group.upgrade(group);
     }
 
-    public void addJingleChild(final Element child) {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        jingle.addChild(child);
+    public void addGroup(final Group group) {
+        this.addChild(group);
     }
 
-    public String getSessionId() {
-        return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
+    // TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
+    public Content getJingleContent() {
+        final Element content = this.findChild("content");
+        return content == null ? null : Content.upgrade(content);
     }
 
-    public Action getAction() {
-        return Action.of(findChild("jingle", Namespace.JINGLE).getAttribute("action"));
+    public void addJingleContent(final Content content) { // take content interface
+        this.addChild(content);
+    }
+
+
+    public Map<String, Content> getJingleContents() {
+        ImmutableMap.Builder<String, Content> builder = new ImmutableMap.Builder<>();
+        for (final Element child : this.getChildren()) {
+            if ("content".equals(child.getName())) {
+                final Content content = Content.upgrade(child);
+                builder.put(content.getContentName(), content);
+            }
+        }
+        return builder.build();
     }
 
     public enum Action {

src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java 🔗

@@ -0,0 +1,44 @@
+package im.conversations.android.xmpp.model.jingle.error;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.error.Error;
+
+public abstract class JingleCondition extends Error.Extension {
+
+    private JingleCondition(Class<? extends JingleCondition> clazz) {
+        super(clazz);
+    }
+
+    @XmlElement(namespace = Namespace.JINGLE_ERRORS)
+    public static class OutOfOrder extends JingleCondition {
+
+        public OutOfOrder() {
+            super(OutOfOrder.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.JINGLE_ERRORS)
+    public static class TieBreak extends JingleCondition {
+
+        public TieBreak() {
+            super(TieBreak.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.JINGLE_ERRORS)
+    public static class UnknownSession extends JingleCondition {
+
+        public UnknownSession() {
+            super(UnknownSession.class);
+        }
+    }
+
+    @XmlElement(namespace = Namespace.JINGLE_ERRORS)
+    public static class UnsupportedInfo extends JingleCondition {
+
+        public UnsupportedInfo() {
+            super(UnsupportedInfo.class);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java 🔗

@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.jmi;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class JingleMessage extends Extension {
+
+    public JingleMessage(Class<? extends JingleMessage> clazz) {
+        super(clazz);
+    }
+
+    public String getSessionId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java 🔗

@@ -0,0 +1,24 @@
+package im.conversations.android.xmpp.model.jmi;
+
+import com.google.common.primitives.Ints;
+
+import eu.siacs.conversations.xml.Element;
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Proceed extends JingleMessage {
+
+    public Proceed() {
+        super(Proceed.class);
+    }
+
+    public Integer getDeviceId() {
+        // TODO use proper namespace and create extension
+        final Element device = this.findChild("device");
+        final String id = device == null ? null : device.getAttribute("id");
+        if (id == null) {
+            return null;
+        }
+        return Ints.tryParse(id);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java 🔗

@@ -0,0 +1,38 @@
+package im.conversations.android.xmpp.model.jmi;
+
+import com.google.common.collect.ImmutableList;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import im.conversations.android.annotation.XmlElement;
+
+import java.util.List;
+
+@XmlElement
+public class Propose extends JingleMessage {
+
+    public Propose() {
+        super(Propose.class);
+    }
+
+    public List<GenericDescription> getDescriptions() {
+        final ImmutableList.Builder<GenericDescription> builder = new ImmutableList.Builder<>();
+        // TODO create proper extension for description
+        for (final Element child : this.children) {
+            if ("description".equals(child.getName())) {
+                final String namespace = child.getNamespace();
+                if (Namespace.JINGLE_APPS_FILE_TRANSFER.contains(namespace)) {
+                    builder.add(FileTransferDescription.upgrade(child));
+                } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
+                    builder.add(RtpDescription.upgrade(child));
+                } else {
+                    builder.add(GenericDescription.upgrade(child));
+                }
+            }
+        }
+        return builder.build();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/End.java 🔗

@@ -0,0 +1,15 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class End extends Extension {
+    public End() {
+        super(End.class);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/Fin.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Fin extends Extension {
+
+    public Fin() {
+        super(Fin.class);
+    }
+
+    public boolean isComplete() {
+        return this.getAttributeAsBoolean("complete");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Metadata extends Extension {
+
+    public Metadata() {
+        super(Metadata.class);
+    }
+
+    public Start getStart() {
+        return this.getExtension(Start.class);
+    }
+
+    public End getEnd() {
+        return this.getExtension(End.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/Query.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Query extends Extension {
+
+    public Query() {
+        super(Query.class);
+    }
+
+    public void setQueryId(final String id) {
+        this.setAttribute("queryid", id);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/Result.java 🔗

@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.forward.Forwarded;
+
+@XmlElement
+public class Result extends Extension {
+
+    public Result() {
+        super(Result.class);
+    }
+
+    public Forwarded getForwarded() {
+        return this.getExtension(Forwarded.class);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+
+    public String getQueryId() {
+        return this.getAttribute("queryid");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mam/Start.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.mam;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Start extends Extension {
+
+    public Start() {
+        super(Start.class);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.markers;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Displayed extends Extension {
+
+    public Displayed() {
+        super(Displayed.class);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/markers/Markable.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.markers;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.DeliveryReceiptRequest;
+
+@XmlElement
+public class Markable extends DeliveryReceiptRequest {
+
+    public Markable() {
+        super(Markable.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/markers/Received.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.markers;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.DeliveryReceipt;
+
+@XmlElement
+public class Received extends DeliveryReceipt {
+
+    public Received() {
+        super(Received.class);
+    }
+
+    public void setId(String id) {
+        this.setAttribute("id", id);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.mds;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.MDS_DISPLAYED)
+public class Displayed extends Extension {
+    public Displayed() {
+        super(Displayed.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/muc/History.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.muc;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class History extends Extension {
+
+    public History() {
+        super(History.class);
+    }
+
+    public void setMaxChars(final int maxChars) {
+        this.setAttribute("maxchars", maxChars);
+    }
+
+    public void setMaxStanzas(final int maxStanzas) {
+        this.setAttribute("maxstanzas", maxStanzas);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.muc;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x")
+public class MultiUserChat extends Extension {
+
+    public MultiUserChat() {
+        super(MultiUserChat.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java 🔗

@@ -0,0 +1,58 @@
+package im.conversations.android.xmpp.model.muc.user;
+
+import android.util.Log;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.Jid;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
+
+import java.util.Locale;
+
+@XmlElement
+public class Item extends Extension {
+
+
+    public Item() {
+        super(Item.class);
+    }
+
+    public Affiliation getAffiliation() {
+        final var affiliation = this.getAttribute("affiliation");
+        if (Strings.isNullOrEmpty(affiliation)) {
+            return Affiliation.NONE;
+        }
+        try {
+            return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT));
+        } catch (final IllegalArgumentException e) {
+            Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation);
+            return Affiliation.NONE;
+        }
+    }
+
+    public Role getRole() {
+        final var role = this.getAttribute("role");
+        if (Strings.isNullOrEmpty(role)) {
+            return Role.NONE;
+        }
+        try {
+            return Role.valueOf(role.toUpperCase(Locale.ROOT));
+        } catch (final IllegalArgumentException e) {
+            Log.d(Config.LOGTAG,"could not parse role "+ role);
+            return Role.NONE;
+        }
+    }
+
+    public String getNick() {
+        return this.getAttribute("nick");
+    }
+
+    public Jid getJid() {
+        return this.getAttributeAsJid("jid");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java 🔗

@@ -0,0 +1,27 @@
+package im.conversations.android.xmpp.model.muc.user;
+
+import com.google.common.collect.Collections2;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement(name = "x")
+public class MucUser extends Extension {
+
+    public static final int STATUS_CODE_SELF_PRESENCE = 110;
+
+    public MucUser() {
+        super(MucUser.class);
+    }
+
+    public Item getItem() {
+        return this.getExtension(Item.class);
+    }
+
+    public Collection<Integer> getStatus() {
+        return Collections2.filter(
+                Collections2.transform(getExtensions(Status.class), Status::getCode),
+                Objects::nonNull);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.muc.user;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Status extends Extension {
+
+    public Status() {
+        super(Status.class);
+    }
+
+    public Integer getCode() {
+        return this.getOptionalIntAttribute("code").orNull();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/nick/Nick.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.nick;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.NICK)
+public class Nick extends Extension {
+
+    public Nick() {
+        super(Nick.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java 🔗

@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp.model.occupant;
+
+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;
+
+@XmlElement(namespace = Namespace.OCCUPANT_ID)
+public class OccupantId extends Extension {
+
+    public OccupantId() {
+        super(OccupantId.class);
+    }
+
+    public String getId() {
+        return Strings.emptyToNull(this.getAttribute("id"));
+    }
+}

src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.oob;
+
+import com.google.common.base.Strings;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x")
+public class OutOfBandData extends Extension {
+
+    public OutOfBandData() {
+        super(OutOfBandData.class);
+    }
+
+    public String getURL() {
+        final URL url = this.getExtension(URL.class);
+        return url == null ? null : Strings.emptyToNull(url.getContent());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/oob/URL.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.oob;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "url")
+public class URL extends Extension {
+
+    public URL() {
+        super(URL.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.pars;
+
+import im.conversations.android.annotation.XmlElement;
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.PARS)
+public class PreAuth extends Extension {
+
+    public PreAuth() {
+        super(PreAuth.class);
+    }
+
+    public void setToken(final String token) {
+        this.setAttribute("token", token);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java 🔗

@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.pgp;
+
+import eu.siacs.conversations.xml.Namespace;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x", namespace = Namespace.PGP_ENCRYPTED)
+public class Encrypted extends Extension {
+
+    public Encrypted() {
+        super(Encrypted.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java 🔗

@@ -0,0 +1,15 @@
+package im.conversations.android.xmpp.model.pgp;
+
+import eu.siacs.conversations.xml.Namespace;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED)
+public class Signed extends Extension {
+
+
+    public Signed() {
+        super(Signed.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/ping/Ping.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.ping;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.PING)
+public class Ping extends Extension {
+
+    public Ping() {
+        super(Ping.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java 🔗

@@ -0,0 +1,52 @@
+package im.conversations.android.xmpp.model.pubsub;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.pubsub.event.Retract;
+import java.util.Collection;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+public interface Items {
+
+    Collection<? extends Item> getItems();
+
+    String getNode();
+
+    Collection<Retract> getRetractions();
+
+    default <T extends Extension> Map<String, T> getItemMap(final Class<T> clazz) {
+        final ImmutableMap.Builder<String, T> builder = ImmutableMap.builder();
+        for (final Item item : getItems()) {
+            final var id = item.getId();
+            final T extension = item.getExtension(clazz);
+            if (extension == null || Strings.isNullOrEmpty(id)) {
+                continue;
+            }
+            builder.put(id, extension);
+        }
+        return builder.buildKeepingLast();
+    }
+
+    default <T extends Extension> T getItemOrThrow(final String id, final Class<T> clazz) {
+        final var map = getItemMap(clazz);
+        final var item = map.get(id);
+        if (item == null) {
+            throw new NoSuchElementException(
+                    String.format("An item with id %s does not exist", id));
+        }
+        return item;
+    }
+
+    default <T extends Extension> T getFirstItem(final Class<T> clazz) {
+        final var map = getItemMap(clazz);
+        return Iterables.getFirst(map.values(), null);
+    }
+
+    default <T extends Extension> T getOnlyItem(final Class<T> clazz) {
+        final var map = getItemMap(clazz);
+        return Iterables.getOnlyElement(map.values());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java 🔗

@@ -0,0 +1,64 @@
+package im.conversations.android.xmpp.model.pubsub;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.pubsub.event.Retract;
+import java.util.Collection;
+
+@XmlElement(name = "pubsub")
+public class PubSub extends Extension {
+
+    public PubSub() {
+        super(PubSub.class);
+    }
+
+    public Items getItems() {
+        return this.getExtension(ItemsWrapper.class);
+    }
+
+    @XmlElement(name = "items")
+    public static class ItemsWrapper extends Extension implements Items {
+
+        public ItemsWrapper() {
+            super(ItemsWrapper.class);
+        }
+
+        public String getNode() {
+            return this.getAttribute("node");
+        }
+
+        public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
+            return this.getExtensions(Item.class);
+        }
+
+        public Collection<Retract> getRetractions() {
+            return this.getExtensions(Retract.class);
+        }
+
+        public void setNode(String node) {
+            this.setAttribute("node", node);
+        }
+
+        public void setMaxItems(final int maxItems) {
+            this.setAttribute("max_items", maxItems);
+        }
+    }
+
+    @XmlElement(name = "item")
+    public static class Item extends Extension
+            implements im.conversations.android.xmpp.model.pubsub.Item {
+
+        public Item() {
+            super(Item.class);
+        }
+
+        @Override
+        public String getId() {
+            return this.getAttribute("id");
+        }
+
+        public void setId(String itemId) {
+            this.setAttribute("id", itemId);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.pubsub;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Publish extends Extension {
+
+    public Publish() {
+        super(Publish.class);
+    }
+
+    public void setNode(String node) {
+        this.setAttribute("node", node);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.pubsub;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+
+@XmlElement
+public class PublishOptions extends Extension {
+
+    public PublishOptions() {
+        super(PublishOptions.class);
+    }
+
+    public static PublishOptions of(NodeConfiguration nodeConfiguration) {
+        final var publishOptions = new PublishOptions();
+        publishOptions.addExtension(Data.of(Namespace.PUBSUB_PUBLISH_OPTIONS, nodeConfiguration));
+        return publishOptions;
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.pubsub;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Retract extends Extension {
+
+    public Retract() {
+        super(Retract.class);
+    }
+
+    public void setNode(String node) {
+        this.setAttribute("node", node);
+    }
+
+    public void setNotify(boolean notify) {
+        this.setAttribute("notify", notify ? 1 : 0);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java 🔗

@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp.model.pubsub.error;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class PubSubError extends Extension {
+
+    private PubSubError(Class<? extends PubSubError> clazz) {
+        super(clazz);
+    }
+
+    @XmlElement
+    public static class PreconditionNotMet extends PubSubError {
+
+        public PreconditionNotMet() {
+            super(PreconditionNotMet.class);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java 🔗

@@ -0,0 +1,56 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import java.util.Collection;
+
+@XmlElement
+public class Event extends Extension {
+
+    public Event() {
+        super(Event.class);
+    }
+
+    public Items getItems() {
+        return this.getExtension(ItemsWrapper.class);
+    }
+
+    public Purge getPurge() {
+        return this.getExtension(Purge.class);
+    }
+
+    @XmlElement(name = "items")
+    public static class ItemsWrapper extends Extension implements Items {
+
+        public ItemsWrapper() {
+            super(ItemsWrapper.class);
+        }
+
+        public String getNode() {
+            return this.getAttribute("node");
+        }
+
+        public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
+            return this.getExtensions(Item.class);
+        }
+
+        public Collection<Retract> getRetractions() {
+            return this.getExtensions(Retract.class);
+        }
+    }
+
+    @XmlElement(name = "item")
+    public static class Item extends Extension
+            implements im.conversations.android.xmpp.model.pubsub.Item {
+
+        public Item() {
+            super(Item.class);
+        }
+
+        @Override
+        public String getId() {
+            return this.getAttribute("id");
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Purge extends Extension {
+
+    public Purge() {
+        super(Purge.class);
+    }
+
+    public String getNode() {
+        return this.getAttribute("node");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Retract extends Extension {
+
+    public Retract() {
+        super(Retract.class);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.pubsub.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+
+@XmlElement
+public class Configure extends Extension {
+
+    public Configure() {
+        super(Configure.class);
+    }
+
+    public void setNode(final String node) {
+        this.setAttribute("node", node);
+    }
+
+    public Data getData() {
+        return this.getExtension(Data.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.pubsub.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "pubsub")
+public class PubSubOwner extends Extension {
+
+    public PubSubOwner() {
+        super(PubSubOwner.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.reactions;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Reaction extends Extension {
+
+    public Reaction() {
+        super(Reaction.class);
+    }
+
+    public Reaction(final String reaction) {
+        this();
+        setContent(reaction);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java 🔗

@@ -0,0 +1,36 @@
+package im.conversations.android.xmpp.model.reactions;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Reactions extends Extension {
+
+    public Reactions() {
+        super(Reactions.class);
+    }
+
+    public Collection<String> getReactions() {
+        return Collections2.filter(
+                Collections2.transform(getExtensions(Reaction.class), Reaction::getContent),
+                r -> Objects.nonNull(Strings.nullToEmpty(r)));
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+
+    public void setId(String id) {
+        this.setAttribute("id", id);
+    }
+
+    public static Reactions to(final String id) {
+        final var reactions = new Reactions();
+        reactions.setId(id);
+        return reactions;
+    }
+}

src/main/java/im/conversations/android/xmpp/model/receipts/Received.java 🔗

@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.receipts;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.DeliveryReceipt;
+
+@XmlElement
+public class Received extends DeliveryReceipt {
+
+    public Received() {
+        super(Received.class);
+    }
+
+    public void setId(String id) {
+        this.setAttribute("id", id);
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/receipts/Request.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.receipts;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.DeliveryReceiptRequest;
+
+@XmlElement
+public class Request extends DeliveryReceiptRequest {
+
+    public Request() {
+        super(Request.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/register/Register.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.register;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import org.jxmpp.jid.parts.Localpart;
+
+@XmlElement(name = "query")
+public class Register extends Extension {
+
+    public Register() {
+        super(Register.class);
+    }
+
+    public void addUsername(final Localpart username) {
+        this.addExtension(new Username()).setContent(username.toString());
+    }
+
+    public void addPassword(final String password) {
+        this.addExtension(new Password()).setContent(password);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/roster/Group.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.roster;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Group extends Extension {
+
+    public Group() {
+        super(Group.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/roster/Item.java 🔗

@@ -0,0 +1,61 @@
+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;
+import java.util.Locale;
+import java.util.Objects;
+
+@XmlElement
+public class Item extends Extension {
+
+    public static final List<Subscription> RESULT_SUBSCRIPTIONS =
+            Arrays.asList(Subscription.NONE, Subscription.TO, Subscription.FROM, Subscription.BOTH);
+
+    public Item() {
+        super(Item.class);
+    }
+
+    public Jid getJid() {
+        return getAttributeAsJid("jid");
+    }
+
+    public String getItemName() {
+        return this.getAttribute("name");
+    }
+
+    public boolean isPendingOut() {
+        return "subscribe".equalsIgnoreCase(this.getAttribute("ask"));
+    }
+
+    public Subscription getSubscription() {
+        final String value = this.getAttribute("subscription");
+        try {
+            return value == null ? null : Subscription.valueOf(value.toUpperCase(Locale.ROOT));
+        } catch (final IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    public Collection<String> getGroups() {
+        return Collections2.filter(
+                Collections2.transform(getExtensions(Group.class), Element::getContent),
+                Objects::nonNull);
+    }
+
+    public enum Subscription {
+        NONE,
+        TO,
+        FROM,
+        BOTH,
+        REMOVE
+    }
+}

src/main/java/im/conversations/android/xmpp/model/roster/Query.java 🔗

@@ -0,0 +1,21 @@
+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;
+
+@XmlElement(name = "query", namespace = Namespace.ROSTER)
+public class Query extends Extension {
+
+    public Query() {
+        super(Query.class);
+    }
+
+    public void setVersion(final String rosterVersion) {
+        this.setAttribute("ver", rosterVersion);
+    }
+
+    public String getVersion() {
+        return this.getAttribute("ver");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/After.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class After extends Extension {
+
+    public After() {
+        super(After.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/Before.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Before extends Extension {
+
+    public Before() {
+        super(Before.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/Count.java 🔗

@@ -0,0 +1,23 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Count extends Extension {
+
+    public Count() {
+        super(Count.class);
+    }
+
+    public Integer getCount() {
+        final var content = getContent();
+        if (Strings.isNullOrEmpty(content)) {
+            return null;
+        } else {
+            return Ints.tryParse(content);
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/First.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class First extends Extension {
+
+    public First() {
+        super(First.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/Last.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Last extends Extension {
+
+    public Last() {
+        super(Last.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/Max.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Max extends Extension {
+
+    public Max() {
+        super(Max.class);
+    }
+
+    public void setMax(final int max) {
+        this.setContent(String.valueOf(max));
+    }
+}

src/main/java/im/conversations/android/xmpp/model/rsm/Set.java 🔗

@@ -0,0 +1,55 @@
+package im.conversations.android.xmpp.model.rsm;
+
+import com.google.common.base.Strings;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.Page;
+import im.conversations.android.xmpp.Range;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Set extends Extension {
+
+    public Set() {
+        super(Set.class);
+    }
+
+    public static Set of(final Range range, final Integer max) {
+        final var set = new Set();
+        if (range.order == Range.Order.NORMAL) {
+            final var after = set.addExtension(new After());
+            after.setContent(range.id);
+        } else if (range.order == Range.Order.REVERSE) {
+            final var before = set.addExtension(new Before());
+            before.setContent(range.id);
+        } else {
+            throw new IllegalArgumentException("Invalid order");
+        }
+        if (max != null) {
+            set.addExtension(new Max()).setMax(max);
+        }
+        return set;
+    }
+
+    public Page asPage() {
+        final var first = this.getExtension(First.class);
+        final var last = this.getExtension(Last.class);
+
+        final var firstId = first == null ? null : first.getContent();
+        final var lastId = last == null ? null : last.getContent();
+        if (Strings.isNullOrEmpty(firstId) || Strings.isNullOrEmpty(lastId)) {
+            throw new IllegalStateException("Invalid page. Missing first or last");
+        }
+        return new Page(firstId, lastId, this.getCount());
+    }
+
+    public boolean isEmpty() {
+        final var first = this.getExtension(First.class);
+        final var last = this.getExtension(Last.class);
+        return first == null && last == null;
+    }
+
+    public Integer getCount() {
+        final var count = this.getExtension(Count.class);
+        return count == null ? null : count.getCount();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Auth extends StreamElement {
+
+    public Auth() {
+        super(Auth.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Mechanism extends Extension {
+
+    public Mechanism() {
+        super(Mechanism.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java 🔗

@@ -0,0 +1,29 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import com.google.common.collect.Collections2;
+
+import eu.siacs.conversations.xml.Element;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Mechanisms extends AuthenticationStreamFeature {
+
+
+    public Mechanisms() {
+        super(Mechanisms.class);
+    }
+
+    public Collection<Mechanism> getMechanisms() {
+        return getExtensions(Mechanism.class);
+    }
+
+    public Collection<String> getMechanismNames() {
+        return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl/Response.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Response extends StreamElement {
+
+    public Response() {
+        super(Response.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl/Success.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Success extends StreamElement {
+
+
+    public Success() {
+        super(Success.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Authenticate extends StreamElement {
+
+    public Authenticate() {
+        super(Authenticate.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java 🔗

@@ -0,0 +1,30 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import com.google.common.collect.Collections2;
+
+import eu.siacs.conversations.xml.Element;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Authentication extends AuthenticationStreamFeature {
+    public Authentication() {
+        super(Authentication.class);
+    }
+
+    public Collection<Mechanism> getMechanisms() {
+        return getExtensions(Mechanism.class);
+    }
+
+    public Collection<String> getMechanismNames() {
+        return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull);
+    }
+
+    public Inline getInline() {
+        return this.getExtension(Inline.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java 🔗

@@ -0,0 +1,28 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class AuthorizationIdentifier extends Extension {
+
+
+    public AuthorizationIdentifier() {
+        super(AuthorizationIdentifier.class);
+    }
+
+    public Jid get() {
+        final var content = getContent();
+        if ( Strings.isNullOrEmpty(content)) {
+            return null;
+        }
+        try {
+            return Jid.ofEscaped(content);
+        } catch (final IllegalArgumentException e) {
+            return null;
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java 🔗

@@ -0,0 +1,34 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import com.google.common.collect.Collections2;
+
+import eu.siacs.conversations.xml.Element;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.fast.Fast;
+import im.conversations.android.xmpp.model.fast.Mechanism;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+
+@XmlElement
+public class Inline extends Extension {
+
+    public Inline() {
+        super(Inline.class);
+    }
+
+    public Fast getFast() {
+        return this.getExtension(Fast.class);
+    }
+
+    public Collection<String> getFastMechanisms() {
+        final var fast = getFast();
+        final Collection<Mechanism> mechanisms =
+                fast == null ? Collections.emptyList() : fast.getExtensions(Mechanism.class);
+        return Collections2.filter(
+                Collections2.transform(mechanisms, Element::getContent), Objects::nonNull);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Mechanism extends Extension {
+
+    public Mechanism() {
+        super(Mechanism.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Response extends StreamElement {
+
+    public Response() {
+        super(Response.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java 🔗

@@ -0,0 +1,23 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import eu.siacs.conversations.xmpp.Jid;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Success extends StreamElement {
+
+
+    public Success() {
+        super(Success.class);
+    }
+
+    public Jid getAuthorizationIdentifier() {
+        final var id = this.getExtension(AuthorizationIdentifier.class);
+        if (id == null) {
+            return null;
+        }
+        return id.get();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Ack.java 🔗

@@ -0,0 +1,23 @@
+package im.conversations.android.xmpp.model.sm;
+
+import com.google.common.base.Optional;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement(name = "a")
+public class Ack extends StreamElement {
+
+    public Ack() {
+        super(Ack.class);
+    }
+
+    public Ack(final int sequence) {
+        super(Ack.class);
+        this.setAttribute("h", sequence);
+    }
+
+    public Optional<Integer> getHandled() {
+        return this.getOptionalIntAttribute("h");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Enable.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.sm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Enable extends StreamElement {
+
+    public Enable() {
+        super(Enable.class);
+        this.setAttribute("resume", "true");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java 🔗

@@ -0,0 +1,35 @@
+package im.conversations.android.xmpp.model.sm;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Enabled extends StreamElement {
+
+    public Enabled() {
+        super(Enabled.class);
+    }
+
+    public boolean isResume() {
+        return this.getAttributeAsBoolean("resume");
+    }
+
+    public String getLocation() {
+        return this.getAttribute("location");
+    }
+
+    public Optional<String> getResumeId() {
+        final var id = this.getAttribute("id");
+        if (Strings.isNullOrEmpty(id)) {
+            return Optional.absent();
+        }
+        if (isResume()) {
+            return Optional.of(id);
+        } else {
+            return Optional.absent();
+        }
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Failed.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sm;
+
+import com.google.common.base.Optional;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Failed extends StreamElement {
+    public Failed() {
+        super(Failed.class);
+    }
+
+    public Optional<Integer> getHandled() {
+        return this.getOptionalIntAttribute("h");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Request.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement(name = "r")
+public class Request extends StreamElement {
+
+    public Request() {
+        super(Request.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Resume.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.sm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Resume extends StreamElement {
+
+    public Resume() {
+        super(Resume.class);
+    }
+
+    public Resume(final String id, final int sequence) {
+        super(Resume.class);
+        this.setAttribute("previd", id);
+        this.setAttribute("h", sequence);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.sm;
+
+import com.google.common.base.Optional;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Resumed extends StreamElement {
+
+    public Resumed() {
+        super(Resumed.class);
+    }
+
+    public Optional<Integer> getHandled() {
+        return this.getOptionalIntAttribute("h");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.sm;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement(name = "sm")
+public class StreamManagement extends StreamFeature {
+
+    public StreamManagement() {
+        super(StreamManagement.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java 🔗

@@ -0,0 +1,77 @@
+package im.conversations.android.xmpp.model.stanza;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.xml.Element;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.error.Error;
+
+import java.util.Locale;
+
+@XmlElement
+public class Iq extends Stanza {
+
+    public static Iq TIMEOUT = new Iq(Type.TIMEOUT);
+
+    public Iq() {
+        super(Iq.class);
+    }
+
+    public Iq(final Type type) {
+        super(Iq.class);
+        this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
+    }
+
+    // TODO get rid of timeout
+    public enum Type {
+        SET,
+        GET,
+        ERROR,
+        RESULT,
+        TIMEOUT
+    }
+
+    public Type getType() {
+        return Type.valueOf(
+                Strings.nullToEmpty(this.getAttribute("type")).toUpperCase(Locale.ROOT));
+    }
+
+    @Override
+    public boolean isInvalid() {
+        final var id = getId();
+        if (Strings.isNullOrEmpty(id)) {
+            return true;
+        }
+        return super.isInvalid();
+    }
+
+    // Legacy methods that need to be refactored:
+
+    public Element query() {
+        final Element query = findChild("query");
+        if (query != null) {
+            return query;
+        }
+        return addChild("query");
+    }
+
+    public Element query(final String xmlns) {
+        final Element query = query();
+        query.setAttribute("xmlns", xmlns);
+        return query();
+    }
+
+    public Iq generateResponse(final Iq.Type type) {
+        final var packet = new Iq(type);
+        packet.setTo(this.getFrom());
+        packet.setId(this.getId());
+        return packet;
+    }
+
+    public String getErrorCondition() {
+        final Error error = getError();
+        final var condition = error == null ? null : error.getCondition();
+        return condition == null ? null : condition.getName();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/stanza/Message.java 🔗

@@ -0,0 +1,64 @@
+package im.conversations.android.xmpp.model.stanza;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.LocalizedContent;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.jabber.Body;
+
+import java.util.Locale;
+
+@XmlElement
+public class Message extends Stanza {
+
+    public Message() {
+        super(Message.class);
+    }
+
+    public Message(Type type) {
+        this();
+        this.setType(type);
+    }
+
+    public LocalizedContent getBody() {
+        return findInternationalizedChildContentInDefaultNamespace("body");
+    }
+    
+    public Type getType() {
+        final var value = this.getAttribute("type");
+        if (value == null) {
+            return Type.NORMAL;
+        } else {
+            try {
+                return Type.valueOf(value.toUpperCase(Locale.ROOT));
+            } catch (final IllegalArgumentException e) {
+                return null;
+            }
+        }
+    }
+
+    public void setType(final Type type) {
+        if (type == null || type == Type.NORMAL) {
+            this.removeAttribute("type");
+        } else {
+            this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
+        }
+    }
+
+    public void setBody(final String text) {
+        this.addExtension(new Body(text));
+    }
+
+    public void setAxolotlMessage(Element axolotlMessage) {
+        this.children.remove(findChild("body"));
+        this.children.add(0, axolotlMessage);
+    }
+
+    public enum Type {
+        ERROR,
+        NORMAL,
+        GROUPCHAT,
+        HEADLINE,
+        CHAT
+    }
+}

src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.stanza;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
+
+@XmlElement
+public class Presence extends Stanza implements EntityCapabilities {
+
+    public Presence() {
+        super(Presence.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java 🔗

@@ -0,0 +1,74 @@
+package im.conversations.android.xmpp.model.stanza;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.InvalidJid;
+import eu.siacs.conversations.xmpp.Jid;
+
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.StreamElement;
+import im.conversations.android.xmpp.model.error.Error;
+
+public abstract class Stanza extends StreamElement {
+
+    protected Stanza(final Class<? extends Stanza> clazz) {
+        super(clazz);
+    }
+
+    public Jid getTo() {
+        return this.getAttributeAsJid("to");
+    }
+
+    public Jid getFrom() {
+        return this.getAttributeAsJid("from");
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+
+    public void setId(final String id) {
+        this.setAttribute("id", id);
+    }
+
+    public void setFrom(final Jid from) {
+        this.setAttribute("from", from);
+    }
+
+    public void setTo(final Jid to) {
+        this.setAttribute("to", to);
+    }
+
+    public Error getError() {
+        return this.getExtension(Error.class);
+    }
+
+    public boolean isInvalid() {
+        final var to = getTo();
+        final var from = getFrom();
+        if (to instanceof InvalidJid || from instanceof InvalidJid) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean fromServer(final Account account) {
+        final Jid from = getFrom();
+        return from == null
+                || from.equals(account.getDomain())
+                || from.equals(account.getJid().asBareJid())
+                || from.equals(account.getJid());
+    }
+
+    public boolean toServer(final Account account) {
+        final Jid to = getTo();
+        return to == null
+                || to.equals(account.getDomain())
+                || to.equals(account.getJid().asBareJid())
+                || to.equals(account.getJid());
+    }
+
+    public boolean fromAccount(final Account account) {
+        final Jid from = getFrom();
+        return from != null && from.asBareJid().equals(account.getJid().asBareJid());
+    }
+}

src/main/java/im/conversations/android/xmpp/model/streams/Features.java 🔗

@@ -0,0 +1,33 @@
+package im.conversations.android.xmpp.model.streams;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.StreamElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.sm.StreamManagement;
+
+@XmlElement
+public class Features extends StreamElement implements EntityCapabilities {
+    public Features() {
+        super(Features.class);
+    }
+
+    public boolean streamManagement() {
+        return hasStreamFeature(StreamManagement.class);
+    }
+
+    public boolean invite() {
+        return this.hasChild("register", Namespace.INVITE);
+    }
+
+    public boolean clientStateIndication() {
+        return this.hasChild("csi", Namespace.CSI);
+    }
+
+
+    public boolean hasStreamFeature(final Class<? extends StreamFeature> clazz) {
+        return hasExtension(clazz);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.tls;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Proceed extends StreamElement {
+
+    public Proceed() {
+        super(Proceed.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/tls/Required.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.tls;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Required extends Extension {
+    public Required() {
+        super(Required.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java 🔗

@@ -0,0 +1,15 @@
+package im.conversations.android.xmpp.model.tls;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement(name = "starttls")
+public class StartTls extends StreamElement {
+    public StartTls() {
+        super(StartTls.class);
+    }
+
+    public boolean isRequired() {
+        return hasExtension(Required.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.unique;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class OriginId extends Extension {
+
+    public OriginId() {
+        super(OriginId.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.unique;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class StanzaId extends Extension {
+
+    public StanzaId() {
+        super(StanzaId.class);
+    }
+
+    public Jid getBy() {
+        return this.getAttributeAsJid("by");
+    }
+
+    public String getId() {
+        return this.getAttribute("id");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/upload/Get.java 🔗

@@ -0,0 +1,22 @@
+package im.conversations.android.xmpp.model.upload;
+
+import com.google.common.base.Strings;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import okhttp3.HttpUrl;
+
+@XmlElement
+public class Get extends Extension {
+
+    public Get() {
+        super(Get.class);
+    }
+
+    public HttpUrl getUrl() {
+        final var url = this.getAttribute("url");
+        if (Strings.isNullOrEmpty(url)) {
+            return null;
+        }
+        return HttpUrl.parse(url);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/upload/Header.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.upload;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Header extends Extension {
+
+    public Header() {
+        super(Header.class);
+    }
+
+    public String getHeaderName() {
+        return this.getAttribute("name");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/upload/Put.java 🔗

@@ -0,0 +1,27 @@
+package im.conversations.android.xmpp.model.upload;
+
+import com.google.common.base.Strings;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import okhttp3.HttpUrl;
+
+@XmlElement
+public class Put extends Extension {
+
+    public Put() {
+        super(Put.class);
+    }
+
+    public HttpUrl getUrl() {
+        final var url = this.getAttribute("url");
+        if (Strings.isNullOrEmpty(url)) {
+            return null;
+        }
+        return HttpUrl.parse(url);
+    }
+
+    public Collection<Header> getHeaders() {
+        return this.getExtensions(Header.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/upload/Request.java 🔗

@@ -0,0 +1,24 @@
+package im.conversations.android.xmpp.model.upload;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Request extends Extension {
+
+    public Request() {
+        super(Request.class);
+    }
+
+    public void setFilename(String filename) {
+        this.setAttribute("filename", filename);
+    }
+
+    public void setSize(long size) {
+        this.setAttribute("size", size);
+    }
+
+    public void setContentType(String type) {
+        this.setAttribute("content-ype", type);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/upload/Slot.java 🔗

@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.upload;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Slot extends Extension {
+
+    public Slot() {
+        super(Slot.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java 🔗

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.vcard;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "BINVAL")
+public class BinaryValue extends Extension implements ByteContent {
+
+    public BinaryValue() {
+        super(BinaryValue.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java 🔗

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.vcard;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "PHOTO")
+public class Photo extends Extension {
+    public Photo() {
+        super(Photo.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/vcard/VCard.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 = "vCard")
+public class VCard extends Extension {
+
+    public VCard() {
+        super(VCard.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java 🔗

@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.vcard.update;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x")
+public class VCardUpdate extends Extension {
+
+    public VCardUpdate() {
+        super(VCardUpdate.class);
+    }
+
+    public Photo getPhoto() {
+        return this.getExtension(Photo.class);
+    }
+
+    public String getHash() {
+        final var photo = getPhoto();
+        return photo == null ? null : photo.getContent();
+    }
+}

src/main/java/im/conversations/android/xmpp/model/version/Version.java 🔗

@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.version;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import eu.siacs.conversations.xml.Namespace;
+
+@XmlElement(name = "query", namespace = Namespace.VERSION)
+public class Version extends Extension {
+
+    public Version() {
+        super(Version.class);
+    }
+
+    public void setSoftwareName(final String name) {
+        this.addChild("name").setContent(name);
+    }
+
+    public void setVersion(final String version) {
+        this.addChild("version").setContent(version);
+    }
+
+    public void setOs(final String os) {
+        this.addChild("os").setContent(os);
+    }
+}

src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java 🔗

@@ -0,0 +1,90 @@
+package im.conversations.android.xmpp.processor;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+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 im.conversations.android.xmpp.model.stanza.Iq;
+
+public class BindProcessor implements Runnable {
+
+
+    private final XmppConnectionService service;
+    private final Account account;
+
+    public BindProcessor(XmppConnectionService service, Account account) {
+        this.service = service;
+        this.account = account;
+    }
+
+    @Override
+    public void run() {
+        final XmppConnection connection = account.getXmppConnection();
+        service.cancelAvatarFetches(account);
+        final boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
+        final boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, connection.getFeatures().httpUpload(0));
+        if (loggedInSuccessfully || gainedFeature) {
+            service.databaseBackend.updateAccount(account);
+        }
+
+        if (loggedInSuccessfully) {
+            if (!TextUtils.isEmpty(account.getDisplayName())) {
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing");
+                service.publishDisplayName(account);
+            }
+        }
+
+        account.getRoster().clearPresences();
+        synchronized (account.inProgressConferenceJoins) {
+            account.inProgressConferenceJoins.clear();
+        }
+        synchronized (account.inProgressConferencePings) {
+            account.inProgressConferencePings.clear();
+        }
+        service.getJingleConnectionManager().notifyRebound(account);
+        service.getQuickConversationsService().considerSyncBackground(false);
+
+
+        connection.fetchRoster();
+
+        if (connection.getFeatures().bookmarks2()) {
+            service.fetchBookmarks2(account);
+        } else if (!connection.getFeatures().bookmarksConversion()) {
+            service.fetchBookmarks(account);
+        }
+
+        if (connection.getFeatures().mds()) {
+            service.fetchMessageDisplayedSynchronization(account);
+        } else {
+            Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds");
+        }
+        final boolean flexible = connection.getFeatures().flexibleOfflineMessageRetrieval();
+        final boolean catchup = service.getMessageArchiveService().inCatchup(account);
+        final boolean trackOfflineMessageRetrieval;
+        if (flexible && catchup && connection.isMamPreferenceAlways()) {
+            trackOfflineMessageRetrieval = false;
+            connection.sendIqPacket(IqGenerator.purgeOfflineMessages(), (packet) -> {
+                if (packet.getType() == Iq.Type.RESULT) {
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully purged offline messages");
+                }
+            });
+        } else {
+            trackOfflineMessageRetrieval = true;
+        }
+        service.sendPresence(account);
+        connection.trackOfflineMessageRetrieval(trackOfflineMessageRetrieval);
+        if (service.getPushManagementService().available(account)) {
+            service.getPushManagementService().registerPushTokenOnServer(account);
+        }
+        service.connectMultiModeConversations(account);
+        service.syncDirtyContacts(account);
+
+        service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account);
+
+    }
+}

src/playstore/java/eu/siacs/conversations/services/PushManagementService.java 🔗

@@ -15,7 +15,8 @@ import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class PushManagementService {
 
@@ -25,7 +26,7 @@ public class PushManagementService {
         this.mXmppConnectionService = service;
     }
 
-    private static Data findResponseData(IqPacket response) {
+    private static Data findResponseData(Iq response) {
         final Element command = response.findChild("command", Namespace.COMMANDS);
         final Element x = command == null ? null : command.findChild("x", Namespace.DATA);
         return x == null ? null : Data.parse(x);
@@ -35,43 +36,70 @@ public class PushManagementService {
         return Jid.of(mXmppConnectionService.getString(R.string.app_server));
     }
 
-    void registerPushTokenOnServer(final Account account) {
+    public void registerPushTokenOnServer(final Account account) {
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
-        retrieveFcmInstanceToken(token -> {
-            final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
-            final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId);
-            mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> {
-                final Data data = findResponseData(response);
-                if (response.getType() == IqPacket.TYPE.RESULT && data != null) {
-                    try {
-                        String node = data.getValue("node");
-                        String secret = data.getValue("secret");
-                        Jid jid = Jid.of(data.getValue("jid"));
-                        if (node != null && secret != null) {
-                            enablePushOnServer(a, jid, node, secret);
-                        }
-                    } catch (IllegalArgumentException e) {
-                        e.printStackTrace();
-                    }
-                } else {
-                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": failed to enable push. invalid response from app server " + response);
-                }
-            });
-        });
+        retrieveFcmInstanceToken(
+                token -> {
+                    final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
+                    final var packet =
+                            mXmppConnectionService
+                                    .getIqGenerator()
+                                    .pushTokenToAppServer(getAppServer(), token, androidId);
+                    mXmppConnectionService.sendIqPacket(
+                            account,
+                            packet,
+                            (response) -> {
+                                final Data data = findResponseData(response);
+                                if (response.getType() == Iq.Type.RESULT && data != null) {
+                                    final Jid jid;
+                                    try {
+                                        jid = Jid.ofEscaped(data.getValue("jid"));
+                                    } catch (final IllegalArgumentException e) {
+                                        Log.d(
+                                                Config.LOGTAG,
+                                                account.getJid().asBareJid()
+                                                        + ": failed to enable push. invalid jid");
+                                        return;
+                                    }
+                                    final String node = data.getValue("node");
+                                    final String secret = data.getValue("secret");
+                                    if (node != null && secret != null) {
+                                        enablePushOnServer(account, jid, node, secret);
+                                    }
+                                } else {
+                                    Log.d(
+                                            Config.LOGTAG,
+                                            account.getJid().asBareJid()
+                                                    + ": failed to enable push. invalid response from app server "
+                                                    + response);
+                                }
+                            });
+                });
     }
 
-    private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) {
-        final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
-        mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> {
-            if (p.getType() == IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server");
-            } else if (p.getType() == IqPacket.TYPE.ERROR) {
-                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed");
-            }
-        });
+    private void enablePushOnServer(
+            final Account account, final Jid appServer, final String node, final String secret) {
+        final Iq enable =
+                mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
+        mXmppConnectionService.sendIqPacket(
+                account,
+                enable,
+                (p) -> {
+                    if (p.getType() == Iq.Type.RESULT) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": successfully enabled push on server");
+                    } else if (p.getType() == Iq.Type.ERROR) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid() + ": enabling push on server failed");
+                    }
+                });
     }
 
-    private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
+    private void retrieveFcmInstanceToken(
+            final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
         final FirebaseMessaging firebaseMessaging;
         try {
             firebaseMessaging = FirebaseMessaging.getInstance();
@@ -79,26 +107,33 @@ public class PushManagementService {
             Log.d(Config.LOGTAG, "unable to get firebase instance token ", e);
             return;
         }
-        firebaseMessaging.getToken().addOnCompleteListener(task -> {
-            if (!task.isSuccessful()) {
-                Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException());
-            }
-            final String result;
-            try {
-                result = task.getResult();
-            } catch (Exception e) {
-                Log.d(Config.LOGTAG, "unable to get Firebase instance token due to bug in library ", e);
-                return;
-            }
-            if (result != null) {
-                instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result);
-            }
-        });
-
+        firebaseMessaging
+                .getToken()
+                .addOnCompleteListener(
+                        task -> {
+                            if (!task.isSuccessful()) {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        "unable to get Firebase instance token",
+                                        task.getException());
+                            }
+                            final String result;
+                            try {
+                                result = task.getResult();
+                            } catch (Exception e) {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        "unable to get Firebase instance token due to bug in library ",
+                                        e);
+                                return;
+                            }
+                            if (result != null) {
+                                instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result);
+                            }
+                        });
     }
 
-
-    public boolean available(Account account) {
+    public boolean available(final Account account) {
         final XmppConnection connection = account.getXmppConnection();
         return connection != null
                 && connection.getFeatures().sm()
@@ -107,7 +142,9 @@ public class PushManagementService {
     }
 
     private boolean playServicesAvailable() {
-        return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
+        return GoogleApiAvailabilityLight.getInstance()
+                        .isGooglePlayServicesAvailable(mXmppConnectionService)
+                == ConnectionResult.SUCCESS;
     }
 
     public boolean isStub() {

src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java 🔗

@@ -69,7 +69,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.stanzas.IqPacket;
+import im.conversations.android.xmpp.model.stanza.Iq;
 import io.michaelrocks.libphonenumber.android.Phonenumber;
 
 public class QuickConversationsService extends AbstractQuickConversationsService {
@@ -463,15 +463,15 @@ public class QuickConversationsService extends AbstractQuickConversationsService
         for (final PhoneNumberContact c : contacts.values()) {
             entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber()));
         }
-        final IqPacket query = new IqPacket(IqPacket.TYPE.GET);
+        final Iq query = new Iq(Iq.Type.GET);
         query.setTo(syncServer);
         final Element book = new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries);
         final String statusQuo = Entry.statusQuo(contacts.values(), account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
         book.setAttribute("ver", statusQuo);
         query.addChild(book);
         mLastSyncAttempt = Attempt.create(hash);
-        service.sendIqPacket(account, query, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        service.sendIqPacket(account, query, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION);
                 if (phoneBook != null) {
                     final List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
@@ -498,7 +498,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
                 } else {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": phone number contact list remains unchanged");
                 }
-            } else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+            } else if (response.getType() == Iq.Type.TIMEOUT) {
                 mLastSyncAttempt = Attempt.NULL;
             } else {
                 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to sync contact list with api server");