Merge branch 'master' of https://codeberg.org/iNPUTmice/Conversations

Stephen Paul Weber created

* 'master' of https://codeberg.org/iNPUTmice/Conversations: (192 commits)
  version bump to 2.16.6
  Translated using Weblate (Albanian)
  Translated using Weblate (French)
  Translated using Weblate (Chinese (Simplified))
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Polish)
  Translated using Weblate (Dutch)
  Translated using Weblate (Galician)
  Translated using Weblate (French)
  Translated using Weblate (German)
  Translated using Weblate (Albanian)
  Translated using Weblate (Galician)
  Translated using Weblate (German)
  Translated using Weblate (Albanian)
  Translated using Weblate (Italian)
  Translated using Weblate (Chinese (Simplified))
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Polish)
  Translated using Weblate (Dutch)
  Translated using Weblate (Ukrainian)
  ...

Change summary

CHANGELOG.md                                                                                                   |   34 
build.gradle                                                                                                   |   28 
fastlane/metadata/android/de-DE/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/de-DE/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/de-DE/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/de-DE/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/de-DE/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/en-US/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/en-US/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/en-US/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/en-US/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/en-US/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/en-US/changelogs/4211704.txt                                                         |    3 
fastlane/metadata/android/es-ES/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/es-ES/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/es-ES/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/es-ES/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/es-ES/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/fr-FR/changelogs/349.txt                                                             |    4 
fastlane/metadata/android/gl-ES/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/gl-ES/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/gl-ES/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/gl-ES/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/gl-ES/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/it-IT/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/it-IT/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/it-IT/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/it-IT/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/it-IT/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/pl-PL/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/pl-PL/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/pl-PL/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/pl-PL/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/pl-PL/changelogs/4211604.txt                                                         |    1 
fastlane/metadata/android/sq/changelogs/4211104.txt                                                            |    3 
fastlane/metadata/android/sq/changelogs/4211204.txt                                                            |    2 
fastlane/metadata/android/sq/changelogs/4211304.txt                                                            |    1 
fastlane/metadata/android/sq/changelogs/4211404.txt                                                            |    2 
fastlane/metadata/android/sq/changelogs/4211604.txt                                                            |    1 
fastlane/metadata/android/uk/changelogs/367.txt                                                                |    2 
fastlane/metadata/android/uk/changelogs/395.txt                                                                |    2 
fastlane/metadata/android/uk/changelogs/402.txt                                                                |    2 
fastlane/metadata/android/uk/changelogs/42041.txt                                                              |    2 
fastlane/metadata/android/uk/changelogs/4211104.txt                                                            |    3 
fastlane/metadata/android/uk/changelogs/4211204.txt                                                            |    2 
fastlane/metadata/android/uk/changelogs/4211304.txt                                                            |    1 
fastlane/metadata/android/uk/changelogs/4211404.txt                                                            |    2 
fastlane/metadata/android/uk/changelogs/4211604.txt                                                            |    1 
fastlane/metadata/android/zh-CN/changelogs/42037.txt                                                           |    6 
fastlane/metadata/android/zh-CN/changelogs/4210404.txt                                                         |    2 
fastlane/metadata/android/zh-CN/changelogs/4210904.txt                                                         |    2 
fastlane/metadata/android/zh-CN/changelogs/4211104.txt                                                         |    3 
fastlane/metadata/android/zh-CN/changelogs/4211204.txt                                                         |    2 
fastlane/metadata/android/zh-CN/changelogs/4211304.txt                                                         |    1 
fastlane/metadata/android/zh-CN/changelogs/4211404.txt                                                         |    2 
fastlane/metadata/android/zh-CN/changelogs/4211604.txt                                                         |    1 
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/cheogram/java/com/cheogram/android/BobTransfer.java                                                        |    9 
src/cheogram/java/com/cheogram/android/FinishOnboarding.java                                                   |   15 
src/cheogram/res/xml/cache_paths.xml                                                                           |    5 
src/conversations/fastlane/metadata/android/es-ES/short_description.txt                                        |    2 
src/conversations/fastlane/metadata/android/fr-FR/full_description.txt                                         |   21 
src/conversations/fastlane/metadata/android/fr-FR/short_description.txt                                        |    2 
src/conversations/fastlane/metadata/android/ja-JP/short_description.txt                                        |    2 
src/conversations/fastlane/metadata/android/nl-NL/full_description.txt                                         |   39 
src/conversations/fastlane/metadata/android/nl-NL/short_description.txt                                        |    1 
src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java                                    |    9 
src/conversations/res/values-et/strings.xml                                                                    |    2 
src/conversations/res/values-fr/strings.xml                                                                    |    5 
src/conversations/res/values-ja/strings.xml                                                                    |    9 
src/conversations/res/values-pt-rBR/strings.xml                                                                |    4 
src/conversations/res/values-sv/strings.xml                                                                    |    4 
src/conversations/res/values-zh-rCN/strings.xml                                                                |    2 
src/free/java/eu/siacs/conversations/services/PushManagementService.java                                       |   11 
src/main/AndroidManifest.xml                                                                                   |    4 
src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java                                          |   35 
src/main/java/de/gultsch/minidns/ResolverResult.java                                                           |  178 
src/main/java/eu/siacs/conversations/AppSettings.java                                                          |   26 
src/main/java/eu/siacs/conversations/Conversations.java                                                        |    9 
src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java                                            |   74 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java                                        |  375 
src/main/java/eu/siacs/conversations/entities/Conversation.java                                                |   44 
src/main/java/eu/siacs/conversations/entities/MucOptions.java                                                  |   13 
src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java                                      |   36 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                                                |  198 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java                                           |  101 
src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java                                          |   39 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java                                           |   56 
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                                                |   19 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java                                              |   57 
src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java                                    |   32 
src/main/java/eu/siacs/conversations/services/CallIntegration.java                                             |   34 
src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java                            |   15 
src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java                                     |   72 
src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java                                      |    2 
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/ConferenceDetailsActivity.java                                         |   60 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java                                              |    7 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java                                             |   15 
src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java                                     |   32 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java                                               |  757 
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java                                                 |    7 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                                                |   70 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java                                         |  722 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java                                                |   19 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                                                      |   11 
src/main/java/eu/siacs/conversations/ui/adapter/:w                                                             | 1701 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java                                       |    4 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java                                              |   10 
src/main/java/eu/siacs/conversations/ui/fragment/settings/AttachmentsSettingsFragment.java                     |   18 
src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java                          |   14 
src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java                   |    7 
src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java                        |   20 
src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java                          |   33 
src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java                                             |  103 
src/main/java/eu/siacs/conversations/utils/AccountUtils.java                                                   |   12 
src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java                                               |   69 
src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java                                                |  125 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                                                      |    1 
src/main/java/eu/siacs/conversations/utils/Resolver.java                                                       |  529 
src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java                                            |   23 
src/main/java/eu/siacs/conversations/xml/Element.java                                                          |   55 
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                                                  |  500 
src/main/java/eu/siacs/conversations/xmpp/forms/Data.java                                                      |    4 
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                                      |  201 
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                                           |   98 
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/AuthenticationFailure.java                                   |   18 
src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java                                   |   13 
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                                              |   28 
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/Tag.java                                               |   17 
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                                               |   19 
src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java                                            |   11 
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/SaslError.java                                          |   89 
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                                      |   19 
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/Device.java                                            |   17 
src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java                                           |   12 
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/Software.java                                          |   17 
src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java                                           |   23 
src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java                                         |   25 
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/main/res/drawable/ic_new_releases_24dp.xml                                                                 |   13 
src/main/res/layout/activity_edit_account.xml                                                                  |  120 
src/main/res/layout/activity_muc_details.xml                                                                   |    3 
src/main/res/layout/activity_publish_profile_picture.xml                                                       |    3 
src/main/res/layout/activity_uri_handler.xml                                                                   |    6 
src/main/res/layout/item_message_content.xml                                                                   |    1 
src/main/res/values-ar/strings.xml                                                                             |    2 
src/main/res/values-bg/strings.xml                                                                             |    2 
src/main/res/values-bn-rIN/strings.xml                                                                         |   16 
src/main/res/values-ca/strings.xml                                                                             |    3 
src/main/res/values-cs/strings.xml                                                                             |  149 
src/main/res/values-da-rDK/strings.xml                                                                         |    2 
src/main/res/values-de/strings.xml                                                                             |   22 
src/main/res/values-el/strings.xml                                                                             |    2 
src/main/res/values-es/strings.xml                                                                             |    5 
src/main/res/values-et/strings.xml                                                                             |    2 
src/main/res/values-eu/strings.xml                                                                             |    2 
src/main/res/values-fa-rIR/strings.xml                                                                         |    2 
src/main/res/values-fi/strings.xml                                                                             |    2 
src/main/res/values-fr/strings.xml                                                                             |  242 
src/main/res/values-gl/strings.xml                                                                             |   24 
src/main/res/values-hu/strings.xml                                                                             |    2 
src/main/res/values-it/strings.xml                                                                             |   29 
src/main/res/values-ja/strings.xml                                                                             |   38 
src/main/res/values-nb-rNO/strings.xml                                                                         |    2 
src/main/res/values-nl/strings.xml                                                                             |  391 
src/main/res/values-pl/strings.xml                                                                             |   45 
src/main/res/values-pt-rBR/strings.xml                                                                         |   55 
src/main/res/values-ro-rRO/strings.xml                                                                         |   19 
src/main/res/values-ru/strings.xml                                                                             |   66 
src/main/res/values-sq-rAL/strings.xml                                                                         |   25 
src/main/res/values-sv/strings.xml                                                                             |    2 
src/main/res/values-szl/strings.xml                                                                            |    2 
src/main/res/values-tr-rTR/strings.xml                                                                         |    2 
src/main/res/values-uk/strings.xml                                                                             |   70 
src/main/res/values-vi/strings.xml                                                                             |    2 
src/main/res/values-zh-rCN/strings.xml                                                                         |  136 
src/main/res/values-zh-rTW/strings.xml                                                                         |    2 
src/main/res/values/arrays.xml                                                                                 |   15 
src/main/res/values/dimens.xml                                                                                 |    2 
src/main/res/values/strings.xml                                                                                |   22 
src/main/res/xml/preferences_attachments.xml                                                                   |    2 
src/playstore/java/eu/siacs/conversations/services/PushManagementService.java                                  |  143 
src/quicksy/fastlane/metadata/android/es-ES/full_description.txt                                               |   16 
src/quicksy/fastlane/metadata/android/es-ES/short_description.txt                                              |    2 
src/quicksy/fastlane/metadata/android/fr-FR/full_description.txt                                               |   14 
src/quicksy/fastlane/metadata/android/fr-FR/short_description.txt                                              |    1 
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java                                |   68 
src/quicksy/res/values-ar/strings.xml                                                                          |    6 
src/quicksy/res/values-et/strings.xml                                                                          |    2 
src/quicksy/res/values-gl/strings.xml                                                                          |    2 
src/quicksy/res/values-nl/strings.xml                                                                          |    4 
src/quicksy/res/values-ro-rRO/strings.xml                                                                      |    4 
src/quicksy/res/values-zh-rCN/strings.xml                                                                      |    6 
473 files changed, 12,198 insertions(+), 3,801 deletions(-)

Detailed changes

CHANGELOG.md 🔗

@@ -1,5 +1,39 @@
 # Changelog
 
+### Version 2.16.6
+
+* Offer higher automatic file accept values
+* Provide more information in 'Server info'
+* Various bug fixes
+
+### Version 2.16.5
+
+* Minor bug fixes
+
+### Version 2.16.4
+
+* Fix minor regression introduced in 2.16.3
+
+### Version 2.16.3
+
+* exclude older Oppo devices from call integration
+* various bug fixes
+
+### Version 2.16.2
+
+* Run Backup as foreground service to prevent process being stopped after 10 minutes
+
+### Version 2.16.1
+
+* Fix call getting un-muted when switching output devices
+* Exclude all Umidigi devices from call integration
+
+### Version 2.16.0
+
+* Schedule regular backups
+* Exclude all realme devices up to Android 11 from call integration
+* Minor UI (message bubble) improvements
+
 ### Version 2.15.3
 
 * fix call integration on some Android 14 devices

build.gradle 🔗

@@ -6,7 +6,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.3.1'
+        classpath 'com.android.tools.build:gradle:8.3.2'
     }
 }
 
@@ -50,9 +50,13 @@ dependencies {
     implementation "androidx.core:core:1.10.1"
     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:23.4.1') {
+    playstoreImplementation('com.google.firebase:firebase-messaging:24.0.0') {
         exclude group: 'com.google.firebase', module: 'firebase-core'
         exclude group: 'com.google.firebase', module: 'firebase-analytics'
         exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ -60,24 +64,22 @@ dependencies {
     cheogramPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
     cheogramPlaystoreImplementation 'com.github.singpolyma:play-licensing:1c637ea03c'
     conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
-    quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2'
+    quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0'
     implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1'
     implementation("com.github.CanHub:Android-Image-Cropper:2.0.0")
-    implementation 'androidx.appcompat:appcompat:1.6.1'
+    implementation 'androidx.appcompat:appcompat:1.7.0'
     implementation 'androidx.exifinterface:exifinterface:1.3.7'
     implementation 'androidx.cardview:cardview:1.0.0'
     implementation "androidx.preference:preference:1.2.1"
     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-    implementation 'com.google.android.material:material:1.11.0'
+    implementation 'com.google.android.material:material:1.12.0'
     implementation 'androidx.work:work-runtime:2.9.0'
 
     implementation "androidx.emoji2:emoji2:1.4.0"
     freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
 
-    implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
-    //zxing stopped supporting Java 7 so we have to stick with 3.3.3
-    //https://github.com/zxing/zxing/issues/1170
-    implementation 'com.google.zxing:core:3.3.3'
+    implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1'
+    implementation 'com.google.zxing:core:3.5.3'
     implementation 'org.minidns:minidns-hla:1.0.5'
     implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
     implementation 'org.whispersystems:signal-protocol-java:2.6.2'
@@ -97,12 +99,12 @@ dependencies {
     implementation 'me.drakeet.support:toastcompat:1.1.0'
     implementation "com.leinardi.android:speed-dial:3.3.0"
 
-    implementation "com.squareup.retrofit2:retrofit:2.9.0"
-    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
+    implementation "com.squareup.retrofit2:retrofit:2.11.0"
+    implementation "com.squareup.retrofit2:converter-gson:2.11.0"
     implementation "com.squareup.okhttp3:okhttp:4.12.0"
 
     implementation 'com.google.guava:guava:32.1.3-android'
-    implementation 'io.michaelrocks:libphonenumber-android:8.13.28'
+    implementation 'io.michaelrocks:libphonenumber-android:8.13.35'
     implementation 'im.conversations.webrtc:webrtc-android:119.0.1'
     implementation 'io.github.nishkarsh:android-permissions:2.1.6'
     implementation 'androidx.recyclerview:recyclerview:1.1.0'
@@ -299,7 +301,7 @@ android {
     }
     packagingOptions {
         resources {
-            excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF']
+            excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF']
         }
     }
     lint {

fastlane/metadata/android/fr-FR/changelogs/349.txt 🔗

@@ -0,0 +1,4 @@
+* Introduction d'un paramètre expert pour faire la découverte de salons sur le serveur local au lieu de search.jabber.network
+* Active les coches de délivrance par défaut et supprimer le paramètre
+* Active ‘Le bouton Envoyer indique l'état’ par défaut et supprimer le paramètre
+* Déplacer les paramètres du service de sauvegarde et de premier plan vers l'écran principal

fastlane/metadata/android/uk/changelogs/367.txt 🔗

@@ -1,2 +1,2 @@
-* Виправлено вибір піктограми користувача на деяких пристроях з Android 10
+* Виправлено вибір аватара на деяких пристроях з Android 10
 * Виправлення обміну файлами для великих файлів

fastlane/metadata/android/uk/changelogs/395.txt 🔗

@@ -1,3 +1,3 @@
-* Додано «Повернутися до чату» на екрані звукового виклику
+* Додано «Повернутися до чату» на екрані голосового виклику
 * Удосконалено комбінації клавіш
 * Виправлення помилок

fastlane/metadata/android/uk/changelogs/402.txt 🔗

@@ -1,3 +1,3 @@
 * Просте створення запрошень на серверах з підтримкою запрошень
 * Перегляд файлів GIF, отриманих з Movim
-* Піктограми користувачів зберігаються у кеші
+* Аватари зберігаються у кеші

fastlane/metadata/android/uk/changelogs/42041.txt 🔗

@@ -1,5 +1,5 @@
 * Реалізація Extensible SASL Profile, Bind 2.0 і Fast для швидшого повторного з'єднання
 * Реалізація Channel Binding
 * Додано можливість перемикатися з голосового на відеовиклик
-* Додано можливість видаляти свою піктограму користувача
+* Додано можливість видаляти свій аватар
 * Додано сповіщення про пропущені виклики

fastlane/metadata/android/uk/changelogs/4211104.txt 🔗

@@ -0,0 +1,3 @@
+* Планування регулярного резервного копіювання
+* Виключення всіх пристроїв Realme до Android 11 з інтеграції викликів
+* Незначні покращення інтерфейсу повідомлень

fastlane/metadata/android/uk/changelogs/4211204.txt 🔗

@@ -0,0 +1,2 @@
+* Виправлено ввімкнення звуку виклику при перемиканні пристроїв виводу
+* Виключення всіх пристроїв Umidigi з інтеграції викликів

fastlane/metadata/android/zh-CN/changelogs/42037.txt 🔗

@@ -1,11 +1,11 @@
-版本2.10.9
+版本 2.10.9
 * 进行音视频通话时请求蓝牙权限(如果您不使用蓝牙耳机可以拒绝)
 * 修复呼叫 Movim 时的错误
-* 修复群组聊天的显示错误头像的问题
+* 修复群聊显示错误头像的问题
 * 始终要求选择退出电池优化
 * 在“x 个已连接账号”通知上设置仅本地标志
 * 修复与 Google 地图分享位置插件的交互
 * 移除有关服务器费用的脚注
 * 将文件存储在适合 Android 11 的位置
 * 网络切换后尝试重新连接通话
-* 在来电屏幕中显示来电者JID和帐户JID
+* 在来电屏幕中显示来电者 JID 和账号JID

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/cheogram/java/com/cheogram/android/BobTransfer.java 🔗

@@ -30,7 +30,8 @@ import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class BobTransfer implements Transferable {
 	protected int status = Transferable.STATUS_OFFER;
@@ -93,13 +94,13 @@ public class BobTransfer implements Transferable {
          attempts.put(uri, System.currentTimeMillis());
 			changeStatus(Transferable.STATUS_DOWNLOADING);
 
-			IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+			final var request = new Iq(Iq.Type.GET);
 			request.setTo(to);
 			final Element dataq = request.addChild("data", "urn:xmpp:bob");
 			dataq.setAttribute("cid", uri.getSchemeSpecificPart());
-			xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> {
+			xmppConnectionService.sendIqPacket(account, request, (packet) -> {
 				final Element data = packet.findChild("data", "urn:xmpp:bob");
-				if (packet.getType() == IqPacket.TYPE.ERROR || data == null) {
+				if (packet.getType() == Iq.Type.ERROR || data == null) {
 					Log.d(Config.LOGTAG, "BobTransfer failed: " + packet);
 					finish(null);
 				} else {

src/cheogram/java/com/cheogram/android/FinishOnboarding.java 🔗

@@ -16,7 +16,8 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class FinishOnboarding {
 	private static final AtomicBoolean WORKING = new AtomicBoolean(false);
@@ -32,14 +33,14 @@ public class FinishOnboarding {
 	public static void finish(final XmppConnectionService xmppConnectionService, final XmppActivity activity, final Account onboardAccount, final Account newAccount) {
 		if (!WORKING.compareAndSet(false, true)) return;
 
-		final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+		final var packet = new Iq(Iq.Type.SET);
 		packet.setTo(Jid.of("cheogram.com"));
 		final Element c = packet.addChild("command", Namespace.COMMANDS);
 		c.setAttribute("node", "change jabber id");
 		c.setAttribute("action", "execute");
 
 		Log.d(Config.LOGTAG, "" + packet);
-		xmppConnectionService.sendIqPacket(onboardAccount, packet, (a, iq) -> {
+		xmppConnectionService.sendIqPacket(onboardAccount, packet, (iq) -> {
 			Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
 			if (command == null) {
 				Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq);
@@ -62,15 +63,15 @@ public class FinishOnboarding {
 			iq.setAttribute("type", "set");
 			iq.removeAttribute("from");
 			iq.removeAttribute("id");
-			xmppConnectionService.sendIqPacket(a, iq, (a2, iq2) -> {
+			xmppConnectionService.sendIqPacket(onboardAccount, iq, (iq2) -> {
 				Element command2 = iq2.findChild("command", "http://jabber.org/protocol/commands");
 				if (command2 != null && command2.getAttribute("status") != null && command2.getAttribute("status").equals("completed")) {
-					final IqPacket regPacket = new IqPacket(IqPacket.TYPE.SET);
+					final var regPacket = new Iq(Iq.Type.SET);
 					regPacket.setTo(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"));
 					final Element c2 = regPacket.addChild("command", Namespace.COMMANDS);
 					c2.setAttribute("node", "jabber:iq:register");
 					c2.setAttribute("action", "execute");
-					xmppConnectionService.sendIqPacket(newAccount, regPacket, (a3, iq3) -> {
+					xmppConnectionService.sendIqPacket(newAccount, regPacket, (iq3) -> {
 						Element command3 = iq3.findChild("command", "http://jabber.org/protocol/commands");
 						if (command3 == null) {
 							Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3);
@@ -93,7 +94,7 @@ public class FinishOnboarding {
 						iq3.setAttribute("type", "set");
 						iq3.removeAttribute("from");
 						iq3.removeAttribute("id");
-						xmppConnectionService.sendIqPacket(newAccount, iq3, (a4, iq4) -> {
+						xmppConnectionService.sendIqPacket(newAccount, iq3, (iq4) -> {
 							Element command4 = iq4.findChild("command", "http://jabber.org/protocol/commands");
 							if (command4 != null && command4.getAttribute("status") != null && command4.getAttribute("status").equals("completed")) {
 								xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()), true);

src/cheogram/res/xml/cache_paths.xml 🔗

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+   <cache-path name="cache_media" path="cache/media"/>
+   <cache-path name="cache_avatars" path="cache/avatars"/>
+</paths>

src/conversations/fastlane/metadata/android/fr-FR/full_description.txt 🔗

@@ -1,4 +1,4 @@
-Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout-en-bout.
+Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout en bout.
 
 Principes de conception :
 
@@ -8,6 +8,7 @@ Principes de conception :
 * Nécessiter le moins de permissions possible
 
 Fonctionnalités :
+
 * Chiffrement de bout-en-bout avec au choix, <a href="http://conversations.im/omemo/">OMEMO</a> ou <a href="http://openpgp.org/about/">OpenPGP</a>
 * Envoi et réception d'images
 * Appels audio et vidéo chiffrés (DTLS-SRTP)
@@ -27,12 +28,12 @@ Conversations fonctionne avec n'importe quel serveur XMPP. Cependant XMPP est un
 
 Ces XEP sont actuellement :
 
-* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT).
-* XEP-0163: Personal Eventing Protocol pour les avatars
-* XEP-0191: Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts.
-* XEP-0198: Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente.
-* XEP-0280: Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation.
-* XEP-0237: Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité.
-* XEP-0313: Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne.
-* XEP-0352: Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants.
-* XEP-0363: HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur.
+* XEP-0065 : SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT).
+* XEP-0163 : Personal Eventing Protocol pour les avatars
+* XEP-0191 : Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts.
+* XEP-0198 : Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente.
+* XEP-0280 : Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation.
+* XEP-0237 : Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité.
+* XEP-0313 : Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne.
+* XEP-0352 : Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants.
+* XEP-0363 : HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur.

src/conversations/fastlane/metadata/android/nl-NL/full_description.txt 🔗

@@ -0,0 +1,39 @@
+Eenvoudig te gebruiken, betrouwbaar, batterijvriendelijk. Met ingebouwde ondersteuning voor afbeeldingen, groepschats en e2e-codering.
+
+Ontwerpprincipes:
+
+* Fraai van uiterlijk en gemakkelijk te gebruiken zonder opoffering van de veiligheid of privacy
+* Gebaseerd op bestaande, gevestigde protocollen
+* Vereist geen Google-account of specifiek Google Cloud Messaging (GCM)
+* Verlangt een minimum aan rechten
+
+Functies:
+
+* End-to-end-codering met <a href="http://conversations.im/omemo/">OMEMO</a> of <a href="http://openpgp.org/about/">OpenPGP</a>
+* Afbeeldingen verzenden en ontvangen
+* Versleutelde audio- en videogesprekken (DTLS-SRTP)
+* Intuïtieve gebruikersinterface volgens de richtlijnen van Android Design
+* Afbeeldingen / avatars voor jouw contacten
+* Synchronisatie met desktopclient
+* Conferenties (met ondersteuning voor bladwijzers)
+* Adresboekintegratie
+* Meerdere accounts / uniform Postvak In
+* Zeer lage impact op de levensduur van de batterij
+
+Conversations maakt het heel gemakkelijk om een account aan te maken op de gratis conversations.im-server. Conversations werkt daarbij ook met elke andere XMPP-server. Veel XMPP-servers worden beheerd door vrijwilligers en zijn gratis.
+
+XMPP-functies:
+
+Conversations werkt met elke bestaande XMPP-server. XMPP is echter een uitbreidbaar protocol. Deze extensies zijn ook gestandaardiseerd in zogenaamde XEP's. Conversations ondersteunt een aantal daarvan om de algehele gebruikerservaring te verbeteren. De kans bestaat dat jouw huidige XMPP-server deze extensies niet ondersteunt. Om het meeste uit gesprekken te halen, kun je overwegen om over te schakelen naar een XMPP-server die dat wel doet of - nog beter - je eigen XMPP-server voor jou en je vrienden te gebruiken.
+
+Deze XEP's zijn - vanaf nu:
+
+* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Wordt gebruikt om bestanden over te dragen als beide partijen achter een firewall zitten (NAT).
+* XEP-0163: Personal Eventing Protocol for avatars
+* XEP-0191: Blocking command laat je spammers op de zwarte lijst zetten of contacten blokkeren zonder ze uit je selectie te verwijderen.
+* XEP-0198: Stream Management stelt XMPP in staat om kleine netwerkuitval en veranderingen van de onderliggende TCP-verbinding te overleven.
+* XEP-0280: Message Carbons synchroniseert automatisch de berichten die je naar je desktopclient verzendt en stelt je zo in staat om, binnen één gesprek, naadloos over te schakelen van je mobiele client naar je desktopclient en terug.
+* XEP-0237: Roster Versioning om bandbreedte te besparen op slechte mobiele verbindingen
+* XEP-0313: Message Archive Management synchronisatie van berichtgeschiedenis met de server. Verzamelt berichten die zijn verzonden terwijl Conversations offline was.
+* XEP-0352: Client State Indication laat de server weten of Conversations al dan niet op de achtergrond actief is. Hiermee kan de server bandbreedte besparen door onbelangrijke pakketten achter te houden.
+* XEP-0363: HTTP File Upload stelt je in staat om bestanden te delen tijdens conferenties en met offline contacten. Vereist een extra component op je server.

src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java 🔗

@@ -109,7 +109,6 @@ public class ManageAccountActivity extends XmppActivity
         registerForContextMenu(binding.accountList);
     }
 
-
     @Override
     public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
         if (selectedAccount != null) {
@@ -352,8 +351,14 @@ public class ManageAccountActivity extends XmppActivity
         }
     }
 
-    private void disableAccount(Account account) {
+    private void disableAccount(final Account account) {
         account.setOption(Account.OPTION_DISABLED, true);
+        if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": quick start disabled. account will regain this capability on the next connect");
+        }
         if (!xmppConnectionService.updateAccount(account)) {
             Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
         }

src/conversations/res/values-fr/strings.xml 🔗

@@ -3,7 +3,8 @@
     <string name="pick_a_server">Choisissez votre fournisseur XMPP</string>
     <string name="use_conversations.im">Utiliser conversations.im</string>
     <string name="create_new_account">Créer un nouveau compte</string>
-    <string name="do_you_have_an_account">Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP.</string>
+    <string name="do_you_have_an_account">Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.
+\nRemarque : Certains fournisseurs mail proposent également des comptes XMPP.</string>
     <string name="server_select_text">XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser cette application avec n’importe quel serveur XMPP de votre choix.
 \nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations.</string>
     <string name="magic_create_text_on_x">Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète.</string>
@@ -11,7 +12,7 @@
     <string name="your_server_invitation">Votre invitation au serveur</string>
     <string name="improperly_formatted_provisioning">Code de provisionnement mal formaté</string>
     <string name="tap_share_button_send_invite">Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s.</string>
-    <string name="if_contact_is_nearby_use_qr">Si vos contacts sont à proximité, ils peuvent aussi scanner le code ci-dessous pour accepter votre invitation.</string>
+    <string name="if_contact_is_nearby_use_qr">Si votre contact se trouve près de vous, il peut aussi scanner le code ci-dessous pour accepter votre invitation.</string>
     <string name="easy_invite_share_text">Rejoignez %1$s et discutez avec moi : %2$s</string>
     <string name="share_invite_with">Partager une invitation avec …</string>
 </resources>

src/conversations/res/values-ja/strings.xml 🔗

@@ -2,10 +2,11 @@
 <resources>
     <string name="pick_a_server">XMPP プロバイダーを選択してください</string>
     <string name="use_conversations.im">conversations.im を利用する</string>
-    <string name="create_new_account">新規アカウントを作成</string>
-    <string name="do_you_have_an_account">XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。</string>
-    <string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このアプリを使用することができます。
-\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。</string>
+    <string name="create_new_account">アカウントを新規作成</string>
+    <string name="do_you_have_an_account">XMPP アカウントをお持ちですか? 既に他の XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はアカウントをお持ちです。初めての方は、今すぐ XMPP アカウントを新規作成できます。
+\nヒント: いくつかのメールプロバイダーは XMPP アカウントも提供しています。</string>
+    <string name="server_select_text">XMPP は、プロバイダーに依存しないインスタントメッセージのネットワークです。どの XMPP サーバーでもこのアプリを使用できます。
+\nConversations に最適化された conversations.im で簡単にアカウントを作成することもできます。</string>
     <string name="magic_create_text_on_x">%1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
     <string name="magic_create_text_fixed">%1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。</string>
     <string name="your_server_invitation">サーバーの招待</string>

src/conversations/res/values-pt-rBR/strings.xml 🔗

@@ -4,8 +4,8 @@
     <string name="use_conversations.im">Usar o conversations.im</string>
     <string name="create_new_account">Criar uma nova conta</string>
     <string name="do_you_have_an_account">Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP.</string>
-    <string name="server_select_text">O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.
-\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations.</string>
+    <string name="server_select_text">XMPP é uma rede de mensagens instantâneas independente de provedor. Você pode usar este aplicativo com qualquer servidor XMPP de sua escolha.
+\nNo entanto, para sua comodidade, facilitamos criar uma conta em conversas.im; um provedor especificamente adequado para uso com Conversations.</string>
     <string name="magic_create_text_on_x">Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
     <string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
     <string name="your_server_invitation">Seu convite do servidor</string>

src/conversations/res/values-sv/strings.xml 🔗

@@ -12,8 +12,8 @@
     <string name="share_invite_with">Dela inbjudan med…</string>
     <string name="magic_create_text_fixed">Du har blivit inbjuden till %1$s. Ett användarnamn har redan valts åt dig. Vi guidar dig genom processen för att skapa ett konto.
 \nDu kommer att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress.</string>
-    <string name="server_select_text">XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här klienten med vilken XMPP-server du än väljer.
-\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för användning med Conversations.</string>
+    <string name="server_select_text">XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här appen med en valfri XMPP-server som du själv väljer.
+\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för appen Conversations.</string>
     <string name="magic_create_text_on_x">Du har blivit inbjuden till %1$s. Vi guidar dig genom processen för att skapa ett konto.
 \nNär du väljer %1$s som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress.</string>
 </resources>

src/conversations/res/values-zh-rCN/strings.xml 🔗

@@ -13,7 +13,7 @@
 \n向其他 XMPP 用户提供您的完整地址,就能和对方交流。</string>
     <string name="your_server_invitation">您的服务器邀请</string>
     <string name="improperly_formatted_provisioning">配置代码格式不正确</string>
-    <string name="tap_share_button_send_invite">轻击分享按钮,向您的联系人发送加入 %1$s 的邀请。</string>
+    <string name="tap_share_button_send_invite">点击分享按钮,向您的联系人发送加入 %1$s 的邀请。</string>
     <string name="if_contact_is_nearby_use_qr">如果您的联系人在附近,对方也可以扫描下方二维码接受邀请。</string>
     <string name="easy_invite_share_text">加入 %1$s 和我聊天:%2$s</string>
     <string name="share_invite_with">分享邀请至…</string>

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/AndroidManifest.xml 🔗

@@ -142,10 +142,6 @@
             </intent-filter>
         </service>
 
-        <receiver
-            android:name=".receiver.WorkManagerEventReceiver"
-            android:exported="false" />
-
         <receiver
             android:name=".receiver.SystemEventReceiver"
             android:exported="false">

src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java 🔗

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-2022 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package de.gultsch.minidns;
+
+import org.minidns.MiniDnsException;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
+
+import java.io.Serial;
+
+public class ResolutionUnsuccessfulException extends MiniDnsException {
+
+    /**
+     * 
+     */
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public final Question question;
+    public final RESPONSE_CODE responseCode;
+
+    public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) {
+        super("Asking for " + question + " yielded an error response " + responseCode);
+        this.question = question;
+        this.responseCode = responseCode;
+    }
+}

src/main/java/de/gultsch/minidns/ResolverResult.java 🔗

@@ -0,0 +1,178 @@
+/*
+ * Copyright 2015-2022 the original author or authors
+ *
+ * This software is licensed under the Apache License, Version 2.0,
+ * the GNU Lesser General Public License version 2 or later ("LGPL")
+ * and the WTFPL.
+ * You may choose either license to govern your use of this software only
+ * upon the condition that you accept all of the terms of either
+ * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
+ */
+package de.gultsch.minidns;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.minidns.MiniDnsException;
+import org.minidns.MiniDnsException.NullResultException;
+import org.minidns.dnsmessage.DnsMessage;
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
+import org.minidns.dnssec.DnssecResultNotAuthenticException;
+import org.minidns.dnssec.DnssecUnverifiedReason;
+import org.minidns.record.Data;
+
+public class ResolverResult<D extends Data> {
+
+    protected final Question question;
+    private final RESPONSE_CODE responseCode;
+    private final Set<D> data;
+    private final boolean isAuthenticData;
+    protected final Set<DnssecUnverifiedReason> unverifiedReasons;
+    protected final DnsMessage answer;
+    protected final DnsQueryResult result;
+
+    public ResolverResult(Question question, DnsQueryResult result, Set<DnssecUnverifiedReason> unverifiedReasons) throws NullResultException {
+        // TODO: Is this null check still needed?
+        if (result == null) {
+            throw new MiniDnsException.NullResultException(question.asMessageBuilder().build());
+        }
+
+        this.result = result;
+
+        DnsMessage answer = result.response;
+        this.question = question;
+        this.responseCode = answer.responseCode;
+        this.answer = answer;
+
+        Set<D> r = answer.getAnswersFor(question);
+        if (r == null) {
+            this.data = Collections.emptySet();
+        } else {
+            this.data = Collections.unmodifiableSet(r);
+        }
+
+        if (unverifiedReasons == null) {
+            this.unverifiedReasons = null;
+            isAuthenticData = false;
+        } else {
+            this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
+            isAuthenticData = this.unverifiedReasons.isEmpty();
+        }
+    }
+
+    public boolean wasSuccessful() {
+        return responseCode == RESPONSE_CODE.NO_ERROR;
+    }
+
+    public Set<D> getAnswers() {
+        throwIseIfErrorResponse();
+        return data;
+    }
+
+    public Set<D> getAnswersOrEmptySet() {
+        return data;
+    }
+
+    public RESPONSE_CODE getResponseCode() {
+        return responseCode;
+    }
+
+    public boolean isAuthenticData() {
+        throwIseIfErrorResponse();
+        return isAuthenticData;
+    }
+
+    /**
+     * Get the reasons the result could not be verified if any exists.
+     *
+     * @return The reasons the result could not be verified or <code>null</code>.
+     */
+    public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
+        throwIseIfErrorResponse();
+        return unverifiedReasons;
+    }
+
+    public Question getQuestion() {
+        return question;
+    }
+
+    public void throwIfErrorResponse() throws ResolutionUnsuccessfulException {
+        ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
+        if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException;
+    }
+
+    private ResolutionUnsuccessfulException resolutionUnsuccessfulException;
+
+    public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() {
+        if (wasSuccessful()) return null;
+
+        if (resolutionUnsuccessfulException == null) {
+            resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode);
+        }
+
+        return resolutionUnsuccessfulException;
+    }
+
+    private DnssecResultNotAuthenticException dnssecResultNotAuthenticException;
+
+    public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() {
+        if (!wasSuccessful())
+            return null;
+        if (isAuthenticData)
+            return null;
+
+        if (dnssecResultNotAuthenticException == null) {
+            dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons());
+        }
+
+        return dnssecResultNotAuthenticException;
+    }
+
+    /**
+     * Get the raw answer DNS message we received. <b>This is likely not what you want</b>, try {@link #getAnswers()} instead.
+     *
+     * @return the raw answer DNS Message.
+     * @see #getAnswers()
+     */
+    public DnsMessage getRawAnswer() {
+        return answer;
+    }
+
+    public DnsQueryResult getDnsQueryResult() {
+        return result;
+    }
+
+    @Override
+    public final String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(getClass().getName()).append('\n')
+               .append("Question: ").append(question).append('\n')
+               .append("Response Code: ").append(responseCode).append('\n');
+
+        if (responseCode == RESPONSE_CODE.NO_ERROR) {
+            if (isAuthenticData) {
+                sb.append("Results verified via DNSSEC\n");
+            }
+            if (hasUnverifiedReasons()) {
+                sb.append(unverifiedReasons).append('\n');
+            }
+            sb.append(answer.answerSection);
+        }
+
+        return sb.toString();
+    }
+
+    boolean hasUnverifiedReasons() {
+        return unverifiedReasons != null && !unverifiedReasons.isEmpty();
+    }
+
+    protected void throwIseIfErrorResponse() {
+        ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
+        if (resolutionUnsuccessfulException != null)
+            throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful",
+                    resolutionUnsuccessfulException);
+    }
+}

src/main/java/eu/siacs/conversations/AppSettings.java 🔗

@@ -10,6 +10,8 @@ import androidx.preference.PreferenceManager;
 
 import com.google.common.base.Strings;
 
+import java.security.SecureRandom;
+
 public class AppSettings {
 
     public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
@@ -44,6 +46,9 @@ public class AppSettings {
     public static final String COLORFUL_CHAT_BUBBLES = "use_green_background";
     public static final String LARGE_FONT = "large_font";
 
+    private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+    private static final String INSTALLATION_ID = "im.conversations.android.install_id";
+
     private final Context context;
 
     public AppSettings(final Context context) {
@@ -133,4 +138,25 @@ public class AppSettings {
     public boolean isRequireChannelBinding() {
         return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding);
     }
+
+    public synchronized long getInstallationId() {
+        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        final long existing = sharedPreferences.getLong(INSTALLATION_ID, 0);
+        if (existing != 0) {
+            return existing;
+        }
+        final var secureRandom = new SecureRandom();
+        final var installationId = secureRandom.nextLong();
+        sharedPreferences.edit().putLong(INSTALLATION_ID, installationId).apply();
+        return installationId;
+    }
+
+    public synchronized void resetInstallationId() {
+        final var secureRandom = new SecureRandom();
+        final var installationId = secureRandom.nextLong();
+        PreferenceManager.getDefaultSharedPreferences(context)
+                .edit()
+                .putLong(INSTALLATION_ID, installationId)
+                .apply();
+    }
 }

src/main/java/eu/siacs/conversations/Conversations.java 🔗

@@ -1,5 +1,6 @@
 package eu.siacs.conversations;
 
+import android.annotation.SuppressLint;
 import android.app.Application;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -15,9 +16,17 @@ import eu.siacs.conversations.utils.ThemeHelper;
 
 public class Conversations extends Application {
 
+    @SuppressLint("StaticFieldLeak")
+    private static Context CONTEXT;
+
+    public static Context getContext() {
+        return Conversations.CONTEXT;
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
+        CONTEXT = this.getApplicationContext();
         ExceptionHelper.init(getApplicationContext());
         applyThemeSettings();
     }

src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java 🔗

@@ -3,11 +3,13 @@ package eu.siacs.conversations.crypto;
 import android.util.Log;
 import android.util.Pair;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 
+import org.bouncycastle.asn1.ASN1Object;
 import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.ASN1TaggedObject;
 import org.bouncycastle.asn1.DERIA5String;
-import org.bouncycastle.asn1.DERTaggedObject;
 import org.bouncycastle.asn1.DERUTF8String;
 import org.bouncycastle.asn1.DLSequence;
 import org.bouncycastle.asn1.x500.RDN;
@@ -37,13 +39,15 @@ public class XmppDomainVerifier {
     private static final String SRV_NAME = "1.3.6.1.5.5.7.8.7";
     private static final String XMPP_ADDR = "1.3.6.1.5.5.7.8.5";
 
-    private static List<String> getCommonNames(X509Certificate certificate) {
+    private static List<String> getCommonNames(final X509Certificate certificate) {
         List<String> domains = new ArrayList<>();
         try {
             X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
             RDN[] rdns = x500name.getRDNs(BCStyle.CN);
             for (int i = 0; i < rdns.length; ++i) {
-                domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue()));
+                domains.add(
+                        IETFUtils.valueToString(
+                                x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue()));
             }
             return domains;
         } catch (CertificateEncodingException e) {
@@ -51,26 +55,26 @@ public class XmppDomainVerifier {
         }
     }
 
-    private static Pair<String, String> parseOtherName(byte[] otherName) {
+    private static Pair<String, String> parseOtherName(final byte[] otherName) {
         try {
             ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName);
-            if (asn1Primitive instanceof DERTaggedObject) {
-                ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject();
-                if (inner instanceof DLSequence) {
-                    DLSequence sequence = (DLSequence) inner;
-                    if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) {
-                        String oid = sequence.getObjectAt(0).toString();
-                        ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject();
-                        if (value instanceof DERUTF8String) {
-                            return new Pair<>(oid, ((DERUTF8String) value).getString());
-                        } else if (value instanceof DERIA5String) {
-                            return new Pair<>(oid, ((DERIA5String) value).getString());
+            if (asn1Primitive instanceof ASN1TaggedObject taggedObject) {
+                final ASN1Object inner = taggedObject.getBaseObject();
+                if (inner instanceof DLSequence sequence) {
+                    if (sequence.size() >= 2
+                            && sequence.getObjectAt(1) instanceof ASN1TaggedObject evenInner) {
+                        final String oid = sequence.getObjectAt(0).toString();
+                        final ASN1Object value = evenInner.getBaseObject();
+                        if (value instanceof DERUTF8String derutf8String) {
+                            return new Pair<>(oid, derutf8String.getString());
+                        } else if (value instanceof DERIA5String deria5String) {
+                            return new Pair<>(oid, deria5String.getString());
                         }
                     }
                 }
             }
             return null;
-        } catch (IOException e) {
+        } catch (final IOException e) {
             return null;
         }
     }
@@ -98,14 +102,15 @@ public class XmppDomainVerifier {
         return false;
     }
 
-    public boolean verify(final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) throws SSLPeerUnverifiedException {
+    public boolean verify(
+            final String unicodeDomain, final String unicodeHostname, SSLSession sslSession)
+            throws SSLPeerUnverifiedException {
         final String domain = IDN.toASCII(unicodeDomain);
         final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname);
         final Certificate[] chain = sslSession.getPeerCertificates();
-        if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
+        if (chain.length == 0 || !(chain[0] instanceof X509Certificate certificate)) {
             return false;
         }
-        final X509Certificate certificate = (X509Certificate) chain[0];
         final List<String> commonNames = getCommonNames(certificate);
         if (isSelfSigned(certificate)) {
             if (commonNames.size() == 1 && matchDomain(domain, commonNames)) {
@@ -115,11 +120,11 @@ public class XmppDomainVerifier {
         }
         try {
             final ValidDomains validDomains = parseValidDomains(certificate);
-            Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + validDomains.srvNames + " xmppAddrs: " + validDomains.xmppAddrs + " domains:" + validDomains.domains);
+            Log.d(LOGTAG, "searching for " + domain + " in " + validDomains);
             if (hostname != null) {
                 Log.d(LOGTAG, "also trying to verify hostname " + hostname);
             }
-            return validDomains.xmppAddrs.contains(domain)
+            return validDomains.xmppAddresses.contains(domain)
                     || validDomains.srvNames.contains("_xmpp-client." + domain)
                     || matchDomain(domain, validDomains.domains)
                     || (hostname != null && matchDomain(hostname, validDomains.domains));
@@ -128,7 +133,8 @@ public class XmppDomainVerifier {
         }
     }
 
-    public static ValidDomains parseValidDomains(final X509Certificate certificate) throws CertificateParsingException {
+    public static ValidDomains parseValidDomains(final X509Certificate certificate)
+            throws CertificateParsingException {
         final List<String> commonNames = getCommonNames(certificate);
         final Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
         final List<String> xmppAddrs = new ArrayList<>();
@@ -148,7 +154,9 @@ public class XmppDomainVerifier {
                                 xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
                                 break;
                             default:
-                                Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
+                                Log.d(
+                                        LOGTAG,
+                                        "oid: " + otherName.first + " value: " + otherName.second);
                         }
                     }
                 } else if (type == 2) {
@@ -159,30 +167,40 @@ public class XmppDomainVerifier {
                 }
             }
         }
-        if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) {
+        if (srvNames.isEmpty() && xmppAddrs.isEmpty() && domains.isEmpty()) {
             domains.addAll(commonNames);
         }
         return new ValidDomains(xmppAddrs, srvNames, domains);
     }
 
     public static final class ValidDomains {
-        final List<String> xmppAddrs;
+        final List<String> xmppAddresses;
         final List<String> srvNames;
         final List<String> domains;
 
-        private ValidDomains(List<String> xmppAddrs, List<String> srvNames, List<String> domains) {
-            this.xmppAddrs = xmppAddrs;
+        private ValidDomains(
+                List<String> xmppAddresses, List<String> srvNames, List<String> domains) {
+            this.xmppAddresses = xmppAddresses;
             this.srvNames = srvNames;
             this.domains = domains;
         }
 
         public List<String> all() {
             ImmutableList.Builder<String> all = new ImmutableList.Builder<>();
-            all.addAll(xmppAddrs);
+            all.addAll(xmppAddresses);
             all.addAll(srvNames);
             all.addAll(domains);
             return all.build();
         }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                    .add("xmppAddresses", xmppAddresses)
+                    .add("srvNames", srvNames)
+                    .add("domains", domains)
+                    .toString();
+        }
     }
 
     private boolean isSelfSigned(X509Certificate certificate) {

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 (response.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(response);
+                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/Conversation.java 🔗

@@ -146,10 +146,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.forms.Option;
 import eu.siacs.conversations.xmpp.mam.MamReference;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 import static eu.siacs.conversations.entities.Bookmark.printableValue;
 
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
     public static final String TABLENAME = "conversations";
@@ -1597,7 +1597,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             show();
             CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
 
-            final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+            final var packet = new Iq(Iq.Type.SET);
             packet.setTo(command.getAttributeAsJid("jid"));
             final Element c = packet.addChild("command", Namespace.COMMANDS);
             c.setAttribute("node", command.getAttribute("node"));
@@ -1615,7 +1615,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                             }
                         }, 1000);
                     } else {
-                        xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+                        xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> {
                             session.updateWithResponse(iq);
                         }, 120L);
                     }
@@ -1642,7 +1642,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
         public void startMucConfig(XmppConnectionService xmppConnectionService) {
             MucConfigSession session = new MucConfigSession(xmppConnectionService);
-            final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+            final var packet = new Iq(Iq.Type.GET);
             packet.setTo(Conversation.this.getJid().asBareJid());
             packet.addChild("query", "http://jabber.org/protocol/muc#owner");
 
@@ -1658,7 +1658,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                             }
                         }, 1000);
                     } else {
-                        xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+                        xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> {
                             session.updateWithResponse(iq);
                         }, 120L);
                     }
@@ -2779,7 +2779,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             protected Item mkItem(Element el, int pos) {
                 int viewType = TYPE_ERROR;
 
-                if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
+                if (response != null && response.getType() == Iq.Type.RESULT) {
                     if (el.getName().equals("note")) {
                         viewType = TYPE_NOTE;
                     } else if (el.getNamespace().equals("jabber:x:oob")) {
@@ -2880,7 +2880,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             protected String mTitle;
             protected String mNode;
             protected CommandPageBinding mBinding = null;
-            protected IqPacket response = null;
+            protected Iq response = null;
             protected Element responseElement = null;
             protected boolean expectingRemoval = false;
             protected List<Field> reported = null;
@@ -2890,7 +2890,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             protected GridLayoutManager layoutManager;
             protected WebView actionToWebview = null;
             protected int fillableFieldCount = 0;
-            protected IqPacket pendingResponsePacket = null;
+            protected Iq pendingResponsePacket = null;
             protected boolean waitingForRefresh = false;
 
             CommandSession(String title, String node, XmppConnectionService xmppConnectionService) {
@@ -2909,7 +2909,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 return mNode;
             }
 
-            public void updateWithResponse(final IqPacket iq) {
+            public void updateWithResponse(final Iq iq) {
                 if (getView() != null && getView().isAttachedToWindow()) {
                     getView().post(() -> updateWithResponseUiThread(iq));
                 } else {
@@ -2917,7 +2917,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 }
             }
 
-            protected void updateWithResponseUiThread(final IqPacket iq) {
+            protected void updateWithResponseUiThread(final Iq iq) {
                 Timer oldTimer = this.loadingTimer;
                 this.loadingTimer = new Timer();
                 oldTimer.cancel();
@@ -2934,7 +2934,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
                 boolean actionsCleared = false;
                 Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
-                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
+                if (iq.getType() == Iq.Type.RESULT && command != null) {
                     if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) {
                         xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true);
                     }
@@ -3098,7 +3098,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             public int getItemCount() {
                 if (loading) return 1;
                 if (response == null) return 0;
-                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
+                if (response.getType() == Iq.Type.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
                     int i = 0;
                     for (Element el : responseElement.getChildren()) {
                         if (!el.getNamespace().equals("jabber:x:data")) continue;
@@ -3131,7 +3131,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 if (items.get(position) != null) return items.get(position);
                 if (response == null) return null;
 
-                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
+                if (response.getType() == Iq.Type.RESULT && responseElement != null) {
                     if (responseElement.getNamespace().equals("jabber:x:data")) {
                         int i = 0;
                         for (Element el : responseElement.getChildren()) {
@@ -3314,7 +3314,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     return false;
                 }
 
-                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+                final var packet = new Iq(Iq.Type.SET);
                 packet.setTo(response.getFrom());
                 final Element c = packet.addChild("command", Namespace.COMMANDS);
                 c.setAttribute("node", mNode);
@@ -3357,7 +3357,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 if (c.getAttribute("action") == null) c.setAttribute("action", action);
 
                 executing = true;
-                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+                xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> {
                     updateWithResponse(iq);
                 }, 120L);
 
@@ -3492,7 +3492,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 actionsAdapter.notifyDataSetChanged();
 
                 if (pendingResponsePacket != null) {
-                    final IqPacket pending = pendingResponsePacket;
+                    final var pending = pendingResponsePacket;
                     pendingResponsePacket = null;
                     updateWithResponseUiThread(pending);
                 }
@@ -3567,7 +3567,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             }
 
             @Override
-            protected void updateWithResponseUiThread(final IqPacket iq) {
+            protected void updateWithResponseUiThread(final Iq iq) {
                 Timer oldTimer = this.loadingTimer;
                 this.loadingTimer = new Timer();
                 oldTimer.cancel();
@@ -3583,7 +3583,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 layoutManager.setSpanCount(1);
 
                 final Element query = iq.findChild("query", "http://jabber.org/protocol/muc#owner");
-                if (iq.getType() == IqPacket.TYPE.RESULT && query != null) {
+                if (iq.getType() == Iq.Type.RESULT && query != null) {
                     final Data form = Data.parse(query.findChild("x", "jabber:x:data"));
                     final String title = form.getTitle();
                     if (title != null) {
@@ -3602,7 +3602,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     if (actionsAdapter.getPosition("cancel") < 0) {
                         actionsAdapter.insert(Pair.create("cancel", "cancel"), 0);
                     }
-                } else if (iq.getType() == IqPacket.TYPE.RESULT) {
+                } else if (iq.getType() == Iq.Type.RESULT) {
                     expectingRemoval = true;
                     removeSession(this);
                     return;
@@ -3616,7 +3616,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             @Override
             public synchronized boolean execute(String action) {
                 if ("cancel".equals(action)) {
-                    final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+                    final var packet = new Iq(Iq.Type.SET);
                     packet.setTo(response.getFrom());
                     final Element form = packet
                         .addChild("query", "http://jabber.org/protocol/muc#owner")
@@ -3628,7 +3628,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
                 if (!"save".equals(action)) return true;
 
-                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+                final var packet = new Iq(Iq.Type.SET);
                 packet.setTo(response.getFrom());
 
                 String formType = responseElement == null ? null : responseElement.getAttribute("type");
@@ -3644,7 +3644,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 }
 
                 executing = true;
-                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
+                xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> {
                     updateWithResponse(iq);
                 }, 120L);
 

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

@@ -28,6 +28,14 @@ import eu.siacs.conversations.xmpp.forms.Field;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xml.Element;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
 public class MucOptions {
 
     public static final String STATUS_CODE_SELF_PRESENCE = "110";
@@ -198,6 +206,11 @@ public class MucOptions {
         }
     }
 
+    public boolean allowPmRaw() {
+        final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
+        return  field == null || Arrays.asList("anyone","participants").contains(field.getValue());
+    }
+
     public boolean participating() {
         return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
     }

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<>();
@@ -279,7 +279,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;
@@ -327,8 +327,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()));
@@ -342,18 +355,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 🔗

@@ -45,7 +45,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 {
 
@@ -53,8 +53,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");
@@ -69,8 +69,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());
@@ -93,8 +93,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));
@@ -113,14 +113,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);
@@ -132,12 +132,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);
@@ -147,36 +147,36 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket retrieveVcard4(final Jid jid) {
-        final IqPacket packet = retrieve("urn:xmpp:vcard4", null);
+    public Iq retrieveVcard4(final Jid jid) {
+        final var packet = retrieve("urn:xmpp:vcard4", null);
         packet.setTo(jid);
         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);
@@ -185,7 +185,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);
@@ -193,14 +193,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
@@ -214,57 +214,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);
@@ -314,7 +314,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");
@@ -338,7 +338,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);
@@ -356,8 +356,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();
@@ -387,15 +387,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) {
@@ -411,15 +411,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();
@@ -428,14 +428,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");
@@ -447,8 +447,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");
@@ -457,11 +457,11 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket moderateMessage(Account account, Message m, String reason) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+    public Iq moderateMessage(Account account, Message m, String reason) {
+        final var packet = new Iq(Iq.Type.SET);
         packet.setTo(m.getConversation().getJid().asBareJid());
         packet.setFrom(account.getJid());
-        Element moderate =
+        final var moderate =
             packet.addChild("apply-to", "urn:xmpp:fasten:0")
                   .setAttribute("id", m.getServerMsgId())
                   .addChild("moderate", "urn:xmpp:message-moderate:0");
@@ -470,8 +470,8 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+    public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) {
+        final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(host);
         Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
         request.setAttribute("filename", name == null ? convertFilename(file.getName()) : name);
@@ -480,8 +480,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()));
@@ -507,8 +507,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);
@@ -519,12 +519,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");
@@ -540,8 +540,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");
@@ -554,8 +554,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);
@@ -569,16 +569,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;
@@ -611,16 +611,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);
@@ -630,43 +630,43 @@ 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.query(Namespace.DISCO_ITEMS);
         return packet;
     }
 
-    public IqPacket queryDiscoItems(Jid jid, String node) {
-        IqPacket packet = queryDiscoItems(jid);
-        final Element query = packet.query(Namespace.DISCO_ITEMS);
+    public Iq queryDiscoItems(Jid jid, String node) {
+        final var packet = queryDiscoItems(jid);
+        final var query = packet.query(Namespace.DISCO_ITEMS);
         query.setAttribute("node", node);
         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;
     }
 
-    public IqPacket bobResponse(IqPacket request) {
+    public Iq bobResponse(Iq request) {
         try {
-            String bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
-            Cid cid = BobTransfer.cid(bobCid);
-            DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
+            final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
+            final var cid = BobTransfer.cid(bobCid);
+            final var f = mXmppConnectionService.getFileForCid(cid);
             if (f == null || !f.canRead()) {
                 throw new IOException("No such file");
             } else if (f.getSize() > 129000) {
-                final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR);
-                final Element error = response.addChild("error");
+                final var response = request.generateResponse(Iq.Type.ERROR);
+                final var error = response.addChild("error");
                 error.setAttribute("type", "cancel");
                 error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas");
                 return response;
             } else {
-                final IqPacket response = request.generateResponse(IqPacket.TYPE.RESULT);
-                final Element data = response.addChild("data", "urn:xmpp:bob");
+                final var response = request.generateResponse(Iq.Type.RESULT);
+                final var data = response.addChild("data", "urn:xmpp:bob");
                 data.setAttribute("cid", bobCid);
                 data.setAttribute("type", f.getMimeType());
                 ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2);
@@ -678,8 +678,8 @@ public class IqGenerator extends AbstractGenerator {
                 return response;
             }
         } catch (final IOException | IllegalStateException e) {
-            final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR);
-            final Element error = response.addChild("error");
+            final var response = request.generateResponse(Iq.Type.ERROR);
+            final var error = response.addChild("error");
             error.setAttribute("type", "cancel");
             error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
             return response;

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

@@ -23,7 +23,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";
@@ -33,25 +32,25 @@ public class MessageGenerator extends AbstractGenerator {
         super(service);
     }
 
-    private MessagePacket preparePacket(Message message, boolean legacyEncryption) {
+    private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) {
         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");
@@ -78,7 +77,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"));
@@ -87,8 +86,8 @@ public class MessageGenerator extends AbstractGenerator {
         delay.setAttribute("stamp", mDateFormat.format(date));
     }
 
-    public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
-        MessagePacket packet = preparePacket(message, true);
+    public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
+        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
         if (axolotlMessage == null) {
             return null;
         }
@@ -101,17 +100,18 @@ 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, false);
+    public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
+        im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false);
+        String content;
         if (message.hasFileOnRemoteHost()) {
             final Message.FileParams fileParams = message.getFileParams();
 
@@ -139,8 +139,8 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket generatePgpChat(Message message) {
-        MessagePacket packet = preparePacket(message, true);
+    public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
+        final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
         if (message.hasFileOnRemoteHost()) {
             Message.FileParams fileParams = message.getFileParams();
             final String url = fileParams.url;
@@ -163,10 +163,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()));
@@ -175,11 +175,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) {
@@ -197,20 +197,20 @@ 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 requestVoice(Jid jid) {
-        MessagePacket packet = new MessagePacket();
-        packet.setType(MessagePacket.TYPE_NORMAL);
+    public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) {
+        final var packet = new im.conversations.android.xmpp.model.stanza.Message();
+        packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
         packet.setTo(jid.asBareJid());
-        Data form = new Data();
+        final var form = new Data();
         form.setFormType("http://jabber.org/protocol/muc#request");
         form.put("muc#role", "participant");
         form.submit();
@@ -218,9 +218,9 @@ public class MessageGenerator extends AbstractGenerator {
         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");
@@ -236,8 +236,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");
@@ -249,8 +249,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());
@@ -261,8 +262,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);
@@ -270,10 +271,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);
@@ -283,9 +284,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);
@@ -298,9 +299,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);
@@ -309,9 +310,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, null);
     }
 
-    public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) {
-        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 String nickname) {
+        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();
@@ -87,16 +86,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/HttpConnectionManager.java 🔗

@@ -2,11 +2,27 @@ package eu.siacs.conversations.http;
 
 import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
+import android.content.Context;
 import android.os.Build;
 import android.util.Log;
 
 import androidx.core.util.Consumer;
 
+import eu.siacs.conversations.BuildConfig;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.TrustManagers;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.TLSSocketFactory;
+
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.ResponseBody;
+
 import org.apache.http.conn.ssl.StrictHostnameVerifier;
 
 import java.io.IOException;
@@ -16,7 +32,9 @@ import java.net.InetSocketAddress;
 import java.net.Proxy;
 import java.net.UnknownHostException;
 import java.security.KeyManagementException;
+import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -26,19 +44,6 @@ import java.util.concurrent.TimeUnit;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
 
-import eu.siacs.conversations.BuildConfig;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.TLSSocketFactory;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.ResponseBody;
-
 public class HttpConnectionManager extends AbstractConnectionManager {
 
     private final List<HttpDownloadConnection> downloadConnections = new ArrayList<>();
@@ -46,7 +51,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
 
     public static final Executor EXECUTOR = Executors.newFixedThreadPool(4);
 
-    public static final OkHttpClient OK_HTTP_CLIENT;
+    private static final OkHttpClient OK_HTTP_CLIENT;
 
     static {
         OK_HTTP_CLIENT = new OkHttpClient.Builder()
@@ -209,4 +214,27 @@ public class HttpConnectionManager extends AbstractConnectionManager {
 
         return filename;
     }
+
+    public static OkHttpClient okHttpClient(final Context context) {
+        final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
+        try {
+            final X509TrustManager trustManager;
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
+                trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
+            } else {
+                trustManager = TrustManagers.createDefaultTrustManager();
+            }
+            final SSLSocketFactory socketFactory =
+                    new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
+            builder.sslSocketFactory(socketFactory, trustManager);
+        } catch (final IOException
+                       | KeyManagementException
+                       | NoSuchAlgorithmException
+                       | KeyStoreException
+                       | CertificateException e) {
+            Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt");
+            throw new RuntimeException(e);
+        }
+        return builder.build();
+    }
 }

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 fname, String mime) {
         final SettableFuture<Slot> future = SettableFuture.create();
-        final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime);
-        service.sendIqPacket(account, request, (a, packet) -> {
-            if (packet.getType() == IqPacket.TYPE.RESULT) {
+        final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, 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 🔗

@@ -18,14 +18,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) {
@@ -36,8 +38,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;
 		}
@@ -125,7 +127,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;
@@ -38,18 +39,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) {
@@ -66,7 +66,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;
@@ -144,7 +144,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;
@@ -153,10 +153,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;
@@ -169,7 +169,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");
@@ -190,7 +190,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;
@@ -202,7 +202,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) {
@@ -216,7 +216,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;
@@ -229,7 +229,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;
@@ -242,7 +242,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) {
@@ -285,7 +285,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;
@@ -313,7 +313,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;
@@ -337,7 +337,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) {
@@ -352,15 +352,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);
@@ -374,7 +374,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);
             }
@@ -390,7 +390,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);
@@ -402,15 +402,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 {
@@ -426,7 +426,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")
@@ -434,18 +434,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");
@@ -453,18 +453,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");
@@ -476,8 +476,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
             final Conversation conversation = mXmppConnectionService.find(account, packet.getFrom());
             if (packet.hasChild("data", "urn:xmpp:bob") && isGet && (conversation == null ? contact != null && contact.canInferPresence() : conversation.canInferPresence())) {
                 mXmppConnectionService.sendIqPacket(account, mXmppConnectionService.getIqGenerator().bobResponse(packet), null);
-            } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
-                final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
+            } else if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) {
+                final var 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 🔗

@@ -22,6 +22,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.Consumer;
 
 import io.ipfs.cid.Cid;
 
@@ -60,17 +61,20 @@ import eu.siacs.conversations.xmpp.forms.Data;
 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) {
@@ -109,7 +113,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();
@@ -251,7 +255,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);
@@ -358,10 +362,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);
                 }
@@ -404,11 +408,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;
@@ -422,7 +426,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;
             }
@@ -442,9 +446,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;
@@ -514,7 +518,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;
@@ -1297,6 +1301,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())) {
@@ -1309,7 +1341,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) {
@@ -1321,7 +1353,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,9 +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();
@@ -300,7 +297,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())) {
@@ -434,7 +431,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/persistance/FileBackend.java 🔗

@@ -53,6 +53,21 @@ import com.madebyevan.thumbhash.ThumbHash;
 
 import com.wolt.blurhashkt.BlurHashDecoder;
 
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.AttachFileToConversationRunnable;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.adapter.MediaAdapter;
+import eu.siacs.conversations.ui.util.Attachment;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.FileWriterException;
+import eu.siacs.conversations.utils.MimeUtils;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
@@ -85,22 +100,6 @@ import org.tomlj.TomlTable;
 
 import io.ipfs.cid.Cid;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.services.AttachFileToConversationRunnable;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.ui.adapter.MediaAdapter;
-import eu.siacs.conversations.ui.util.Attachment;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.FileUtils;
-import eu.siacs.conversations.utils.FileWriterException;
-import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.xmpp.pep.Avatar;
-import eu.siacs.conversations.xml.Element;
-
 public class FileBackend {
 
     private static final Object THUMBNAIL_LOCK = new Object();
@@ -784,16 +783,16 @@ public class FileBackend {
     }
 
     private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
-        Log.d(
-                Config.LOGTAG,
-                "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
-        file.getParentFile().mkdirs();
+        final var parentDirectory = file.getParentFile();
+        if (parentDirectory != null && parentDirectory.mkdirs()) {
+            Log.d(Config.LOGTAG,"created directory "+parentDirectory.getAbsolutePath());
+        }
         try {
             if (!file.createNewFile() && file.length() > 0) {
                 if (file.canRead() && file.getName().startsWith("zb2")) return; // We have this content already
                 throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
             }
-        } catch (IOException e) {
+        } catch (final IOException e) {
             throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
         }
         try (final OutputStream os = new FileOutputStream(file);
@@ -803,12 +802,12 @@ public class FileBackend {
             }
             try {
                 ByteStreams.copy(is, os);
-            } catch (IOException e) {
+            } catch (final IOException e) {
                 throw new FileWriterException(file);
             }
             try {
                 os.flush();
-            } catch (IOException e) {
+            } catch (final IOException e) {
                 throw new FileWriterException(file);
             }
         } catch (final FileNotFoundException e) {
@@ -817,7 +816,7 @@ public class FileBackend {
         } catch (final FileWriterException e) {
             cleanup(file);
             throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
-        } catch (final SecurityException | IllegalStateException e) {
+        } catch (final SecurityException | IllegalStateException | IllegalArgumentException e) {
             cleanup(file);
             throw new FileCopyException(R.string.error_security_exception);
         } catch (final IOException e) {
@@ -828,7 +827,7 @@ public class FileBackend {
 
     public void copyFileToPrivateStorage(Message message, Uri uri, String type)
             throws FileCopyException {
-        String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
+        final String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type);
         Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")");
         String extension = MimeUtils.guessExtensionFromMimeType(mime);
         if (extension == null) {
@@ -1168,9 +1167,9 @@ public class FileBackend {
     }
 
     public BitmapDrawable getFallbackThumbnail(final Message message, int size, boolean cacheOnly) {
-        List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
+        final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
         if (thumbs != null && !thumbs.isEmpty()) {
-            for (Element thumb : thumbs) {
+            for (final var thumb : thumbs) {
                 final var uriS = thumb.getAttribute("uri");
                 if (uriS == null) continue;
                 Uri uri = Uri.parse(uriS);
@@ -1240,9 +1239,9 @@ public class FileBackend {
 
         if ((thumbnail == null) && (!cacheOnly)) {
             synchronized (THUMBNAIL_LOCK) {
-                List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
+                final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
                 if (thumbs != null && !thumbs.isEmpty()) {
-                    for (Element thumb : thumbs) {
+                    for (final var thumb : thumbs) {
                         final var uriS = thumb.getAttribute("uri");
                         if (uriS == null) continue;
                         Uri uri = Uri.parse(uriS);

src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java 🔗

@@ -1,32 +0,0 @@
-package eu.siacs.conversations.receiver;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import androidx.work.WorkManager;
-
-import com.google.common.base.Strings;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment;
-
-public class WorkManagerEventReceiver extends BroadcastReceiver {
-
-    public static final String ACTION_STOP_BACKUP = "eu.siacs.conversations.receiver.STOP_BACKUP";
-
-    @Override
-    public void onReceive(final Context context, final Intent intent) {
-        final var action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
-        if (action.equals(ACTION_STOP_BACKUP)) {
-            stopBackup(context);
-        }
-    }
-
-    private void stopBackup(final Context context) {
-        Log.d(Config.LOGTAG, "trying to stop one-off backup worker");
-        final var workManager = WorkManager.getInstance(context);
-        workManager.cancelUniqueWork(BackupSettingsFragment.CREATE_ONE_OFF_BACKUP);
-    }
-}

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

@@ -42,8 +42,12 @@ public class CallIntegration extends Connection {
      *
      * <p>Samsung Galaxy Tab A claims to have FEATURE_CONNECTION_SERVICE but then throws
      * SecurityException when invoking placeCall(). Both Stock and LineageOS have this problem.
+     *
+     * <p>Lenovo Yoga Smart Tab YT-X705F claims to have FEATURE_CONNECTION_SERVICE but throws
+     * SecurityException
      */
-    private static final List<String> BROKEN_DEVICE_MODELS = Arrays.asList("OnePlus6", "gtaxlwifi");
+    private static final List<String> BROKEN_DEVICE_MODELS =
+            Arrays.asList("OnePlus6", "gtaxlwifi", "YT-X705F");
 
     public static final int DEFAULT_TONE_VOLUME = 60;
     private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90;
@@ -393,9 +397,7 @@ public class CallIntegration extends Connection {
 
     public void success() {
         Log.d(Config.LOGTAG, "CallIntegration.success()");
-        final var toneGenerator =
-                new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
-        toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
+        startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
         this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375);
     }
 
@@ -410,9 +412,7 @@ public class CallIntegration extends Connection {
 
     public void error() {
         Log.d(Config.LOGTAG, "CallIntegration.error()");
-        final var toneGenerator =
-                new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
-        toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
+        startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
         this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375);
     }
 
@@ -429,8 +429,7 @@ public class CallIntegration extends Connection {
 
     public void busy() {
         Log.d(Config.LOGTAG, "CallIntegration.busy()");
-        final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80);
-        toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
+        startTone(80, ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
         this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
     }
 
@@ -458,6 +457,17 @@ public class CallIntegration extends Connection {
         Log.d(Config.LOGTAG, "destroyed!");
     }
 
+    private void startTone(final int volume, final int toneType, final int durationMs) {
+        final ToneGenerator toneGenerator;
+        try {
+            toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, volume);
+        } catch (final RuntimeException e) {
+            Log.e(Config.LOGTAG, "could not initialize tone generator", e);
+            return;
+        }
+        toneGenerator.startTone(toneType, durationMs);
+    }
+
     public static Uri address(final Jid contact) {
         return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
     }
@@ -532,6 +542,12 @@ public class CallIntegration extends Connection {
                 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
             return false;
         }
+        // we are relatively sure that old Oppo devices are broken too. We get reports of 'number
+        // not sent' from Oppo R15x (Android 10)
+        if ("OPPO".equalsIgnoreCase(Build.MANUFACTURER)
+                && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
+            return false;
+        }
         // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being
         // routed properly) However with those devices being extremely rare it's impossible to gauge
         // how many might be effected and no Naomi Wu around to clarify with the company directly

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

@@ -36,6 +36,7 @@ import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
@@ -126,9 +127,17 @@ public class CallIntegrationConnectionService extends ConnectionService {
                 // actually attempted
                 // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR);
             } else {
-                final var proposal =
-                        service.getJingleConnectionManager()
-                                .proposeJingleRtpSession(account, with, media);
+                final JingleConnectionManager.RtpSessionProposal proposal;
+                try {
+                    proposal =
+                            service.getJingleConnectionManager()
+                                    .proposeJingleRtpSession(account, with, media);
+                } catch (final IllegalStateException e) {
+                    return Connection.createFailedConnection(
+                            new DisconnectCause(
+                                    DisconnectCause.ERROR,
+                                    "Phone is busy. Probably race condition. Try again in a moment"));
+                }
                 if (proposal == null) {
                     // TODO instead of just null checking try to get the sessionID
                     return Connection.createFailedConnection(

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

@@ -1,8 +1,6 @@
 package eu.siacs.conversations.services;
 
-import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
-import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -12,17 +10,15 @@ import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.crypto.TrustManagers;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Room;
 import eu.siacs.conversations.http.HttpConnectionManager;
 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;
@@ -34,10 +30,6 @@ import retrofit2.Retrofit;
 import retrofit2.converter.gson.GsonConverterFactory;
 
 import java.io.IOException;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -47,9 +39,6 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.X509TrustManager;
-
 public class ChannelDiscoveryService {
 
     private final XmppConnectionService service;
@@ -68,25 +57,7 @@ public class ChannelDiscoveryService {
             this.muclumbusService = null;
             return;
         }
-        final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder();
-        try {
-            final X509TrustManager trustManager;
-            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
-                trustManager = TrustManagers.defaultWithBundledLetsEncrypt(service);
-            } else {
-                trustManager = TrustManagers.createDefaultTrustManager();
-            }
-            final SSLSocketFactory socketFactory =
-                    new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
-            builder.sslSocketFactory(socketFactory, trustManager);
-        } catch (final IOException
-                | KeyManagementException
-                | NoSuchAlgorithmException
-                | KeyStoreException
-                | CertificateException e) {
-            Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt");
-            throw new RuntimeException(e);
-        }
+        final OkHttpClient.Builder builder = HttpConnectionManager.okHttpClient(service).newBuilder();
         if (service.useTorToConnect()) {
             builder.proxy(HttpConnectionManager.getProxy());
         }
@@ -203,7 +174,7 @@ public class ChannelDiscoveryService {
             final String query, Map<Jid, Account> mucServices, final OnChannelSearchResultsFound listener) {
         final Map<Jid, Account> localMucService = mucServices == null ? getLocalMucServices() : mucServices;
         Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
-        if (localMucService.size() == 0) {
+        if (localMucService.isEmpty()) {
             listener.onChannelSearchResultsFound(Collections.emptyList());
             return;
         }
@@ -217,38 +188,37 @@ 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) {
+                            for (final Jid item : items) {
                                 if (item.isDomainJid()) continue; // Only looking for MUCs for now, and by spec they have a localpart
-                                IqPacket infoRequest =
+                                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);
-                                                    }
+                                        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, mucServices, listener);
                                                 }
+                                            } else {
+                                                queriesInFlight.decrementAndGet();
                                             }
                                         }, 20L);
                             }

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

@@ -41,7 +41,6 @@ import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.util.Consumer;
 
 import com.google.common.base.Charsets;
 import com.google.common.base.Joiner;
@@ -86,6 +85,7 @@ import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Locale;
+import java.util.function.Consumer;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;

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) {
@@ -233,17 +233,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);
@@ -253,10 +253,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) {
@@ -303,7 +303,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 🔗

@@ -56,7 +56,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.cheogram.android.EmojiSearch;
 import com.cheogram.android.WebxdcUpdate;
@@ -111,6 +110,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 io.ipfs.cid.Cid;
 
@@ -145,8 +145,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;
@@ -186,11 +184,8 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnBindListener;
 import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 import eu.siacs.conversations.xmpp.OnGatewayResult;
-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;
@@ -204,9 +199,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;
 
 import okhttp3.HttpUrl;
@@ -255,12 +248,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);
             }
         }
     };
@@ -272,6 +265,7 @@ public class XmppConnectionService extends Service {
     private long mLastMucPing = 0;
     private Map<String, Message> mScheduledMessages = new HashMap<>();
     private long mLastStickerRescan = 0;
+    private final AppSettings appSettings = new AppSettings(this);
     private final FileBackend fileBackend = new FileBackend(this);
     private MemorizingTrustManager mMemorizingTrustManager;
     private final NotificationService mNotificationService = new NotificationService(this);
@@ -282,9 +276,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);
@@ -371,79 +362,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);
@@ -609,6 +527,10 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public AppSettings getAppSettings() {
+        return this.appSettings;
+    }
+
     public FileBackend getFileBackend() {
         return this.fileBackend;
     }
@@ -1512,13 +1434,11 @@ public class XmppConnectionService extends Service {
         toggleForegroundService();
         this.destroyed = false;
         OmemoSetting.load(this);
-        ExceptionHelper.init(getApplicationContext());
         try {
             Security.insertProviderAt(Conscrypt.newProvider(), 1);
         } catch (Throwable throwable) {
             Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
         }
-        Resolver.init(this);
         updateMemorizingTrustManager();
         final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
         final int cacheSize = maxMemory / 15;
@@ -1910,12 +1830,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);
@@ -1928,7 +1844,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);
         }
     }
@@ -1966,7 +1882,7 @@ public class XmppConnectionService extends Service {
             }
         }
 
-        MessagePacket packet = null;
+        im.conversations.android.xmpp.model.stanza.Message packet = null;
         final boolean addToConversation = !message.edited() && message.getRawBody() != null;
         boolean saveInDb = addToConversation;
         message.setStatus(Message.STATUS_WAITING);
@@ -2282,13 +2198,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) {
@@ -2303,7 +2219,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));
@@ -2312,54 +2228,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);
@@ -2517,11 +2421,11 @@ public class XmppConnectionService extends Service {
         if (connection == null) return;
 
         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()) {
@@ -2535,7 +2439,7 @@ public class XmppConnectionService extends Service {
         if (!account.areBookmarksLoaded()) return;
 
         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()) {
@@ -2562,9 +2466,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)) {
@@ -2879,11 +2783,11 @@ public class XmppConnectionService extends Service {
 
     public void maybeRegisterWithMuc(Conversation c, String nickArg) {
         final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
-        final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
+        final var register = new Iq(Iq.Type.GET);
         register.query(Namespace.REGISTER);
         register.setTo(c.getJid().asBareJid());
-        sendIqPacket(c.getAccount(), register, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        sendIqPacket(c.getAccount(), register, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 final Element query = response.query(Namespace.REGISTER);
                 String username = query.findChildContent("username", Namespace.REGISTER);
                 if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
@@ -2908,11 +2812,11 @@ public class XmppConnectionService extends Service {
                     }
                     form.put("muc#register_roomnick", nick);
                     form.submit();
-                    final IqPacket finish = new IqPacket(IqPacket.TYPE.SET);
+                    final var finish = new Iq(Iq.Type.SET);
                     finish.query(Namespace.REGISTER).addChild(form);
                     finish.setTo(c.getJid().asBareJid());
-                    sendIqPacket(c.getAccount(), finish, (a2, response2) -> {
-                        if (response.getType() == IqPacket.TYPE.RESULT) {
+                    sendIqPacket(c.getAccount(), finish, (response2) -> {
+                        if (response.getType() == Iq.Type.RESULT) {
                             Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
                         } else {
                             Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
@@ -2930,11 +2834,11 @@ public class XmppConnectionService extends Service {
     }
 
     public void deregisterWithMuc(Conversation c) {
-        final IqPacket register = new IqPacket(IqPacket.TYPE.GET);
+        final Iq register = new Iq(Iq.Type.GET);
         register.query(Namespace.REGISTER).addChild("remove");
         register.setTo(c.getJid().asBareJid());
-        sendIqPacket(c.getAccount(), register, (a, response) -> {
-            if (response.getType() == IqPacket.TYPE.RESULT) {
+        sendIqPacket(c.getAccount(), register, (response) -> {
+            if (response.getType() == Iq.Type.RESULT) {
                 Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
             } else {
                 Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
@@ -3114,6 +3018,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);
@@ -3218,12 +3126,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();
@@ -3232,12 +3140,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);
@@ -3571,7 +3479,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) {
@@ -3595,20 +3503,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);
@@ -3667,7 +3575,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, mucOptions.getSelf().getNick());
+                    final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick());
                     packet.setTo(joinJid);
                     Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
                     if (conversation.getMucOptions().getPassword() != null) {
@@ -3759,16 +3667,16 @@ public class XmppConnectionService extends Service {
         final var affiliations = new ArrayList<String>();
         affiliations.add("outcast");
         if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("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);
@@ -3856,29 +3764,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");
@@ -3886,12 +3794,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());
@@ -3956,7 +3864,7 @@ public class XmppConnectionService extends Service {
         if (options.online()) {
             Account account = conversation.getAccount();
             final Jid joinJid = options.getSelf().getFullJid();
-            final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
+            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
             packet.setTo(joinJid);
             sendPresencePacket(account, packet);
         }
@@ -3976,7 +3884,7 @@ public class XmppConnectionService extends Service {
 
                 @Override
                 public void onSuccess() {
-                    final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
+                    final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
                     packet.setTo(joinJid);
                     sendPresencePacket(account, packet);
                     callback.success(conversation);
@@ -3988,7 +3896,7 @@ public class XmppConnectionService extends Service {
                 }
             });
 
-            final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
+            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
             packet.setTo(joinJid);
             sendPresencePacket(account, packet);
         } else {
@@ -4152,9 +4060,9 @@ public class XmppConnectionService extends Service {
             return;
         }
 
-        IqPacket request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
-        sendIqPacket(account, request, (acct, reply) -> {
-            ServiceDiscoveryResult result = new ServiceDiscoveryResult(reply);
+        final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
+        sendIqPacket(account, request, (reply) -> {
+            final var result = new ServiceDiscoveryResult(reply);
             cb.accept(
                 result.getFeatures().contains("http://jabber.org/protocol/muc") &&
                 result.hasIdentity("conference", null)
@@ -4167,39 +4075,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());
                 }
             }
         });
@@ -4211,33 +4117,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) {
-                        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();
             }
         });
     }
@@ -4251,54 +4151,56 @@ public class XmppConnectionService extends Service {
             final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
             options.putString("members_by_default", moderated ? "0" : "1");
         }
-        final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+        if (options.containsKey("muc#roomconfig_allowpm")) {
+            // ejabberd :-/
+            final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
+            options.putString("allow_private_messages", allow ? "1" : "0");
+            options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
+        }
+        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 {
-                                    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 requestVoice(final Account account, final Jid jid) {
-        MessagePacket packet = this.getMessageGenerator().requestVoice(jid);
+        final var packet = this.getMessageGenerator().requestVoice(jid);
         this.sendMessagePacket(account, 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) {
@@ -4315,39 +4217,37 @@ 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 moderateMessage(final Account account, final Message m, final String reason) {
-        IqPacket request = this.mIqGenerator.moderateMessage(account, m, reason);
-        sendIqPacket(account, request, (a, packet) -> {
-            if (packet.getType() != IqPacket.TYPE.RESULT) {
+        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
+        sendIqPacket(account, request, (packet) -> {
+            if (packet.getType() != Iq.Type.RESULT) {
                 showErrorToastInUi(R.string.unable_to_moderate);
-                Log.d(Config.LOGTAG, a.getJid().asBareJid() + " unable to moderate: " + packet);
+                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
             }
         });
     }
 
     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();
                 }
             }
         });
@@ -4404,7 +4304,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);
@@ -4448,7 +4348,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) {
@@ -4500,10 +4400,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");
@@ -4515,11 +4416,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());
@@ -4545,71 +4446,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);
                 }
             }
         });

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

@@ -158,28 +158,39 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
     };
 
-    private final OnClickListener mChangeConferenceSettings = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final MucOptions mucOptions = mConversation.getMucOptions();
-            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
-            MucConfiguration configuration = MucConfiguration.get(ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
-            builder.setTitle(configuration.title);
-            final boolean[] values = configuration.values;
-            builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked);
-            builder.setNegativeButton(R.string.cancel, null);
-            builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
-                final Bundle options = configuration.toBundle(values);
-                options.putString("muc#roomconfig_persistentroom", "1");
-                options.putString("{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", options.getString("muc#roomconfig_allowinvites"));
-                xmppConnectionService.pushConferenceConfiguration(mConversation,
-                        options,
-                        ConferenceDetailsActivity.this);
-            });
-            builder.create().show();
-        }
-    };
-
+    private final OnClickListener mChangeConferenceSettings =
+            new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    final MucOptions mucOptions = mConversation.getMucOptions();
+                    final MaterialAlertDialogBuilder builder =
+                            new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this);
+                    MucConfiguration configuration =
+                            MucConfiguration.get(
+                                    ConferenceDetailsActivity.this, mAdvancedMode, mucOptions);
+                    builder.setTitle(configuration.title);
+                    final boolean[] values = configuration.values;
+                    builder.setMultiChoiceItems(
+                            configuration.names,
+                            values,
+                            (dialog, which, isChecked) -> values[which] = isChecked);
+                    builder.setNegativeButton(R.string.cancel, null);
+                    builder.setPositiveButton(
+                            R.string.confirm,
+                            (dialog, which) -> {
+                                final Bundle options = configuration.toBundle(values);
+                                options.putString("muc#roomconfig_persistentroom", "1");
+                                if (options.containsKey("muc#roomconfig_allowinvites")) {
+                                    options.putString(
+                                            "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites",
+                                            options.getString("muc#roomconfig_allowinvites"));
+                                }
+                                xmppConnectionService.pushConferenceConfiguration(
+                                        mConversation, options, ConferenceDetailsActivity.this);
+                            });
+                    builder.create().show();
+                }
+            };
 
     @Override
     public void onConversationUpdate() {
@@ -256,6 +267,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             popupMenu.show();
             return true;
         });
+        this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
         this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
         this.binding.mucEditTitle.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(this);
@@ -343,6 +355,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             this.binding.mucEditor.setVisibility(View.VISIBLE);
             this.binding.mucDisplay.setVisibility(View.GONE);
             this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
+            this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
             final String name = mucOptions.getName();
             this.binding.mucEditTitle.setText("");
             final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
@@ -417,6 +430,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.mucEditor.setVisibility(View.GONE);
         this.binding.mucDisplay.setVisibility(View.VISIBLE);
         this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp);
+        this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic));
     }
 
     private void onMucInfoUpdated(String subject, String name) {
@@ -776,8 +790,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             final Bookmark bookmark = mConversation.getBookmark();
             if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) {
                 this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp);
+                this.binding.editMucNameButton.setContentDescription(getString(R.string.save));
             } else {
                 this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp);
+                this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
             }
         }
     }

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

@@ -185,7 +185,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
 import eu.siacs.conversations.xmpp.jingle.RtpCapability;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
 
 import org.jetbrains.annotations.NotNull;
 
@@ -3333,13 +3334,13 @@ public class ConversationFragment extends XmppFragment
         } else {
             if (!delayShow) conversation.showViewPager();
             binding.commandsViewProgressbar.setVisibility(View.VISIBLE);
-            activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> {
+            activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (iq) -> {
                 if (activity == null) return;
 
                 activity.runOnUiThread(() -> {
                     binding.commandsViewProgressbar.setVisibility(View.GONE);
                     commandAdapter.clear();
-                    if (iq.getType() == IqPacket.TYPE.RESULT) {
+                    if (iq.getType() == Iq.Type.RESULT) {
                         for (Element child : iq.query().getChildren()) {
                             if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue;
                             commandAdapter.add(new CommandAdapter.Command0050(child));

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

@@ -242,7 +242,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             if (ExceptionHelper.checkForCrash(this)) return;
             if (offerToSetupDiallerIntegration()) return;
             if (offerToDownloadStickers()) return;
-            openBatteryOptimizationDialogIfNeeded();
+            if (openBatteryOptimizationDialogIfNeeded()) return;
+            requestNotificationPermissionIfNeeded();
             xmppConnectionService.rescanStickers();
         }
     }
@@ -267,7 +268,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                 intent.setData(uri);
                 try {
                     startActivityForResult(intent, REQUEST_BATTERY_OP);
-                } catch (ActivityNotFoundException e) {
+                } catch (final ActivityNotFoundException e) {
                     Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
                 }
             });
@@ -360,16 +361,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
 
     private void notifyFragmentOfBackendConnected(@IdRes int id) {
         final Fragment fragment = getFragmentManager().findFragmentById(id);
-        if (fragment instanceof OnBackendConnected) {
-            ((OnBackendConnected) fragment).onBackendConnected();
+        if (fragment instanceof OnBackendConnected callback) {
+            callback.onBackendConnected();
         }
     }
 
     private void refreshFragment(@IdRes int id) {
         final Fragment fragment = getFragmentManager().findFragmentById(id);
-        if (fragment instanceof XmppFragment) {
-            ((XmppFragment) fragment).refresh();
-            if (refreshForNewCaps) ((XmppFragment) fragment).refreshForNewCaps(newCapsJids);
+        if (fragment instanceof XmppFragment xmppFragment) {
+            xmppFragment.refresh();
+            if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids);
         }
     }
 

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

@@ -47,6 +47,7 @@ import android.view.ViewGroup;
 import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.LinearLayoutManager;
@@ -96,42 +97,36 @@ public class ConversationsOverviewFragment extends XmppFragment {
 	private FragmentConversationsOverviewBinding binding;
 	private ConversationAdapter conversationsAdapter;
 	private XmppActivity activity;
-	private float mSwipeEscapeVelocity = 0f;
 	private final PendingActionHelper pendingActionHelper = new PendingActionHelper();
 
 	private final ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,LEFT|RIGHT) {
 		@Override
-		public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
-			//todo maybe we can manually changing the position of the conversation
+		public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
 			return false;
 		}
 
 		@Override
-		public float getSwipeEscapeVelocity (float defaultValue) {
-			return mSwipeEscapeVelocity;
+		public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
+								float dX, float dY, int actionState, boolean isCurrentlyActive) {
+			if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) {
+				getDefaultUIUtil().onDraw(c,recyclerView,conversationViewHolder.binding.frame,dX,dY,actionState,isCurrentlyActive);
+			}
 		}
 
 		@Override
-		public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
-									float dX, float dY, int actionState, boolean isCurrentlyActive) {
-			super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
-			if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
-				Paint paint = new Paint();
-				paint.setColor(MaterialColors.getColor(viewHolder.itemView, com.google.android.material.R.attr.colorSecondaryFixedDim));
-				paint.setStyle(Paint.Style.FILL);
-				c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop()
-						,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint);
+		public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+			if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) {
+				getDefaultUIUtil().clearView(conversationViewHolder.binding.frame);
 			}
 		}
 
 		@Override
-		public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
-			super.clearView(recyclerView, viewHolder);
-			viewHolder.itemView.setAlpha(1f);
+		public float getSwipeEscapeVelocity(final float defaultEscapeVelocity) {
+			return 32 * defaultEscapeVelocity;
 		}
 
 		@Override
-		public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+		public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
 			pendingActionHelper.execute();
 			int position = viewHolder.getLayoutPosition();
 			try {
@@ -291,7 +286,6 @@ public class ConversationsOverviewFragment extends XmppFragment {
 
 	@Override
 	public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-		this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity);
 		this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false);
 		this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity()));
 

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

@@ -43,6 +43,7 @@ import com.google.android.material.color.MaterialColors;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
 
 import com.rarepebble.colorpicker.ColorPickerView;
 
@@ -104,8 +105,14 @@ import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
-public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
-        OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
+public class EditAccountActivity extends OmemoActivity
+        implements OnAccountUpdate,
+                OnUpdateBlocklist,
+                OnKeyStatusUpdated,
+                OnCaptchaRequested,
+                KeyChainAliasCallback,
+                XmppConnectionService.OnShowErrorToast,
+                XmppConnectionService.OnMamPreferencesFetched {
 
     public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification";
     public static final String EXTRA_FORCE_REGISTER = "force_register";
@@ -122,37 +129,44 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     private boolean mUsernameMode = false;
     private boolean mShowOptions = false;
     private Account mAccount;
-    private final OnClickListener mCancelButtonClickListener = v -> {
-        deleteAccountAndReturnIfNecessary();
-        finish();
-    };
-    private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
+    private final OnClickListener mCancelButtonClickListener =
+            v -> {
+                deleteAccountAndReturnIfNecessary();
+                finish();
+            };
+    private final UiCallback<Avatar> mAvatarFetchCallback =
+            new UiCallback<Avatar>() {
 
-        @Override
-        public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
-            finishInitialSetup(avatar);
-        }
+                @Override
+                public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
+                    finishInitialSetup(avatar);
+                }
 
-        @Override
-        public void success(final Avatar avatar) {
-            finishInitialSetup(avatar);
-        }
+                @Override
+                public void success(final Avatar avatar) {
+                    finishInitialSetup(avatar);
+                }
 
-        @Override
-        public void error(final int errorCode, final Avatar avatar) {
-            finishInitialSetup(avatar);
-        }
-    };
-    private final OnClickListener mAvatarClickListener = new OnClickListener() {
-        @Override
-        public void onClick(final View view) {
-            if (mAccount != null) {
-                final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
-                intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
-                startActivity(intent);
-            }
-        }
-    };
+                @Override
+                public void error(final int errorCode, final Avatar avatar) {
+                    finishInitialSetup(avatar);
+                }
+            };
+    private final OnClickListener mAvatarClickListener =
+            new OnClickListener() {
+                @Override
+                public void onClick(final View view) {
+                    if (mAccount != null) {
+                        final Intent intent =
+                                new Intent(
+                                        getApplicationContext(),
+                                        PublishProfilePictureActivity.class);
+                        intent.putExtra(
+                                EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
+                        startActivity(intent);
+                    }
+                }
+            };
     private String messageFingerprint;
     private boolean mFetchingAvatar = false;
     private Toast mFetchingMamPrefsToast;
@@ -161,215 +175,268 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     private XmppUri pendingUri = null;
     private boolean mUseTor;
     private ActivityEditAccountBinding binding;
-    private String newPassword = null;
-    private final OnClickListener mSaveButtonClickListener = new OnClickListener() {
-
-        @Override
-        public void onClick(final View v) {
-            final String password = binding.accountPassword.getText().toString();
-            final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED;
-            final boolean accountInfoEdited = accountInfoEdited();
-
-            ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground();
-            if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) {
-                mAccount.setColor(previewColor.getColor());
-            }
+    private final OnClickListener mSaveButtonClickListener =
+            new OnClickListener() {
+
+                @Override
+                public void onClick(final View v) {
+                    final String password = binding.accountPassword.getText().toString();
+                    final boolean wasDisabled =
+                            mAccount != null && mAccount.getStatus() == Account.State.DISABLED;
+                    final boolean accountInfoEdited = accountInfoEdited();
+
+                    ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground();
+                    if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) {
+                        mAccount.setColor(previewColor.getColor());
+                    }
 
-            if (mInitMode && mAccount != null) {
-                mAccount.setOption(Account.OPTION_DISABLED, false);
-            }
-            if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) {
-                mAccount.setOption(Account.OPTION_SOFT_DISABLED, false);
-                mAccount.setOption(Account.OPTION_DISABLED, false);
-                if (!xmppConnectionService.updateAccount(mAccount)) {
-                    Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
-                }
-                return;
-            }
-            final boolean registerNewAccount;
-            if (mForceRegister != null) {
-                registerNewAccount = mForceRegister;
-            } else {
-                registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
-            }
-            if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) {
-                binding.accountJidLayout.setError(getString(R.string.invalid_username));
-                removeErrorsOnAllBut(binding.accountJidLayout);
-                binding.accountJid.requestFocus();
-                return;
-            }
+                    if (mInitMode && mAccount != null) {
+                        mAccount.setOption(Account.OPTION_DISABLED, false);
+                    }
+                    if (mAccount != null
+                            && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT)
+                                    .contains(mAccount.getStatus())
+                            && !accountInfoEdited) {
+                        mAccount.setOption(Account.OPTION_SOFT_DISABLED, false);
+                        mAccount.setOption(Account.OPTION_DISABLED, false);
+                        if (!xmppConnectionService.updateAccount(mAccount)) {
+                            Toast.makeText(
+                                            EditAccountActivity.this,
+                                            R.string.unable_to_update_account,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                        }
+                        return;
+                    }
+                    final boolean registerNewAccount;
+                    if (mForceRegister != null) {
+                        registerNewAccount = mForceRegister;
+                    } else {
+                        registerNewAccount =
+                                binding.accountRegisterNew.isChecked()
+                                        && !Config.DISALLOW_REGISTRATION_IN_UI;
+                    }
+                    if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) {
+                        binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                        removeErrorsOnAllBut(binding.accountJidLayout);
+                        binding.accountJid.requestFocus();
+                        return;
+                    }
 
-            XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-            final boolean startOrbot = mAccount != null && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE;
-            if (startOrbot) {
-                if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) {
-                    TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT);
-                } else {
-                    TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT);
-                }
-                return;
-            }
+                    XmppConnection connection =
+                            mAccount == null ? null : mAccount.getXmppConnection();
+                    final boolean startOrbot =
+                            mAccount != null
+                                    && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE;
+                    if (startOrbot) {
+                        if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) {
+                            TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT);
+                        } else {
+                            TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT);
+                        }
+                        return;
+                    }
 
-            if (inNeedOfSaslAccept()) {
-                mAccount.resetPinnedMechanism();
-                if (!xmppConnectionService.updateAccount(mAccount)) {
-                    Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
-                }
-                return;
-            }
+                    if (inNeedOfSaslAccept()) {
+                        mAccount.resetPinnedMechanism();
+                        if (!xmppConnectionService.updateAccount(mAccount)) {
+                            Toast.makeText(
+                                            EditAccountActivity.this,
+                                            R.string.unable_to_update_account,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                        }
+                        return;
+                    }
 
-            final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
-            final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
-            final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
-            final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null;
-            if (url != null && !wasDisabled) {
-                try {
-                    startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
-                    return;
-                } catch (ActivityNotFoundException e) {
-                    Toast.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, Toast.LENGTH_SHORT).show();
-                    return;
-                }
-            }
+                    final boolean openRegistrationUrl =
+                            registerNewAccount
+                                    && !accountInfoEdited
+                                    && mAccount != null
+                                    && mAccount.getStatus() == Account.State.REGISTRATION_WEB;
+                    final boolean openPaymentUrl =
+                            mAccount != null
+                                    && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED;
+                    final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl;
+                    final HttpUrl url =
+                            connection != null && redirectionWorthyStatus
+                                    ? connection.getRedirectionUrl()
+                                    : null;
+                    if (url != null && !wasDisabled) {
+                        try {
+                            startActivity(
+                                    new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString())));
+                            return;
+                        } catch (ActivityNotFoundException e) {
+                            Toast.makeText(
+                                            EditAccountActivity.this,
+                                            R.string.application_found_to_open_website,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                            return;
+                        }
+                    }
 
-            final Jid jid;
-            try {
-                if (mUsernameMode) {
-                    jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null);
-                } else {
-                    jid = Jid.ofEscaped(binding.accountJid.getText().toString());
-                    Resolver.checkDomain(jid);
-                }
-            } catch (final NullPointerException | IllegalArgumentException e) {
-                if (mUsernameMode) {
-                    binding.accountJidLayout.setError(getString(R.string.invalid_username));
-                } else {
-                    binding.accountJidLayout.setError(getString(R.string.invalid_jid));
-                }
-                binding.accountJid.requestFocus();
-                removeErrorsOnAllBut(binding.accountJidLayout);
-                return;
-            }
-            final String hostname;
-            int numericPort = 5222;
-            if (mShowOptions) {
-                hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText());
-                final String port = CharMatcher.whitespace().removeFrom(binding.port.getText());
-                if (Resolver.invalidHostname(hostname)) {
-                    binding.hostnameLayout.setError(getString(R.string.not_valid_hostname));
-                    binding.hostname.requestFocus();
-                    removeErrorsOnAllBut(binding.hostnameLayout);
-                    return;
-                }
-                if (!hostname.isEmpty()) {
+                    final Jid jid;
                     try {
-                        numericPort = Integer.parseInt(port);
-                        if (numericPort < 0 || numericPort > 65535) {
-                            binding.portLayout.setError(getString(R.string.not_a_valid_port));
-                            removeErrorsOnAllBut(binding.portLayout);
-                            binding.port.requestFocus();
+                        if (mUsernameMode) {
+                            jid =
+                                    Jid.ofEscaped(
+                                            binding.accountJid.getText().toString(),
+                                            getUserModeDomain(),
+                                            null);
+                        } else {
+                            jid = Jid.ofEscaped(binding.accountJid.getText().toString());
+                            Resolver.checkDomain(jid);
+                        }
+                    } catch (final NullPointerException | IllegalArgumentException e) {
+                        if (mUsernameMode) {
+                            binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                        } else {
+                            binding.accountJidLayout.setError(getString(R.string.invalid_jid));
+                        }
+                        binding.accountJid.requestFocus();
+                        removeErrorsOnAllBut(binding.accountJidLayout);
+                        return;
+                    }
+                    final String hostname;
+                    int numericPort = 5222;
+                    if (mShowOptions) {
+                        hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText());
+                        final String port =
+                                CharMatcher.whitespace().removeFrom(binding.port.getText());
+                        if (Resolver.invalidHostname(hostname)) {
+                            binding.hostnameLayout.setError(getString(R.string.not_valid_hostname));
+                            binding.hostname.requestFocus();
+                            removeErrorsOnAllBut(binding.hostnameLayout);
                             return;
                         }
+                        if (!hostname.isEmpty()) {
+                            try {
+                                numericPort = Integer.parseInt(port);
+                                if (numericPort < 0 || numericPort > 65535) {
+                                    binding.portLayout.setError(
+                                            getString(R.string.not_a_valid_port));
+                                    removeErrorsOnAllBut(binding.portLayout);
+                                    binding.port.requestFocus();
+                                    return;
+                                }
+
+                            } catch (NumberFormatException e) {
+                                binding.portLayout.setError(getString(R.string.not_a_valid_port));
+                                removeErrorsOnAllBut(binding.portLayout);
+                                binding.port.requestFocus();
+                                return;
+                            }
+                        }
+                    } else {
+                        hostname = null;
+                    }
 
-                    } catch (NumberFormatException e) {
-                        binding.portLayout.setError(getString(R.string.not_a_valid_port));
-                        removeErrorsOnAllBut(binding.portLayout);
-                        binding.port.requestFocus();
+                    if (jid.getLocal() == null) {
+                        if (mUsernameMode) {
+                            binding.accountJidLayout.setError(getString(R.string.invalid_username));
+                        } else {
+                            binding.accountJidLayout.setError(getString(R.string.invalid_jid));
+                        }
+                        removeErrorsOnAllBut(binding.accountJidLayout);
+                        binding.accountJid.requestFocus();
                         return;
                     }
+                    if (mAccount != null) {
+                        if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
+                            mAccount.setOption(
+                                    Account.OPTION_MAGIC_CREATE,
+                                    mAccount.getPassword().contains(password));
+                        }
+                        mAccount.setJid(jid);
+                        mAccount.setPort(numericPort);
+                        mAccount.setHostname(hostname);
+                        binding.accountJidLayout.setError(null);
+                        mAccount.setPassword(password);
+                        mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+                        if (!xmppConnectionService.updateAccount(mAccount)) {
+                            Toast.makeText(
+                                            EditAccountActivity.this,
+                                            R.string.unable_to_update_account,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                            return;
+                        }
+                    } else {
+                        if (xmppConnectionService.findAccountByJid(jid) != null) {
+                            binding.accountJidLayout.setError(
+                                    getString(R.string.account_already_exists));
+                            removeErrorsOnAllBut(binding.accountJidLayout);
+                            binding.accountJid.requestFocus();
+                            return;
+                        }
+                        mAccount = new Account(jid.asBareJid(), password);
+                        mAccount.setPort(numericPort);
+                        mAccount.setHostname(hostname);
+                        mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
+                        xmppConnectionService.createAccount(mAccount);
+                    }
+                    binding.hostnameLayout.setError(null);
+                    binding.portLayout.setError(null);
+                    if (mAccount.isOnion()) {
+                        Toast.makeText(
+                                        EditAccountActivity.this,
+                                        R.string.audio_video_disabled_tor,
+                                        Toast.LENGTH_LONG)
+                                .show();
+                    }
+                    if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) {
+                        finish();
+                    } else {
+                        updateSaveButton();
+                        updateAccountInformation(true);
+                    }
                 }
-            } else {
-                hostname = null;
-            }
-
-            if (jid.getLocal() == null) {
-                if (mUsernameMode) {
-                    binding.accountJidLayout.setError(getString(R.string.invalid_username));
-                } else {
-                    binding.accountJidLayout.setError(getString(R.string.invalid_jid));
-                }
-                removeErrorsOnAllBut(binding.accountJidLayout);
-                binding.accountJid.requestFocus();
-                return;
-            }
-            if (mAccount != null) {
-                if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
-                    mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password));
-                }
-                mAccount.setJid(jid);
-                mAccount.setPort(numericPort);
-                mAccount.setHostname(hostname);
-                binding.accountJidLayout.setError(null);
-                mAccount.setPassword(password);
-                mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
-                if (!xmppConnectionService.updateAccount(mAccount)) {
-                    Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
-                    return;
-                }
-            } else {
-                if (xmppConnectionService.findAccountByJid(jid) != null) {
-                    binding.accountJidLayout.setError(getString(R.string.account_already_exists));
-                    removeErrorsOnAllBut(binding.accountJidLayout);
-                    binding.accountJid.requestFocus();
-                    return;
+            };
+    private final TextWatcher mTextWatcher =
+            new TextWatcher() {
+
+                @Override
+                public void onTextChanged(
+                        final CharSequence s, final int start, final int before, final int count) {
+                    updatePortLayout();
+                    updateSaveButton();
                 }
-                mAccount = new Account(jid.asBareJid(), password);
-                mAccount.setPort(numericPort);
-                mAccount.setHostname(hostname);
-                mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
-                xmppConnectionService.createAccount(mAccount);
-            }
-            binding.hostnameLayout.setError(null);
-            binding.portLayout.setError(null);
-            if (mAccount.isOnion()) {
-                Toast.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, Toast.LENGTH_LONG).show();
-            }
-            if (mAccount.isEnabled()
-                    && !registerNewAccount
-                    && !mInitMode) {
-                finish();
-            } else {
-                updateSaveButton();
-                updateAccountInformation(true);
-            }
 
-        }
-    };
-    private final TextWatcher mTextWatcher = new TextWatcher() {
-
-        @Override
-        public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
-            updatePortLayout();
-            updateSaveButton();
-        }
-
-        @Override
-        public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
-        }
-
-        @Override
-        public void afterTextChanged(final Editable s) {
-
-        }
-    };
-    private final View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() {
-        @Override
-        public void onFocusChange(View view, boolean b) {
-            EditText et = (EditText) view;
-            if (b) {
-                int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id;
-                if (view.getId() == R.id.hostname) {
-                    resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example;
+                @Override
+                public void beforeTextChanged(
+                        final CharSequence s, final int start, final int count, final int after) {}
+
+                @Override
+                public void afterTextChanged(final Editable s) {}
+            };
+    private final View.OnFocusChangeListener mEditTextFocusListener =
+            new View.OnFocusChangeListener() {
+                @Override
+                public void onFocusChange(View view, boolean b) {
+                    EditText et = (EditText) view;
+                    if (b) {
+                        int resId =
+                                mUsernameMode
+                                        ? R.string.username
+                                        : R.string.account_settings_example_jabber_id;
+                        if (view.getId() == R.id.hostname) {
+                            resId =
+                                    mUseTor
+                                            ? R.string.hostname_or_onion
+                                            : R.string.hostname_example;
+                        }
+                        final int res = resId;
+                        new Handler().postDelayed(() -> et.setHint(res), 200);
+                    } else {
+                        et.setHint(null);
+                    }
                 }
-                final int res = resId;
-                new Handler().postDelayed(() -> et.setHint(res), 200);
-            } else {
-                et.setHint(null);
-            }
-        }
-    };
+            };
 
-    private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) {
+    private static void setAvailabilityRadioButton(
+            Presence.Status status, DialogPresenceBinding binding) {
         if (status == null) {
             binding.online.setChecked(true);
             return;
@@ -403,9 +470,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
     public void refreshUiReal() {
         invalidateOptionsMenu();
-        if (mAccount != null
-                && mAccount.getStatus() != Account.State.ONLINE
-                && mFetchingAvatar) {
+        if (mAccount != null && mAccount.getStatus() != Account.State.ONLINE && mFetchingAvatar) {
             Intent intent = new Intent(this, StartConversationActivity.class);
             StartConversationActivity.addInviteUri(intent, getIntent());
             startActivity(intent);
@@ -435,30 +500,41 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private void deleteAccountAndReturnIfNecessary() {
-        if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
+        if (mInitMode
+                && mAccount != null
+                && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) {
             xmppConnectionService.deleteAccount(mAccount);
         }
 
-        final boolean magicCreate = mAccount != null && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
+        final boolean magicCreate =
+                mAccount != null
+                        && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
+                        && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
         final Jid jid = mAccount == null ? null : mAccount.getJid();
 
-        if (SignupUtils.isSupportTokenRegistry() && jid != null && magicCreate && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) {
+        if (SignupUtils.isSupportTokenRegistry()
+                && jid != null
+                && magicCreate
+                && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) {
             final Jid preset;
             if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) {
                 preset = jid.asBareJid();
             } else {
                 preset = jid.getDomain();
             }
-            final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
+            final Intent intent =
+                    SignupUtils.getTokenRegistrationIntent(
+                            this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
             StartConversationActivity.addInviteUri(intent, getIntent());
             startActivity(intent);
             return;
         }
 
-
-        final List<Account> accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts();
+        final List<Account> accounts =
+                xmppConnectionService == null ? null : xmppConnectionService.getAccounts();
         if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
-            Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister);
+            Intent intent =
+                    SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister);
             StartConversationActivity.addInviteUri(intent, getIntent());
             startActivity(intent);
         }
@@ -470,27 +546,38 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     protected void finishInitialSetup(final Avatar avatar) {
-        runOnUiThread(() -> {
-            SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
-            final Intent intent;
-            final XmppConnection connection = mAccount.getXmppConnection();
-            final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1;
-            if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
-                intent = new Intent(getApplicationContext(), StartConversationActivity.class);
-                intent.putExtra("init", true);
-                intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
-            } else {
-                intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
-                intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
-                intent.putExtra("setup", true);
-            }
-            if (wasFirstAccount) {
-                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-            }
-            StartConversationActivity.addInviteUri(intent, getIntent());
-            startActivity(intent);
-            finish();
-        });
+        runOnUiThread(
+                () -> {
+                    SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
+                    final Intent intent;
+                    final XmppConnection connection = mAccount.getXmppConnection();
+                    final boolean wasFirstAccount =
+                            xmppConnectionService != null
+                                    && xmppConnectionService.getAccounts().size() == 1;
+                    if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
+                        intent =
+                                new Intent(
+                                        getApplicationContext(), StartConversationActivity.class);
+                        intent.putExtra("init", true);
+                        intent.putExtra(
+                                EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
+                    } else {
+                        intent =
+                                new Intent(
+                                        getApplicationContext(),
+                                        PublishProfilePictureActivity.class);
+                        intent.putExtra(
+                                EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
+                        intent.putExtra("setup", true);
+                    }
+                    if (wasFirstAccount) {
+                        intent.setFlags(
+                                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                    }
+                    StartConversationActivity.addInviteUri(intent, getIntent());
+                    startActivity(intent);
+                    finish();
+                });
     }
 
     @Override
@@ -514,8 +601,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         if (requestCode == REQUEST_UNLOCK) {
             if (resultCode == RESULT_OK) {
                 openChangePassword(true);
-            } else {
-                this.newPassword = null;
             }
         }
     }
@@ -526,7 +611,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) {
-        if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+        if (mAccount != null
+                && mAccount.getJid().asBareJid().equals(uri.getJid())
+                && uri.hasFingerprints()) {
             if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) {
                 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
                 updateAccountInformation(false);
@@ -553,10 +640,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             this.binding.saveButton.setText(R.string.save);
             this.binding.saveButton.setEnabled(true);
         } else if (mAccount != null
-                && (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) {
+                && (mAccount.getStatus() == Account.State.CONNECTING
+                        || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL
+                        || mFetchingAvatar)) {
             this.binding.saveButton.setEnabled(false);
             this.binding.saveButton.setText(R.string.account_status_connecting);
-        } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
+        } else if (mAccount != null
+                && mAccount.getStatus() == Account.State.DISABLED
+                && !mInitMode) {
             this.binding.saveButton.setEnabled(true);
             this.binding.saveButton.setText(R.string.enable);
         } else if (torNeedsInstall(mAccount)) {
@@ -574,8 +665,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                         this.binding.saveButton.setEnabled(false);
                     }
                 } else {
-                    XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-                    HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null;
+                    XmppConnection connection =
+                            mAccount == null ? null : mAccount.getXmppConnection();
+                    HttpUrl url =
+                            connection != null
+                                            && mAccount.getStatus()
+                                                    == Account.State.PAYMENT_REQUIRED
+                                    ? connection.getRedirectionUrl()
+                                    : null;
                     if (url != null) {
                         this.binding.saveButton.setText(R.string.open_website);
                     } else if (inNeedOfSaslAccept()) {
@@ -586,8 +683,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 }
             } else {
                 XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection();
-                HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null;
-                if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) {
+                HttpUrl url =
+                        connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB
+                                ? connection.getRedirectionUrl()
+                                : null;
+                if (url != null
+                        && this.binding.accountRegisterNew.isChecked()
+                        && !accountInfoEdited) {
                     this.binding.saveButton.setText(R.string.open_website);
                 } else {
                     this.binding.saveButton.setText(R.string.next);
@@ -597,7 +699,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private boolean torNeedsInstall(final Account account) {
-        return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE && !TorServiceUtils.isOrbotInstalled(this);
+        return account != null
+                && account.getStatus() == Account.State.TOR_NOT_AVAILABLE
+                && !TorServiceUtils.isOrbotInstalled(this);
     }
 
     private boolean torNeedsStart(final Account account) {
@@ -609,11 +713,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             return false;
         }
         ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground();
-        return jidEdited() ||
-                !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) ||
-                !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) ||
-                this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) ||
-                !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString());
+        return jidEdited()
+                || !this.mAccount
+                        .getPassword()
+                        .equals(this.binding.accountPassword.getText().toString())
+                || !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString())
+                || this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor())
+                || !String.valueOf(this.mAccount.getPort())
+                        .equals(this.binding.port.getText().toString());
     }
 
     protected boolean jidEdited() {
@@ -660,7 +767,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) {
             changeMoreTableVisibility(true);
         }
-        final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> updateSaveButton();
+        final OnCheckedChangeListener OnCheckedShowConfirmPassword =
+                (buttonView, isChecked) -> updateSaveButton();
         this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
         if (Config.DISALLOW_REGISTRATION_IN_UI) {
             this.binding.accountRegisterNew.setVisibility(View.GONE);
@@ -703,18 +811,23 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private void onEditYourNameClicked(View view) {
-        quickEdit(mAccount.getDisplayName(), R.string.your_name, value -> {
-            final String displayName = value.trim();
-            updateDisplayName(displayName);
-            mAccount.setDisplayName(displayName);
-            xmppConnectionService.publishDisplayName(mAccount);
-            refreshAvatar();
-            return null;
-        }, true);
+        quickEdit(
+                mAccount.getDisplayName(),
+                R.string.your_name,
+                value -> {
+                    final String displayName = value.trim();
+                    updateDisplayName(displayName);
+                    mAccount.setDisplayName(displayName);
+                    xmppConnectionService.publishDisplayName(mAccount);
+                    refreshAvatar();
+                    return null;
+                },
+                true);
     }
 
     private void refreshAvatar() {
-        AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
+        AvatarWorkerTask.loadAvatar(
+                mAccount, binding.avater, R.dimen.avatar_on_details_screen_size);
     }
 
     @Override
@@ -789,9 +902,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 }
             }
             boolean init = intent.getBooleanExtra("init", false);
-            boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
+            boolean openedFromNotification =
+                    intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false);
             Log.d(Config.LOGTAG, "extras " + intent.getExtras());
-            this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) : null;
+            this.mForceRegister =
+                    intent.hasExtra(EXTRA_FORCE_REGISTER)
+                            ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false)
+                            : null;
             Log.d(Config.LOGTAG, "force register=" + mForceRegister);
             this.mInitMode = init || this.jidToEdit == null;
             this.messageFingerprint = intent.getStringExtra("fingerprint");
@@ -801,7 +918,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 configureActionBar(getSupportActionBar(), !openedFromNotification);
             } else {
                 this.binding.avater.setVisibility(View.GONE);
-                configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
+                configureActionBar(
+                        getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null));
                 if (mForceRegister != null) {
                     if (mForceRegister) {
                         setTitle(R.string.register_new_account);
@@ -833,13 +951,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         TextView warning = view.findViewById(R.id.warning);
         warning.setText(R.string.verifying_omemo_keys_trusted_source_account);
         builder.setView(view);
-        builder.setPositiveButton(R.string.continue_btn, (dialog, which) -> {
-            if (isTrustedSource.isChecked()) {
-                processFingerprintVerification(xmppUri, false);
-            } else {
-                finish();
-            }
-        });
+        builder.setPositiveButton(
+                R.string.continue_btn,
+                (dialog, which) -> {
+                    if (isTrustedSource.isChecked()) {
+                        processFingerprintVerification(xmppUri, false);
+                    } else {
+                        finish();
+                    }
+                });
         builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish());
         final var dialog = builder.create();
         dialog.setCanceledOnTouchOutside(false);
@@ -863,9 +983,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     @Override
     public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
         if (mAccount != null) {
-            savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString());
+            savedInstanceState.putString(
+                    "account", mAccount.getJid().asBareJid().toEscapedString());
             savedInstanceState.putBoolean("initMode", mInitMode);
-            savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
+            savedInstanceState.putBoolean(
+                    "showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
         }
         super.onSaveInstanceState(savedInstanceState);
     }
@@ -874,7 +996,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         boolean init = true;
         if (mSavedInstanceAccount != null) {
             try {
-                this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount));
+                this.mAccount =
+                        xmppConnectionService.findAccountByJid(
+                                Jid.ofEscaped(mSavedInstanceAccount));
                 this.mInitMode = mSavedInstanceInit;
                 init = false;
             } catch (IllegalArgumentException e) {
@@ -887,7 +1011,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
         if (mAccount != null) {
             this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER);
-            this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER);
+            this.mUsernameMode |=
+                    mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)
+                            && mAccount.isOptionSet(Account.OPTION_REGISTER);
             if (mPendingFingerprintVerificationUri != null) {
                 processFingerprintVerification(mPendingFingerprintVerificationUri, false);
                 mPendingFingerprintVerificationUri = null;
@@ -895,16 +1021,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             updateAccountInformation(init);
         }
 
-
-        if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) {
+        if (Config.MAGIC_CREATE_DOMAIN == null
+                && this.xmppConnectionService.getAccounts().size() == 0) {
             this.binding.cancelButton.setEnabled(false);
         }
         if (mUsernameMode) {
             this.binding.accountJidLayout.setHint(getString(R.string.username_hint));
         } else {
-            final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
-                    R.layout.item_autocomplete,
-                    xmppConnectionService.getKnownHosts());
+            final KnownHostsAdapter mKnownHostsAdapter =
+                    new KnownHostsAdapter(
+                            this,
+                            R.layout.item_autocomplete,
+                            xmppConnectionService.getKnownHosts());
             this.binding.accountJid.setAdapter(mKnownHostsAdapter);
         }
 
@@ -952,7 +1080,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 shareLink(false);
                 break;
             case R.id.action_change_password_on_server:
-                gotoChangePassword(null);
+                gotoChangePassword();
                 break;
             case R.id.action_delete_account:
                 deleteAccount();
@@ -971,13 +1099,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
     }
 
     private void deleteAccount() {
-        this.deleteAccount(mAccount,()->{
-            finish();
-        });
+        this.deleteAccount(
+                mAccount,
+                () -> {
+                    finish();
+                });
     }
 
     private boolean inNeedOfSaslAccept() {
-        return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited();
+        return mAccount != null
+                && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK
+                && mAccount.getPinnedMechanismPriority() >= 0
+                && !accountInfoEdited();
     }
 
     private void shareBarcode() {
@@ -988,12 +1121,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
     }
 
-    private void changeMoreTableVisibility(boolean visible) {
+    private void changeMoreTableVisibility(final boolean visible) {
         binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE);
+        binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE);
     }
 
-    private void gotoChangePassword(String newPassword) {
-        this.newPassword = newPassword;
+    private void gotoChangePassword() {
         KeyguardManager keyguardManager = (KeyguardManager) this.getSystemService(Context.KEYGUARD_SERVICE);
         Intent credentialsIntent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock required", "Please unlock in order to change your password");
         if (credentialsIntent == null) {
@@ -1007,10 +1140,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
         final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
         changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString());
         changePasswordIntent.putExtra("did_unlock", didUnlock);
-        if (newPassword != null) {
-            changePasswordIntent.putExtra("password", newPassword);
-        }
-        this.newPassword = null;
         startActivity(changePasswordIntent);
     }
 
@@ -1020,9 +1149,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
 
     private void changePresence() {
         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
-        boolean manualStatus = sharedPreferences.getBoolean(AppSettings.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
+        boolean manualStatus =
+                sharedPreferences.getBoolean(
+                        AppSettings.MANUALLY_CHANGE_PRESENCE,
+                        getResources().getBoolean(R.bool.manually_change_presence));
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
-        final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
+        final DialogPresenceBinding binding =
+                DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false);
         String current = mAccount.getPresenceStatusMessage();
         if (current != null && !current.trim().isEmpty()) {
             binding.statusMessage.append(current);

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

@@ -111,8 +111,13 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe
 
     private boolean startRecording() {
         mRecorder = new MediaRecorder();
-        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
         final String userChosenCodec = getPreferences().getString("voice_message_codec", "");
+        try {
+            mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+        } catch (final RuntimeException e) {
+            Log.e(Config.LOGTAG,"could not set audio source", e);
+            return false;
+        }
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
             mRecorder.setPrivacySensitive(true);
         }

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

@@ -34,6 +34,7 @@ import androidx.databinding.DataBindingUtil;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -363,10 +364,15 @@ public class RtpSessionActivity extends XmppActivity
 
     private void acceptContentAdd() {
         try {
-            requireRtpConnection()
-                    .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary);
+            final ContentAddition pendingContentAddition =
+                    requireRtpConnection().getPendingContentAddition();
+            if (pendingContentAddition == null) {
+                Log.d(Config.LOGTAG, "content offer was gone after granting permission");
+                return;
+            }
+            requireRtpConnection().acceptContentAdd(pendingContentAddition.summary);
         } catch (final IllegalStateException e) {
-            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+            Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show();
         }
     }
 
@@ -537,7 +543,12 @@ public class RtpSessionActivity extends XmppActivity
         final String action = intent.getAction();
         Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")");
         final Account account = extractAccount(intent);
-        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
+        final var extraWith = intent.getStringExtra(EXTRA_WITH);
+        final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.ofEscaped(extraWith);
+        if (with == null || account == null) {
+            Log.e(Config.LOGTAG, "intent is missing extras (account or with)");
+            return;
+        }
         final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
         if (sessionId != null) {
             if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
@@ -1089,16 +1100,21 @@ public class RtpSessionActivity extends XmppActivity
             final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
         switch (selectedAudioDevice) {
             case EARPIECE -> {
-                this.binding.inCallActionRight.setImageResource(
-                        R.drawable.ic_volume_off_24dp);
+                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp);
                 if (numberOfChoices >= 2) {
+                    this.binding.inCallActionRight.setContentDescription(
+                            getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker));
                     this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
                 } else {
+                    this.binding.inCallActionRight.setContentDescription(
+                            getString(R.string.call_is_using_earpiece));
                     this.binding.inCallActionRight.setOnClickListener(null);
                     this.binding.inCallActionRight.setClickable(false);
                 }
             }
             case WIRED_HEADSET -> {
+                this.binding.inCallActionRight.setContentDescription(
+                        getString(R.string.call_is_using_wired_headset));
                 this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp);
                 this.binding.inCallActionRight.setOnClickListener(null);
                 this.binding.inCallActionRight.setClickable(false);
@@ -1106,15 +1122,20 @@ public class RtpSessionActivity extends XmppActivity
             case SPEAKER_PHONE -> {
                 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp);
                 if (numberOfChoices >= 2) {
+                    this.binding.inCallActionRight.setContentDescription(
+                            getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece));
                     this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
                 } else {
+                    this.binding.inCallActionRight.setContentDescription(
+                            getString(R.string.call_is_using_speaker));
                     this.binding.inCallActionRight.setOnClickListener(null);
                     this.binding.inCallActionRight.setClickable(false);
                 }
             }
             case BLUETOOTH -> {
-                this.binding.inCallActionRight.setImageResource(
-                        R.drawable.ic_bluetooth_audio_24dp);
+                this.binding.inCallActionRight.setContentDescription(
+                        getString(R.string.call_is_using_bluetooth));
+                this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp);
                 this.binding.inCallActionRight.setOnClickListener(null);
                 this.binding.inCallActionRight.setClickable(false);
             }
@@ -1131,15 +1152,21 @@ public class RtpSessionActivity extends XmppActivity
                     R.drawable.ic_flip_camera_android_24dp);
             this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
             this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
+            this.binding.inCallActionFarRight.setContentDescription(
+                    getString(R.string.flip_camera));
         } else {
             this.binding.inCallActionFarRight.setVisibility(View.GONE);
         }
         if (videoEnabled) {
             this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp);
             this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
+            this.binding.inCallActionRight.setContentDescription(
+                    getString(R.string.video_is_enabled_tap_to_disable));
         } else {
             this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp);
             this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
+            this.binding.inCallActionRight.setContentDescription(
+                    getString(R.string.video_is_disabled_tap_to_enable));
         }
     }
 
@@ -1168,7 +1195,7 @@ public class RtpSessionActivity extends XmppActivity
                 MainThreadExecutor.getInstance());
     }
 
-    private void enableVideo(View view) {
+    private void enableVideo(final View view) {
         try {
             requireRtpConnection().setVideoEnabled(true);
         } catch (final IllegalStateException e) {
@@ -1178,14 +1205,19 @@ public class RtpSessionActivity extends XmppActivity
         updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
     }
 
-    private void disableVideo(View view) {
+    private void disableVideo(final View view) {
         final JingleRtpConnection rtpConnection = requireRtpConnection();
         final ContentAddition pending = rtpConnection.getPendingContentAddition();
         if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
             rtpConnection.retractContentAdd();
             return;
         }
-        requireRtpConnection().setVideoEnabled(false);
+        try {
+            requireRtpConnection().setVideoEnabled(false);
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, R.string.could_not_disable_video, Toast.LENGTH_SHORT).show();
+            return;
+        }
         updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
     }
 
@@ -1326,13 +1358,21 @@ public class RtpSessionActivity extends XmppActivity
     }
 
     private void switchToEarpiece(final View view) {
-        requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
-        acquireProximityWakeLock();
+        try {
+            requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
+            acquireProximityWakeLock();
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+        }
     }
 
     private void switchToSpeaker(final View view) {
-        requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
-        releaseProximityWakeLock();
+        try {
+            requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
+            releaseProximityWakeLock();
+        } catch (final IllegalStateException e) {
+            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+        }
     }
 
     private void retry(final View view) {

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

@@ -36,7 +36,6 @@ import android.widget.AutoCompleteTextView;
 import android.widget.CheckBox;
 import android.widget.EditText;
 import android.widget.ListView;
-import android.widget.Spinner;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -47,7 +46,7 @@ import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.widget.PopupMenu;
-import androidx.core.content.ContextCompat;
+import androidx.core.app.ActivityCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
@@ -63,21 +62,11 @@ import com.cheogram.android.FinishOnboarding;
 import com.google.android.material.color.MaterialColors;
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.leinardi.android.speeddial.SpeedDialActionItem;
 import com.leinardi.android.speeddial.SpeedDialView;
 
-import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
-
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -109,11 +98,29 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
-public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+public class StartConversationActivity extends XmppActivity
+        implements XmppConnectionService.OnConversationUpdate,
+                OnRosterUpdate,
+                OnUpdateBlocklist,
+                CreatePrivateGroupChatDialog.CreateConferenceDialogListener,
+                JoinConferenceDialog.JoinConferenceDialogListener,
+                SwipeRefreshLayout.OnRefreshListener,
+                CreatePublicChannelDialog.CreatePublicChannelDialogListener {
 
-    private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
+    private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT =
+            "contact_list_integration_consent";
 
     public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
 
@@ -135,126 +142,138 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
     private boolean mHideOfflineContacts = false;
     private boolean createdByViewIntent = false;
-    private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
-
-        @Override
-        public boolean onMenuItemActionExpand(MenuItem item) {
-            mSearchEditText.post(() -> {
-                updateSearchViewHint();
-                mSearchEditText.requestFocus();
-                if (oneShotKeyboardSuppress.compareAndSet(true, false)) {
-                    return;
-                }
-                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
-                if (imm != null) {
-                    imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
+    private final MenuItem.OnActionExpandListener mOnActionExpandListener =
+            new MenuItem.OnActionExpandListener() {
+
+                @Override
+                public boolean onMenuItemActionExpand(@NonNull final MenuItem item) {
+                    mSearchEditText.post(
+                            () -> {
+                                updateSearchViewHint();
+                                mSearchEditText.requestFocus();
+                                if (oneShotKeyboardSuppress.compareAndSet(true, false)) {
+                                    return;
+                                }
+                                InputMethodManager imm =
+                                        (InputMethodManager)
+                                                getSystemService(Context.INPUT_METHOD_SERVICE);
+                                if (imm != null) {
+                                    imm.showSoftInput(
+                                            mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
+                                }
+                            });
+                    if (binding.speedDial.isOpen()) {
+                        binding.speedDial.close();
+                    }
+                    return true;
                 }
-            });
-            if (binding.speedDial.isOpen()) {
-                binding.speedDial.close();
-            }
-            return true;
-        }
 
-        @Override
-        public boolean onMenuItemActionCollapse(MenuItem item) {
-            SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
-            mSearchEditText.setText("");
-            filter(null);
-            navigateBack();
-            return true;
-        }
-    };
-    private final TextWatcher mSearchTextWatcher = new TextWatcher() {
+                @Override
+                public boolean onMenuItemActionCollapse(@NonNull final MenuItem item) {
+                    SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
+                    mSearchEditText.setText("");
+                    filter(null);
+                    navigateBack();
+                    return true;
+                }
+            };
+    private final TextWatcher mSearchTextWatcher =
+            new TextWatcher() {
 
-        @Override
-        public void afterTextChanged(Editable editable) {
-            filter(editable.toString());
-        }
+                @Override
+                public void afterTextChanged(Editable editable) {
+                    filter(editable.toString());
+                }
 
-        @Override
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-        }
+                @Override
+                public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 
-        @Override
-        public void onTextChanged(CharSequence s, int start, int before, int count) {
-        }
-    };
+                @Override
+                public void onTextChanged(CharSequence s, int start, int before, int count) {}
+            };
     private MenuItem mMenuSearchView;
-    private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
-        @Override
-        public void onTagClicked(String tag) {
-            if (mMenuSearchView != null) {
-                mMenuSearchView.expandActionView();
-                mSearchEditText.setText("");
-                mSearchEditText.append(tag);
-                filter(tag);
-            }
-        }
-    };
+    private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener =
+            new ListItemAdapter.OnTagClickedListener() {
+                @Override
+                public void onTagClicked(String tag) {
+                    if (mMenuSearchView != null) {
+                        mMenuSearchView.expandActionView();
+                        mSearchEditText.setText("");
+                        mSearchEditText.append(tag);
+                        filter(tag);
+                    }
+                }
+            };
     private Pair<Integer, Intent> mPostponedActivityResult;
     private Toast mToast;
-    private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
-        @Override
-        public void success(final Conversation conversation) {
-            runOnUiThread(() -> {
-                hideToast();
-                switchToConversation(conversation);
-            });
-        }
-
-        @Override
-        public void error(final int errorCode, Conversation object) {
-            runOnUiThread(() -> replaceToast(getString(errorCode)));
-        }
+    private final UiCallback<Conversation> mAdhocConferenceCallback =
+            new UiCallback<>() {
+                @Override
+                public void success(final Conversation conversation) {
+                    runOnUiThread(
+                            () -> {
+                                hideToast();
+                                switchToConversation(conversation);
+                            });
+                }
 
-        @Override
-        public void userInputRequired(PendingIntent pi, Conversation object) {
+                @Override
+                public void error(final int errorCode, Conversation object) {
+                    runOnUiThread(() -> replaceToast(getString(errorCode)));
+                }
 
-        }
-    };
+                @Override
+                public void userInputRequired(PendingIntent pi, Conversation object) {}
+            };
     private ActivityStartConversationBinding binding;
-    private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
-        @Override
-        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-            int pos = binding.startConversationViewPager.getCurrentItem();
-            if (pos == 0) {
-                if (contacts.size() == 1) {
-                    openConversation(contacts.get(0));
-                    return true;
-                } else if (contacts.size() == 0 && conferences.size() == 1) {
-                    openConversationsForBookmark((Bookmark) conferences.get(0));
-                    return true;
-                }
-            } else {
-                if (conferences.size() == 1) {
-                    openConversationsForBookmark((Bookmark) conferences.get(0));
-                    return true;
-                } else if (conferences.size() == 0 && contacts.size() == 1) {
-                    openConversation(contacts.get(0));
+    private final TextView.OnEditorActionListener mSearchDone =
+            new TextView.OnEditorActionListener() {
+                @Override
+                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                    int pos = binding.startConversationViewPager.getCurrentItem();
+                    if (pos == 0) {
+                        if (contacts.size() == 1) {
+                            openConversation(contacts.get(0));
+                            return true;
+                        } else if (contacts.isEmpty() && conferences.size() == 1) {
+                            openConversationsForBookmark((Bookmark) conferences.get(0));
+                            return true;
+                        }
+                    } else {
+                        if (conferences.size() == 1) {
+                            openConversationsForBookmark((Bookmark) conferences.get(0));
+                            return true;
+                        } else if (conferences.isEmpty() && contacts.size() == 1) {
+                            openConversation(contacts.get(0));
+                            return true;
+                        }
+                    }
+                    SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
+                    mListPagerAdapter.requestFocus(pos);
                     return true;
                 }
-            }
-            SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
-            mListPagerAdapter.requestFocus(pos);
-            return true;
-        }
-    };
+            };
 
-    public static void populateAccountSpinner(final Context context, final List<String> accounts, final AutoCompleteTextView spinner) {
+    public static void populateAccountSpinner(
+            final Context context,
+            final List<String> accounts,
+            final AutoCompleteTextView spinner) {
         if (accounts.isEmpty()) {
-            ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
-                    R.layout.item_autocomplete,
-                    Collections.singletonList(context.getString(R.string.no_accounts)));
+            ArrayAdapter<String> adapter =
+                    new ArrayAdapter<>(
+                            context,
+                            R.layout.item_autocomplete,
+                            Collections.singletonList(context.getString(R.string.no_accounts)));
             adapter.setDropDownViewResource(R.layout.item_autocomplete);
             spinner.setAdapter(adapter);
             spinner.setEnabled(false);
         } else {
-            final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
+            final ArrayAdapter<String> adapter =
+                    new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
             adapter.setDropDownViewResource(R.layout.item_autocomplete);
             spinner.setAdapter(adapter);
             spinner.setEnabled(true);
-            spinner.setText(Iterables.getFirst(accounts,null),false);
+            spinner.setText(Iterables.getFirst(accounts, null), false);
         }
     }
 
@@ -271,7 +290,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private static boolean isViewIntent(final Intent i) {
-        return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI));
+        return i != null
+                && (Intent.ACTION_VIEW.equals(i.getAction())
+                        || Intent.ACTION_SENDTO.equals(i.getAction())
+                        || i.hasExtra(EXTRA_INVITE_URI));
     }
 
     protected void hideToast() {
@@ -301,12 +323,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
         inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
         binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
-        binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
-            @Override
-            public void onPageSelected(int position) {
-                updateSearchViewHint();
-            }
-        });
+        binding.startConversationViewPager.addOnPageChangeListener(
+                new ViewPager.SimpleOnPageChangeListener() {
+                    @Override
+                    public void onPageSelected(int position) {
+                        updateSearchViewHint();
+                    }
+                });
         mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
         binding.startConversationViewPager.setAdapter(mListPagerAdapter);
 
@@ -316,9 +339,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
         final SharedPreferences preferences = getPreferences();
 
-        this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false);
+        this.mHideOfflineContacts =
+                QuickConversationsService.isConversations()
+                        && preferences.getBoolean("hide_offline", false);
 
-        final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching));
+        final boolean startSearching =
+                preferences.getBoolean(
+                        "start_searching", getResources().getBoolean(R.bool.start_searching));
 
         final Intent intent;
         if (savedInstanceState == null) {
@@ -343,36 +370,42 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         } else if (startSearching && mInitialSearchValue.peek() == null) {
             mInitialSearchValue.push("");
         }
-        mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false));
-        mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false));
-        binding.speedDial.setOnActionSelectedListener(actionItem -> {
-            final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null;
-            final String prefilled;
-            if (isValidJid(searchString)) {
-                prefilled = Jid.ofEscaped(searchString).toEscapedString();
-            } else {
-                prefilled = null;
-            }
-            switch (actionItem.getId()) {
-                case R.id.discover_public_channels:
-                    if (QuickConversationsService.isPlayStoreFlavor()) {
-                        throw new IllegalStateException("Channel discovery is not available on Google Play flavor");
+        mRequestedContactsPermission.set(
+                savedInstanceState != null
+                        && savedInstanceState.getBoolean("requested_contacts_permission", false));
+        mOpenedFab.set(
+                savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false));
+        binding.speedDial.setOnActionSelectedListener(
+                actionItem -> {
+                    final String searchString =
+                            mSearchEditText != null ? mSearchEditText.getText().toString() : null;
+                    final String prefilled;
+                    if (isValidJid(searchString)) {
+                        prefilled = Jid.ofEscaped(searchString).toEscapedString();
                     } else {
-                        startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+                        prefilled = null;
                     }
-                    break;
-                case R.id.create_private_group_chat:
-                    showCreatePrivateGroupChatDialog();
-                    break;
-                case R.id.create_public_channel:
-                    showPublicChannelDialog();
-                    break;
-                case R.id.create_contact:
-                    showCreateContactDialog(prefilled, null);
-                    break;
-            }
-            return false;
-        });
+                    switch (actionItem.getId()) {
+                        case R.id.discover_public_channels:
+                            if (QuickConversationsService.isPlayStoreFlavor()) {
+                                throw new IllegalStateException(
+                                        "Channel discovery is not available on Google Play flavor");
+                            } else {
+                                startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+                            }
+                            break;
+                        case R.id.create_private_group_chat:
+                            showCreatePrivateGroupChatDialog();
+                            break;
+                        case R.id.create_public_channel:
+                            showPublicChannelDialog();
+                            break;
+                        case R.id.create_contact:
+                            showCreateContactDialog(prefilled, null);
+                            break;
+                    }
+                    return false;
+                });
     }
 
     private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
@@ -382,16 +415,29 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         final Menu menu = popupMenu.getMenu();
         for (int i = 0; i < menu.size(); i++) {
             final MenuItem menuItem = menu.getItem(i);
-            if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) {
+            if (QuickConversationsService.isPlayStoreFlavor()
+                    && menuItem.getItemId() == R.id.discover_public_channels) {
                 continue;
             }
-            final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
-                    .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
-                    .setFabImageTintColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorOnSurface))
-                    .setFabBackgroundColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorSurfaceContainerHighest))
-                    .create();
+            final SpeedDialActionItem actionItem =
+                    new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
+                            .setLabel(
+                                    menuItem.getTitle() != null
+                                            ? menuItem.getTitle().toString()
+                                            : null)
+                            .setFabImageTintColor(
+                                    MaterialColors.getColor(
+                                            speedDialView,
+                                            com.google.android.material.R.attr.colorOnSurface))
+                            .setFabBackgroundColor(
+                                    MaterialColors.getColor(
+                                            speedDialView,
+                                            com.google.android.material.R.attr
+                                                    .colorSurfaceContainerHighest))
+                            .create();
             speedDialView.addActionItem(actionItem);
         }
+        speedDialView.setContentDescription(getString(R.string.add_contact_or_create_or_join_group_chat));
     }
 
     public static boolean isValidJid(String input) {
@@ -406,12 +452,16 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     @Override
     public void onSaveInstanceState(Bundle savedInstanceState) {
         Intent pendingIntent = pendingViewIntent.peek();
-        savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
-        savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get());
+        savedInstanceState.putParcelable(
+                "intent", pendingIntent != null ? pendingIntent : getIntent());
+        savedInstanceState.putBoolean(
+                "requested_contacts_permission", mRequestedContactsPermission.get());
         savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
         savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
         if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
-            savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
+            savedInstanceState.putString(
+                    "search",
+                    mSearchEditText != null ? mSearchEditText.getText().toString() : null);
         }
         super.onSaveInstanceState(savedInstanceState);
     }
@@ -419,11 +469,24 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     @Override
     public void onStart() {
         super.onStart();
-        if (pendingViewIntent.peek() == null) {
-            askForContactsPermissions();
-        }
         mConferenceAdapter.refreshSettings();
         mContactsAdapter.refreshSettings();
+        if (pendingViewIntent.peek() == null) {
+            if (askForContactsPermissions()) {
+                return;
+            }
+            requestNotificationPermissionIfNeeded();
+        }
+    }
+
+    private void requestNotificationPermissionIfNeeded() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+                        != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
+                    REQUEST_POST_NOTIFICATION);
+        }
     }
 
     @Override
@@ -450,7 +513,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     protected void openConversationForContact(Contact contact) {
-        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
+        Conversation conversation =
+                xmppConnectionService.findOrCreateConversation(
+                        contact.getAccount(), contact.getJid(), false, true);
         SoftKeyboardUtils.hideSoftKeyboard(this);
         switchToConversation(conversation);
     }
@@ -475,9 +540,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + Uri.encode(address, "@/+") + "?join");
         shareIntent.setType("text/plain");
         try {
-            context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
+            context.startActivity(
+                    Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
         } catch (ActivityNotFoundException e) {
-            Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
+            Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT)
+                    .show();
         }
     }
 
@@ -487,7 +554,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
             return;
         }
-        final Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
+        final Conversation conversation =
+                xmppConnectionService.findOrCreateConversation(
+                        bookmark.getAccount(), jid, true, true, true);
         bookmark.setConversation(conversation);
         if (!bookmark.autojoin()) {
             bookmark.setAutojoin(true);
@@ -514,11 +583,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
         builder.setNegativeButton(R.string.cancel, null);
         builder.setTitle(R.string.action_delete_contact);
-        builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
-        builder.setPositiveButton(R.string.delete, (dialog, which) -> {
-            xmppConnectionService.deleteContactOnServer(contact);
-            filter(mSearchEditText.getText().toString());
-        });
+        builder.setMessage(
+                JidDialog.style(
+                        this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
+        builder.setPositiveButton(
+                R.string.delete,
+                (dialog, which) -> {
+                    xmppConnectionService.deleteContactOnServer(contact);
+                    filter(mSearchEditText.getText().toString());
+                });
         builder.create().show();
     }
 
@@ -530,21 +603,28 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         builder.setNegativeButton(R.string.cancel, null);
         builder.setTitle(R.string.delete_bookmark);
         if (hasConversation) {
-            builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString()));
+            builder.setMessage(
+                    JidDialog.style(
+                            this,
+                            R.string.remove_bookmark_and_close,
+                            bookmark.getJid().toEscapedString()));
         } else {
-            builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
-        }
-        builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> {
-            bookmark.setConversation(null);
-            final Account account = bookmark.getAccount();
-            xmppConnectionService.deleteBookmark(account, bookmark);
-            if (conversation != null) {
-                xmppConnectionService.archiveConversation(conversation);
-            }
-            filter(mSearchEditText.getText().toString());
-        });
+            builder.setMessage(
+                    JidDialog.style(
+                            this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
+        }
+        builder.setPositiveButton(
+                hasConversation ? R.string.delete_and_close : R.string.delete,
+                (dialog, which) -> {
+                    bookmark.setConversation(null);
+                    final Account account = bookmark.getAccount();
+                    xmppConnectionService.deleteBookmark(account, bookmark);
+                    if (conversation != null) {
+                        xmppConnectionService.archiveConversation(conversation);
+                    }
+                    filter(mSearchEditText.getText().toString());
+                });
         builder.create().show();
-
     }
 
     @SuppressLint("InflateParams")
@@ -641,7 +721,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             ft.remove(prev);
         }
         ft.addToBackStack(null);
-        JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts);
+        JoinConferenceDialog joinConferenceFragment =
+                JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts);
         joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
     }
 
@@ -652,7 +733,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             ft.remove(prev);
         }
         ft.addToBackStack(null);
-        CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
+        CreatePrivateGroupChatDialog createConferenceFragment =
+                CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
         createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
     }
 
@@ -663,11 +745,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             ft.remove(prev);
         }
         ft.addToBackStack(null);
-        CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts);
+        CreatePublicChannelDialog dialog =
+                CreatePublicChannelDialog.newInstance(mActivatedAccounts);
         dialog.show(ft, FRAGMENT_TAG_DIALOG);
     }
 
-    public static Account getSelectedAccount(final Context context, final AutoCompleteTextView spinner) {
+    public static Account getSelectedAccount(
+            final Context context, final AutoCompleteTextView spinner) {
         if (spinner == null || !spinner.isEnabled()) {
             return null;
         }
@@ -689,7 +773,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     protected void switchToConversation(Contact contact) {
-        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
+        Conversation conversation =
+                xmppConnectionService.findOrCreateConversation(
+                        contact.getAccount(), contact.getJid(), false, true);
         switchToConversation(conversation);
     }
 
@@ -698,7 +784,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
-        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
+        Conversation conversation =
+                xmppConnectionService.findOrCreateConversation(
+                        contact.getAccount(), contact.getJid(), false, true);
         switchToConversation(conversation, body, false, null, false, true, postInit);
     }
 
@@ -828,7 +916,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                 this.mPostponedActivityResult = null;
                 if (requestCode == REQUEST_CREATE_CONFERENCE) {
                     Account account = extractAccount(intent);
-                    final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
+                    final String name =
+                            intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
                     final List<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
                     if (account != null && jids.size() > 0) {
                         // This hardcodes cheogram.com and is in general a terrible hack
@@ -863,104 +952,109 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         super.onActivityResult(requestCode, requestCode, intent);
     }
 
-    private void askForContactsPermissions() {
-        if (QuickConversationsService.isContactListIntegration(this)) {
-            if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
-                    != PackageManager.PERMISSION_GRANTED) {
-                if (mRequestedContactsPermission.compareAndSet(false, true)) {
-                    final String consent =
-                            PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
-                                    .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
-                    final boolean requiresConsent =
-                            (QuickConversationsService.isQuicksy()
-                                            || QuickConversationsService.isPlayStoreFlavor())
-                                    && !"agreed".equals(consent);
-                    if (requiresConsent && "declined".equals(consent)) {
-                        Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
-                        return;
-                    }
-                    if (requiresConsent
-                            || shouldShowRequestPermissionRationale(
-                                    Manifest.permission.READ_CONTACTS)) {
-                        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
-                        final AtomicBoolean requestPermission = new AtomicBoolean(false);
-                        if (QuickConversationsService.isQuicksy()) {
-                            builder.setTitle(R.string.quicksy_wants_your_consent);
-                            builder.setMessage(
-                                    Html.fromHtml(
-                                            getString(R.string.sync_with_contacts_quicksy_static)));
-                        } else {
-                            builder.setTitle(R.string.sync_with_contacts);
-                            builder.setMessage(
-                                    getString(
-                                            R.string.sync_with_contacts_long,
-                                            getString(R.string.app_name)));
-                        }
-                        @StringRes int confirmButtonText;
-                        if (requiresConsent) {
-                            confirmButtonText = R.string.agree_and_continue;
-                        } else {
-                            confirmButtonText = R.string.next;
-                        }
-                        builder.setPositiveButton(
-                                confirmButtonText,
-                                (dialog, which) -> {
-                                    if (requiresConsent) {
-                                        PreferenceManager.getDefaultSharedPreferences(
-                                                        getApplicationContext())
-                                                .edit()
-                                                .putString(
-                                                        PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
-                                                .apply();
-                                    }
-                                    if (requestPermission.compareAndSet(false, true)) {
-                                        requestPermissions(
-                                                new String[] {Manifest.permission.READ_CONTACTS},
-                                                REQUEST_SYNC_CONTACTS);
-                                    }
-                                });
-                        if (requiresConsent) {
-                            builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
-                                            getApplicationContext())
-                                    .edit()
-                                    .putString(
-                                            PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
-                                    .apply());
-                        } else {
-                            builder.setOnDismissListener(
-                                    dialog -> {
-                                        if (requestPermission.compareAndSet(false, true)) {
-                                            requestPermissions(
-                                                    new String[] {
-                                                        Manifest.permission.READ_CONTACTS
-                                                    },
-                                                    REQUEST_SYNC_CONTACTS);
-                                        }
-                                    });
-                        }
-                        builder.setCancelable(requiresConsent);
-                        final AlertDialog dialog = builder.create();
-                        dialog.setCanceledOnTouchOutside(requiresConsent);
-                        dialog.setOnShowListener(
-                                dialogInterface -> {
-                                    final TextView tv = dialog.findViewById(android.R.id.message);
-                                    if (tv != null) {
-                                        tv.setMovementMethod(LinkMovementMethod.getInstance());
-                                    }
-                                });
-                        dialog.show();
-                    } else {
-                        requestPermissions(
-                                new String[] {Manifest.permission.READ_CONTACTS},
-                                REQUEST_SYNC_CONTACTS);
-                    }
+    private boolean askForContactsPermissions() {
+        if (!QuickConversationsService.isContactListIntegration(this)) {
+            return false;
+        }
+        if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                == PackageManager.PERMISSION_GRANTED) {
+            return false;
+        }
+        if (mRequestedContactsPermission.compareAndSet(false, true)) {
+            final ImmutableList.Builder<String> permissionBuilder = new ImmutableList.Builder<>();
+            permissionBuilder.add(Manifest.permission.READ_CONTACTS);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS);
+            }
+            final String[] permission = permissionBuilder.build().toArray(new String[0]);
+            final String consent =
+                    PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+                            .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
+            final boolean requiresConsent =
+                    (QuickConversationsService.isQuicksy()
+                                    || QuickConversationsService.isPlayStoreFlavor())
+                            && !"agreed".equals(consent);
+            if (requiresConsent && "declined".equals(consent)) {
+                Log.d(
+                        Config.LOGTAG,
+                        "not asking for contacts permission because consent has been declined");
+                return false;
+            }
+            if (requiresConsent
+                    || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+                final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+                final AtomicBoolean requestPermission = new AtomicBoolean(false);
+                if (QuickConversationsService.isQuicksy()) {
+                    builder.setTitle(R.string.quicksy_wants_your_consent);
+                    builder.setMessage(
+                            Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
+                } else {
+                    builder.setTitle(R.string.sync_with_contacts);
+                    builder.setMessage(
+                            getString(
+                                    R.string.sync_with_contacts_long,
+                                    getString(R.string.app_name)));
+                }
+                @StringRes int confirmButtonText;
+                if (requiresConsent) {
+                    confirmButtonText = R.string.agree_and_continue;
+                } else {
+                    confirmButtonText = R.string.next;
+                }
+                builder.setPositiveButton(
+                        confirmButtonText,
+                        (dialog, which) -> {
+                            if (requiresConsent) {
+                                PreferenceManager.getDefaultSharedPreferences(
+                                                getApplicationContext())
+                                        .edit()
+                                        .putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
+                                        .apply();
+                            }
+                            if (requestPermission.compareAndSet(false, true)) {
+                                requestPermissions(permission, REQUEST_SYNC_CONTACTS);
+                            }
+                        });
+                if (requiresConsent) {
+                    builder.setNegativeButton(
+                            R.string.decline,
+                            (dialog, which) ->
+                                    PreferenceManager.getDefaultSharedPreferences(
+                                                    getApplicationContext())
+                                            .edit()
+                                            .putString(
+                                                    PREF_KEY_CONTACT_INTEGRATION_CONSENT,
+                                                    "declined")
+                                            .apply());
+                } else {
+                    builder.setOnDismissListener(
+                            dialog -> {
+                                if (requestPermission.compareAndSet(false, true)) {
+                                    requestPermissions(permission, REQUEST_SYNC_CONTACTS);
+                                }
+                            });
                 }
+                builder.setCancelable(requiresConsent);
+                final AlertDialog dialog = builder.create();
+                dialog.setCanceledOnTouchOutside(requiresConsent);
+                dialog.setOnShowListener(
+                        dialogInterface -> {
+                            final TextView tv = dialog.findViewById(android.R.id.message);
+                            if (tv != null) {
+                                tv.setMovementMethod(LinkMovementMethod.getInstance());
+                            }
+                        });
+                dialog.show();
+            } else {
+                requestPermissions(permission, REQUEST_SYNC_CONTACTS);
             }
         }
+        return true;
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         if (grantResults.length > 0)
             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@@ -980,10 +1074,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         if (actionBar == null) {
             return;
         }
-        boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
+        boolean openConversations =
+                !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
         actionBar.setDisplayHomeAsUpEnabled(openConversations);
         actionBar.setDisplayHomeAsUpEnabled(openConversations);
-
     }
 
     @Override
@@ -995,7 +1089,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
         }
         if (mPostponedActivityResult != null) {
-            onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
+            onActivityResult(
+                    mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
             this.mPostponedActivityResult = null;
         }
         this.mActivatedAccounts.clear();
@@ -1064,7 +1159,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         if (QuickConversationsService.isQuicksy()) {
             setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
         }
-        if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) {
+        if (QuickConversationsService.isConversations()
+                && AccountUtils.hasEnabledAccounts(xmppConnectionService)
+                && this.contacts.size() == 0
+                && this.conferences.size() == 0
+                && mOpenedFab.compareAndSet(false, true)) {
             binding.speedDial.open();
         }
     }
@@ -1087,7 +1186,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             case Intent.ACTION_VIEW:
                 Uri uri = intent.getData();
                 if (uri != null) {
-                    Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
+                    Invite invite =
+                            new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
                     invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
                     invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
                     return invite.invite();
@@ -1099,7 +1199,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private boolean handleJid(Invite invite) {
-        final List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
+        List<Contact> contacts =
+                xmppConnectionService.findContacts(invite.getJid(), invite.account);
         final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account);
         if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) {
             if (muc != null && !invite.forceDialog) {
@@ -1121,8 +1222,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                 displayVerificationWarningDialog(contact, invite);
             } else {
                 if (invite.hasFingerprints()) {
-                    if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
-                        Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
+                    if (xmppConnectionService.verifyFingerprints(
+                            contact, invite.getFingerprints())) {
+                        Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT)
+                                .show();
                     }
                 }
                 if (invite.account != null) {
@@ -1150,15 +1253,23 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
         final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
         TextView warning = view.findViewById(R.id.warning);
-        warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName()));
+        warning.setText(
+                JidDialog.style(
+                        this,
+                        R.string.verifying_omemo_keys_trusted_source,
+                        contact.getJid().asBareJid().toEscapedString(),
+                        contact.getDisplayName()));
         builder.setView(view);
-        builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
-            if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
-                xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
-            }
-            switchToConversationDoNotAppend(contact, invite.getBody());
-        });
-        builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
+        builder.setPositiveButton(
+                R.string.confirm,
+                (dialog, which) -> {
+                    if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
+                        xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
+                    }
+                    switchToConversationDoNotAppend(contact, invite.getBody());
+                });
+        builder.setNegativeButton(
+                R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
         AlertDialog dialog = builder.create();
         dialog.setCanceledOnTouchOutside(false);
         dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
@@ -1181,10 +1292,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             if (account.isEnabled()) {
                 for (Contact contact : account.getRoster().getContacts()) {
                     Presence.Status s = contact.getShownStatus();
-                    if (contact.showInContactList() && contact.match(this, needle)
+                    if (contact.showInContactList()
+                            && contact.match(this, needle)
                             && (!this.mHideOfflineContacts
-                            || (needle != null && !needle.trim().isEmpty())
-                            || s.compareTo(Presence.Status.OFFLINE) < 0)) {
+                                    || (needle != null && !needle.trim().isEmpty())
+                                    || s.compareTo(Presence.Status.OFFLINE) < 0)) {
                         this.contacts.add(contact);
                         tags.addAll(contact.getTags(this));
                     }
@@ -1276,7 +1388,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void navigateBack() {
-        if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) {
+        if (!createdByViewIntent
+                && xmppConnectionService != null
+                && !xmppConnectionService.isConversationsListEmpty(null)) {
             Intent intent = new Intent(this, ConversationsActivity.class);
             intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
             startActivity(intent);

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

@@ -68,9 +68,7 @@ public class UriHandlerActivity extends BaseActivity {
     }
 
     public static void scan(final Activity activity, final boolean provisioning) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
-                || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
-                        == PackageManager.PERMISSION_GRANTED) {
+        if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
             final Intent intent = new Intent(activity, UriHandlerActivity.class);
             intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
             if (provisioning) {
@@ -114,6 +112,7 @@ public class UriHandlerActivity extends BaseActivity {
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler);
+        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
     }
 
     @Override
@@ -187,7 +186,7 @@ public class UriHandlerActivity extends BaseActivity {
                 startActivity(intent);
                 return true;
             }
-            if (accounts.size() == 0
+            if (accounts.isEmpty()
                     && xmppUri.isAction(XmppUri.ACTION_ROSTER)
                     && "y"
                             .equalsIgnoreCase(
@@ -203,7 +202,7 @@ public class UriHandlerActivity extends BaseActivity {
             return false;
         }
 
-        if (accounts.size() == 0) {
+        if (accounts.isEmpty()) {
             if (xmppUri.isValidJid()) {
                 intent = SignupUtils.getSignUpIntent(this);
                 intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
@@ -259,14 +258,14 @@ public class UriHandlerActivity extends BaseActivity {
     private void checkForLinkHeader(final HttpUrl url) {
         Log.d(Config.LOGTAG, "checking for link header on " + url);
         this.call =
-                HttpConnectionManager.OK_HTTP_CLIENT.newCall(
+                HttpConnectionManager.okHttpClient(this).newCall(
                         new Request.Builder().url(url).head().build());
         this.call.enqueue(
                 new Callback() {
                     @Override
                     public void onFailure(@NonNull Call call, @NonNull IOException e) {
                         Log.d(Config.LOGTAG, "unable to check HTTP url", e);
-                        showError(R.string.no_xmpp_adddress_found);
+                        showErrorOnUiThread(R.string.no_xmpp_adddress_found);
                     }
 
                     @Override
@@ -277,7 +276,7 @@ public class UriHandlerActivity extends BaseActivity {
                                 return;
                             }
                         }
-                        showError(R.string.no_xmpp_adddress_found);
+                        showErrorOnUiThread(R.string.no_xmpp_adddress_found);
                     }
                 });
     }
@@ -301,6 +300,10 @@ public class UriHandlerActivity extends BaseActivity {
         this.binding.error.setVisibility(View.VISIBLE);
     }
 
+    private void showErrorOnUiThread(@StringRes int error) {
+        runOnUiThread(()-> showError(error));
+    }
+
     private static Class<?> findShareViaAccountClass() {
         try {
             return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity");

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

@@ -516,14 +516,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/ui/adapter/:w 🔗

@@ -0,0 +1,1701 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.content.res.ColorStateList;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.text.Editable;
+import android.text.Spanned;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.ImageSpan;
+import android.text.style.ClickableSpan;
+import android.text.format.DateUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
+import android.text.style.URLSpan;
+import android.util.DisplayMetrics;
+import android.util.LruCache;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.core.widget.ImageViewCompat;
+
+import com.google.android.material.imageview.ShapeableImageView;
+import com.google.android.material.shape.CornerFamily;
+import com.google.android.material.shape.ShapeAppearanceModel;
+
+import com.cheogram.android.BobTransfer;
+import com.cheogram.android.MessageTextActionModeCallback;
+import com.cheogram.android.SwipeDetector;
+import com.cheogram.android.WebxdcPage;
+import com.cheogram.android.WebxdcUpdate;
+
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.color.MaterialColors;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import com.lelloman.identicon.view.GithubIdenticonView;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.ipfs.cid.Cid;
+
+import me.saket.bettermovementmethod.BetterLinkMovementMethod;
+
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message.FileParams;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.entities.RtpSessionStatus;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.services.NotificationService;
+import eu.siacs.conversations.ui.Activities;
+import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.ui.ConversationsActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.service.AudioPlayer;
+import eu.siacs.conversations.ui.text.DividerSpan;
+import eu.siacs.conversations.ui.text.QuoteSpan;
+import eu.siacs.conversations.ui.util.Attachment;
+import eu.siacs.conversations.ui.util.AvatarWorkerTask;
+import eu.siacs.conversations.ui.util.MyLinkify;
+import eu.siacs.conversations.ui.util.QuoteHelper;
+import eu.siacs.conversations.ui.util.ShareUtil;
+import eu.siacs.conversations.ui.util.ViewUtil;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.Emoticons;
+import eu.siacs.conversations.utils.GeoHelper;
+import eu.siacs.conversations.utils.MessageUtils;
+import eu.siacs.conversations.utils.StylingHelper;
+import eu.siacs.conversations.utils.TimeFrameUtils;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.mam.MamReference;
+import eu.siacs.conversations.xml.Element;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MessageAdapter extends ArrayAdapter<Message> {
+
+    public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
+    private static final int SENT = 0;
+    private static final int RECEIVED = 1;
+    private static final int STATUS = 2;
+    private static final int DATE_SEPARATOR = 3;
+    private static final int RTP_SESSION = 4;
+    private final XmppActivity activity;
+    private final AudioPlayer audioPlayer;
+    private List<String> highlightedTerm = null;
+    private final DisplayMetrics metrics;
+    private ConversationFragment mConversationFragment = null;
+    private OnContactPictureClicked mOnContactPictureClickedListener;
+    private OnContactPictureClicked mOnMessageBoxClickedListener;
+    private OnContactPictureClicked mOnMessageBoxSwipedListener;
+    private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
+    private OnInlineImageLongClicked mOnInlineImageLongClickedListener;
+    private boolean mUseGreenBackground = false;
+    private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
+    private final boolean mForceNames;
+    private final Map<String, WebxdcUpdate> lastWebxdcUpdate = new HashMap<>();
+    private String selectionUuid = null;
+
+    public MessageAdapter(
+            final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
+        super(activity, 0, messages);
+        this.audioPlayer = new AudioPlayer(this);
+        this.activity = activity;
+        metrics = getContext().getResources().getDisplayMetrics();
+        updatePreferences();
+        this.mForceNames = forceNames;
+    }
+
+    public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
+        this(activity, messages, false);
+    }
+
+    private static void resetClickListener(View... views) {
+        for (View view : views) {
+            if (view != null) view.setOnClickListener(null);
+        }
+    }
+
+    public void flagScreenOn() {
+        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    public void flagScreenOff() {
+        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+    }
+
+    public void setVolumeControl(final int stream) {
+        activity.setVolumeControlStream(stream);
+    }
+
+    public void setOnContactPictureClicked(OnContactPictureClicked listener) {
+        this.mOnContactPictureClickedListener = listener;
+    }
+
+    public void setOnMessageBoxClicked(OnContactPictureClicked listener) {
+        this.mOnMessageBoxClickedListener = listener;
+    }
+
+    public void setOnMessageBoxSwiped(OnContactPictureClicked listener) {
+        this.mOnMessageBoxSwipedListener = listener;
+    }
+
+    public void setConversationFragment(ConversationFragment frag) {
+        mConversationFragment = frag;
+    }
+
+    public void quoteText(String text) {
+        if (mConversationFragment != null) mConversationFragment.quoteText(text);
+    }
+
+    public boolean hasSelection() {
+        return selectionUuid != null;
+    }
+
+    public Activity getActivity() {
+        return activity;
+    }
+
+    public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
+        this.mOnContactPictureLongClickedListener = listener;
+    }
+
+    public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) {
+        this.mOnInlineImageLongClickedListener = listener;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 5;
+    }
+
+    private int getItemViewType(Message message) {
+        if (message.getType() == Message.TYPE_STATUS) {
+            if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
+                return DATE_SEPARATOR;
+            } else {
+                return STATUS;
+            }
+        } else if (message.getType() == Message.TYPE_RTP_SESSION) {
+            return RTP_SESSION;
+        } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
+            return RECEIVED;
+        } else {
+            return SENT;
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return this.getItemViewType(getItem(position));
+    }
+
+    private void displayStatus(
+            final ViewHolder viewHolder,
+            final Message message,
+            final int type,
+            final BubbleColor bubbleColor) {
+        final int mergedStatus = message.getMergedStatus();
+        final boolean error;
+        if (viewHolder.indicatorReceived != null) {
+            viewHolder.indicatorReceived.setVisibility(View.GONE);
+        }
+        final Transferable transferable = message.getTransferable();
+        final boolean multiReceived =
+                message.getConversation().getMode() == Conversation.MODE_MULTI
+                        && mergedStatus <= Message.STATUS_RECEIVED;
+        final String fileSize;
+        if (message.isFileOrImage()
+                || transferable != null
+                || MessageUtils.unInitiatedButKnownSize(message)) {
+            final FileParams params = message.getFileParams();
+            fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
+            if (message.getStatus() == Message.STATUS_SEND_FAILED
+                    || (transferable != null
+                            && (transferable.getStatus() == Transferable.STATUS_FAILED
+                                    || transferable.getStatus()
+                                            == Transferable.STATUS_CANCELLED))) {
+                error = true;
+            } else {
+                error = message.getStatus() == Message.STATUS_SEND_FAILED;
+            }
+        } else {
+            fileSize = null;
+            error = message.getStatus() == Message.STATUS_SEND_FAILED;
+        }
+        if (type == SENT) {
+            final @DrawableRes Integer receivedIndicator =
+                    getMessageStatusAsDrawable(message, mergedStatus);
+            if (receivedIndicator == null) {
+                viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
+            } else {
+                viewHolder.indicatorReceived.setImageResource(receivedIndicator);
+                if (mergedStatus == Message.STATUS_SEND_FAILED) {
+                    setImageTintError(viewHolder.indicatorReceived);
+                } else {
+                    setImageTint(viewHolder.indicatorReceived, bubbleColor);
+                }
+                viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+            }
+        }
+        final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
+
+        if (error && type == SENT) {
+            viewHolder.time.setTextColor(
+                    MaterialColors.getColor(
+                            viewHolder.time, com.google.android.material.R.attr.colorError));
+        } else {
+            setTextColor(viewHolder.time, bubbleColor);
+        }
+        setTextColor(viewHolder.subject, bubbleColor);
+        if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+            viewHolder.indicator.setVisibility(View.GONE);
+        } else {
+            boolean verified = false;
+            if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+                final FingerprintStatus status =
+                        message.getConversation()
+                                .getAccount()
+                                .getAxolotlService()
+                                .getFingerprintTrust(message.getFingerprint());
+                if (status != null && status.isVerified()) {
+                    verified = true;
+                }
+            }
+            if (verified) {
+                viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
+            } else {
+                viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
+            }
+            if (error && type == SENT) {
+                setImageTintError(viewHolder.indicator);
+            } else {
+                setImageTint(viewHolder.indicator, bubbleColor);
+            }
+            viewHolder.indicator.setVisibility(View.VISIBLE);
+        }
+
+        if (viewHolder.edit_indicator != null) {
+            if (message.edited()) {
+                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
+                if (error && type == SENT) {
+                    setImageTintError(viewHolder.edit_indicator);
+                } else {
+                    setImageTint(viewHolder.edit_indicator, bubbleColor);
+                }
+            } else {
+                viewHolder.edit_indicator.setVisibility(View.GONE);
+            }
+        }
+
+        final String formattedTime =
+                UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
+        final String bodyLanguage = message.getBodyLanguage();
+        final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
+        if (message.getStatus() <= Message.STATUS_RECEIVED) {
+            timeInfoBuilder.add(formattedTime);
+            if (fileSize != null) {
+                timeInfoBuilder.add(fileSize);
+            }
+            if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) {
+                final String displayName = UIHelper.getMessageDisplayName(message);
+                if (displayName != null) {
+                    timeInfoBuilder.add(displayName);
+                }
+            }
+            if (bodyLanguage != null) {
+                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
+            }
+        } else {
+            if (bodyLanguage != null) {
+                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
+            }
+            if (fileSize != null) {
+                timeInfoBuilder.add(fileSize);
+            }
+            // for space reasons we display only 'additional status info' (send progress or concrete
+            // failure reason) or the time
+            if (additionalStatusInfo != null) {
+                timeInfoBuilder.add(additionalStatusInfo);
+            } else {
+                timeInfoBuilder.add(formattedTime);
+            }
+        }
+        final var timeInfo = timeInfoBuilder.build();
+        viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
+    }
+
+    public static @DrawableRes Integer getMessageStatusAsDrawable(
+            final Message message, final int status) {
+        final var transferable = message.getTransferable();
+        return switch (status) {
+            case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
+            case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
+            case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
+            case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
+                    .ic_done_all_24dp;
+            case Message.STATUS_SEND_FAILED -> {
+                final String errorMessage = message.getErrorMessage();
+                if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
+                    yield R.drawable.ic_cancel_24dp;
+                } else {
+                    yield R.drawable.ic_error_24dp;
+                }
+            }
+            case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
+            default -> null;
+        };
+    }
+
+    @Nullable
+    private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
+        final String additionalStatusInfo;
+        if (mergedStatus == Message.STATUS_SEND_FAILED) {
+            final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
+            final String[] errorParts = errorMessage.split("\\u001f", 2);
+            if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
+                additionalStatusInfo = getContext().getString(R.string.file_too_large);
+            } else {
+                additionalStatusInfo = null;
+            }
+        } else if (mergedStatus == Message.STATUS_UNSEND) {
+            final var transferable = message.getTransferable();
+            if (transferable == null) {
+                return null;
+            }
+            return getContext().getString(R.string.sending_file, transferable.getProgress());
+        } else {
+            additionalStatusInfo = null;
+        }
+        return additionalStatusInfo;
+    }
+
+    private void displayInfoMessage(
+            ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
+        viewHolder.download_button.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.messageBody.setVisibility(View.VISIBLE);
+        viewHolder.messageBody.setText(text);
+        viewHolder.messageBody.setTextColor(
+                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
+        viewHolder.messageBody.setTextIsSelectable(false);
+    }
+
+    private void displayEmojiMessage(
+            final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) {
+        viewHolder.download_button.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.messageBody.setVisibility(View.VISIBLE);
+        setTextColor(viewHolder.messageBody, bubbleColor);
+        ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
+        float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
+        body.setSpan(
+                new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        viewHolder.messageBody.setText(body);
+    }
+
+    private void applyQuoteSpan(
+            final TextView textView,
+            Editable body,
+            int start,
+            int end,
+            final BubbleColor bubbleColor,
+            final boolean makeEdits) {
+        if (makeEdits && start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
+            body.insert(start++, "\n");
+            body.setSpan(
+                    new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            end++;
+        }
+        if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
+            body.insert(end, "\n");
+            body.setSpan(
+                new DividerSpan(false),
+                end,
+                end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+            );
+        }
+        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
+        body.setSpan(
+                new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
+                start,
+                end,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    public boolean handleTextQuotes(final TextView textView, final Editable body) {
+        return handleTextQuotes(textView, body, true);
+    }
+
+    public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) {
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
+        return handleTextQuotes(textView, body, bubbleColor, deleteMarkers);
+    }
+
+    /**
+     * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
+     * and applies DividerSpan to them to show a padding between quote and text.
+     */
+    public boolean handleTextQuotes(
+            final TextView textView,
+            final Editable body,
+            final BubbleColor bubbleColor,
+            final boolean deleteMarkers) {
+        boolean startsWithQuote = false;
+        int quoteDepth = 1;
+        while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
+            char previous = '\n';
+            int lineStart = -1;
+            int lineTextStart = -1;
+            int quoteStart = -1;
+            int skipped = 0;
+            for (int i = 0; i <= body.length(); i++) {
+                if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) {
+                    skipped++;
+                    continue;
+                }
+                char current = body.length() > i ? body.charAt(i) : '\n';
+                if (lineStart == -1) {
+                    if (previous == '\n') {
+                        if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
+                            // Line start with quote
+                            lineStart = i;
+                            if (quoteStart == -1) quoteStart = i - skipped;
+                            if (i == 0) startsWithQuote = true;
+                        } else if (quoteStart >= 0) {
+                            // Line start without quote, apply spans there
+                            applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers);
+                            quoteStart = -1;
+                        }
+                    }
+                } else {
+                    // Remove extra spaces between > and first character in the line
+                    // > character will be removed too
+                    if (current != ' ' && lineTextStart == -1) {
+                        lineTextStart = i;
+                    }
+                    if (current == '\n') {
+                        if (deleteMarkers) {
+                            i -= lineTextStart - lineStart;
+                            body.delete(lineStart, lineTextStart);
+                            if (i == lineStart) {
+                                // Avoid empty lines because span over empty line can be hidden
+                                body.insert(i++, " ");
+                            }
+                        } else {
+                            body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
+                        }
+                        lineStart = -1;
+                        lineTextStart = -1;
+                    }
+                }
+                previous = current;
+                skipped = 0;
+            }
+            if (quoteStart >= 0) {
+                // Apply spans to finishing open quote
+                applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers);
+            }
+            quoteDepth++;
+        }
+        return startsWithQuote;
+    }
+
+    private SpannableStringBuilder getSpannableBody(final Message message) {
+        Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null);
+        return message.getMergedBody((cid) -> {
+            try {
+                DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
+                if (f == null || !f.canRead()) {
+                    if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
+
+                    try {
+                        new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
+                    } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
+                    return null;
+                }
+
+                Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
+                if (d == null) {
+                    new ThumbnailTask().execute(f);
+                }
+                return d;
+            } catch (final IOException e) {
+                return null;
+            }
+        }, fallbackImg);
+    }
+
+    private void displayTextMessage(
+            final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        viewHolder.download_button.setVisibility(View.GONE);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.messageBody.setVisibility(View.VISIBLE);
+        setTextColor(viewHolder.messageBody, bubbleColor);
+        setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
+
+        final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody.getLayoutParams();
+        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+        viewHolder.messageBody.setLayoutParams(layoutParams);
+
+        viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+
+        if (message.getBody() != null && !message.getBody().equals("")) {
+            viewHolder.messageBody.setTextIsSelectable(true);
+            viewHolder.messageBody.setVisibility(View.VISIBLE);
+            final String nick = UIHelper.getMessageDisplayName(message);
+            SpannableStringBuilder body = getSpannableBody(message);
+            final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0;
+            boolean hasMeCommand = message.hasMeCommand();
+            if (hasMeCommand) {
+                body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
+            }
+            if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
+                body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
+                body.append("\u2026");
+            }
+            Message.MergeSeparator[] mergeSeparators =
+                    body.getSpans(0, body.length(), Message.MergeSeparator.class);
+            for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
+                int start = body.getSpanStart(mergeSeparator);
+                int end = body.getSpanEnd(mergeSeparator);
+                body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+            for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
+                int start = body.getSpanStart(quote);
+                int end = body.getSpanEnd(quote);
+                body.removeSpan(quote);
+                applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true);
+            }
+            boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false;
+            if (!message.isPrivateMessage()) {
+                if (hasMeCommand) {
+                    body.setSpan(
+                            new StyleSpan(Typeface.BOLD_ITALIC),
+                            0,
+                            nick.length(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+            } else {
+                String privateMarker;
+                if (message.getStatus() <= Message.STATUS_RECEIVED) {
+                    privateMarker = activity.getString(R.string.private_message);
+                } else {
+                    Jid cp = message.getCounterpart();
+                    privateMarker =
+                            activity.getString(
+                                    R.string.private_message_to,
+                                    Strings.nullToEmpty(cp == null ? null : cp.getResource()));
+                }
+                body.insert(0, privateMarker);
+                int privateMarkerIndex = privateMarker.length();
+                if (startsWithQuote) {
+                    body.insert(privateMarkerIndex, "\n\n");
+                    body.setSpan(
+                            new DividerSpan(false),
+                            privateMarkerIndex,
+                            privateMarkerIndex + 2,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                } else {
+                    body.insert(privateMarkerIndex, " ");
+                }
+                body.setSpan(
+                        new ForegroundColorSpan(
+                                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                        0,
+                        privateMarkerIndex,
+                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                body.setSpan(
+                        new StyleSpan(Typeface.BOLD),
+                        0,
+                        privateMarkerIndex,
+                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                if (hasMeCommand) {
+                    body.setSpan(
+                            new StyleSpan(Typeface.BOLD_ITALIC),
+                            privateMarkerIndex + 1,
+                            privateMarkerIndex + 1 + nick.length(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+            }
+            if (message.getConversation().getMode() == Conversation.MODE_MULTI
+                    && message.getStatus() == Message.STATUS_RECEIVED) {
+                if (message.getConversation() instanceof Conversation conversation) {
+                    Pattern pattern =
+                            NotificationService.generateNickHighlightPattern(
+                                    conversation.getMucOptions().getActualNick());
+                    Matcher matcher = pattern.matcher(body);
+                    while (matcher.find()) {
+                        body.setSpan(
+                                new StyleSpan(Typeface.BOLD),
+                                matcher.start(),
+                                matcher.end(),
+                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    }
+
+                    pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
+                    matcher = pattern.matcher(body);
+                    while (matcher.find()) {
+                        body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    }
+                }
+            }
+            Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
+            while (matcher.find()) {
+                if (matcher.start() < matcher.end()) {
+                    body.setSpan(
+                            new RelativeSizeSpan(1.2f),
+                            matcher.start(),
+                            matcher.end(),
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+            }
+
+            if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
+            MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
+            if (highlightedTerm != null) {
+                StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
+            }
+
+            viewHolder.messageBody.setAutoLinkMask(0);
+            viewHolder.messageBody.setText(body);
+            BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
+                @Override
+                protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) {
+                    if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) {
+                        tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
+                        super.dispatchUrlLongClick(tv, span);
+                        return;
+                    }
+
+                    Spannable body = (Spannable) tv.getText();
+                    ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class);
+                    if (imageSpans.length > 0) {
+                        Uri uri = Uri.parse(imageSpans[0].getSource());
+                        Cid cid = BobTransfer.cid(uri);
+                        if (cid == null) return;
+                        if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) {
+                            tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
+                        }
+                    }
+                }
+            };
+            method.setOnLinkLongClickListener((tv, url) -> {
+                tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
+                ShareUtil.copyLinkToClipboard(activity, url);
+                return true;
+            });
+            viewHolder.messageBody.setMovementMethod(method);
+        } else {
+            viewHolder.messageBody.setText("");
+            viewHolder.messageBody.setTextIsSelectable(false);
+            toggleWhisperInfo(viewHolder, message, bubbleColor);
+        }
+    }
+
+    private void displayDownloadableMessage(
+            ViewHolder viewHolder,
+            final Message message,
+            String text,
+            final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.image.setVisibility(View.GONE);
+        List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
+        if (thumbs != null && !thumbs.isEmpty()) {
+            for (Element thumb : thumbs) {
+                Uri uri = Uri.parse(thumb.getAttribute("uri"));
+                if (uri.getScheme().equals("data")) {
+                    String[] parts = uri.getSchemeSpecificPart().split(",", 2);
+                    parts = parts[0].split(";");
+                    if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue;
+                } else if (uri.getScheme().equals("cid")) {
+                    Cid cid = BobTransfer.cid(uri);
+                    if (cid == null) continue;
+                    DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
+                    if (f == null || !f.canRead()) {
+                        if (!message.trusted() && !message.getConversation().canInferPresence()) continue;
+
+                        try {
+                            new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
+                        } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
+                        continue;
+                    }
+                } else {
+                    continue;
+                }
+
+                int width = message.getFileParams().width;
+                if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
+                if (width < 1) width = 1920;
+
+                int height = message.getFileParams().height;
+                if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
+                if (height < 1) height = 1080;
+
+                viewHolder.image.setVisibility(View.VISIBLE);
+                imagePreviewLayout(width, height, viewHolder.image, true, type, viewHolder);
+                activity.loadBitmap(message, viewHolder.image);
+                viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
+
+                break;
+            }
+        }
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.download_button.setVisibility(View.VISIBLE);
+        viewHolder.download_button.setText(text);
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
+        viewHolder.download_button.setOnClickListener(
+                v -> ConversationFragment.downloadFile(activity, message));
+    }
+
+    private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        Cid webxdcCid = message.getFileParams().getCids().get(0);
+        WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.download_button.setVisibility(View.VISIBLE);
+        viewHolder.download_button.setText("Open " + webxdc.getName());
+        viewHolder.download_button.setOnClickListener(v -> {
+            Conversation conversation = (Conversation) message.getConversation();
+            if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
+                conversation.startWebxdc(webxdc);
+            }
+        });
+
+        final WebxdcUpdate lastUpdate;
+        synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); }
+        if (lastUpdate == null) {
+            new Thread(() -> {
+                final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message);
+                if (update != null) {
+                    synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); }
+                    activity.xmppConnectionService.updateConversationUi();
+                }
+            }).start();
+        } else {
+            if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
+                viewHolder.messageBody.setVisibility(View.VISIBLE);
+                viewHolder.messageBody.setText(
+                    (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
+                    (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
+                );
+            }
+        }
+
+        final LruCache<String, Drawable> cache = activity.xmppConnectionService.getDrawableCache();
+        final Drawable d = cache.get("webxdc:icon:" + webxdcCid);
+        if (d == null) {
+            new Thread(() -> {
+                Drawable icon = webxdc.getIcon();
+                if (icon != null) {
+                    cache.put("webxdc:icon:" + webxdcCid, icon);
+                    activity.xmppConnectionService.updateConversationUi();
+                }
+            }).start();
+        } else {
+            viewHolder.image.setVisibility(View.VISIBLE);
+            viewHolder.image.setImageDrawable(d);
+        }
+    }
+
+    private void displayOpenableMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.download_button.setVisibility(View.VISIBLE);
+        viewHolder.download_button.setText(
+                activity.getString(
+                        R.string.open_x_file,
+                        UIHelper.getFileDescriptionString(activity, message)));
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
+        viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
+    }
+
+    private void displayLocationMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.download_button.setVisibility(View.VISIBLE);
+        viewHolder.download_button.setText(R.string.show_location);
+        final var attachment = Attachment.of(message);
+        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
+        viewHolder.download_button.setIconResource(imageResource);
+        viewHolder.download_button.setOnClickListener(v -> showLocation(message));
+    }
+
+    private void displayAudioMessage(
+            ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.image.setVisibility(View.GONE);
+        viewHolder.download_button.setVisibility(View.GONE);
+        final RelativeLayout audioPlayer = viewHolder.audioPlayer;
+        audioPlayer.setVisibility(View.VISIBLE);
+        AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
+        this.audioPlayer.init(audioPlayer, message);
+    }
+
+    private void displayMediaPreviewMessage(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
+        displayTextMessage(viewHolder, message, bubbleColor, type);
+        viewHolder.download_button.setVisibility(View.GONE);
+        viewHolder.audioPlayer.setVisibility(View.GONE);
+        viewHolder.image.setVisibility(View.VISIBLE);
+        final FileParams params = message.getFileParams();
+        imagePreviewLayout(params.width, params.height, viewHolder.image, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder);
+        activity.loadBitmap(message, viewHolder.image);
+        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
+    }
+
+    private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean withOther, int type, ViewHolder viewHolder) {
+        final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
+        final int scaledW;
+        final int scaledH;
+        if (Math.max(h, w) * metrics.density <= target) {
+            scaledW = (int) (w * metrics.density);
+            scaledH = (int) (h * metrics.density);
+        } else if (Math.max(h, w) <= target) {
+            scaledW = w;
+            scaledH = h;
+        } else if (w <= h) {
+            scaledW = (int) (w / ((double) h / target));
+            scaledH = (int) target;
+        } else {
+            scaledW = (int) target;
+            scaledH = (int) (h / ((double) w / target));
+        }
+        final var small = withOther ? scaledW < target : scaledW < 110 * metrics.density;
+        final LinearLayout.LayoutParams layoutParams =
+                new LinearLayout.LayoutParams(scaledW, scaledH);
+        image.setLayoutParams(layoutParams);
+
+        final var bubbleRadius = activity.getResources().getDimension(R.dimen.bubble_radius);
+        var shape = new ShapeAppearanceModel.Builder().setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius);
+        if (type == SENT) {
+            shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius);
+        }
+        if (small) {
+            final var imageRadius = activity.getResources().getDimension(R.dimen.image_radius);
+            shape = shape.setAllCorners(CornerFamily.ROUNDED, imageRadius);
+            image.setPadding(0, (int)(8 * metrics.density), 0, 0);
+        } else {
+            image.setPadding(0, 0, 0, 0);
+        }
+        image.setShapeAppearanceModel(shape.build());
+
+        if (!small) {
+            final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams();
+            blayoutParams.width = (int) (target - (22 * metrics.density));
+            viewHolder.messageBody.setLayoutParams(blayoutParams);
+        }
+    }
+
+    private void toggleWhisperInfo(
+            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
+        if (message.isPrivateMessage()) {
+            final String privateMarker;
+            if (message.getStatus() <= Message.STATUS_RECEIVED) {
+                privateMarker = activity.getString(R.string.private_message);
+            } else {
+                Jid cp = message.getCounterpart();
+                privateMarker =
+                        activity.getString(
+                                R.string.private_message_to,
+                                Strings.nullToEmpty(cp == null ? null : cp.getResource()));
+            }
+            final SpannableString body = new SpannableString(privateMarker);
+            body.setSpan(
+                    new ForegroundColorSpan(
+                            bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                    0,
+                    privateMarker.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            body.setSpan(
+                    new StyleSpan(Typeface.BOLD),
+                    0,
+                    privateMarker.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            viewHolder.messageBody.setText(body);
+            viewHolder.messageBody.setVisibility(View.VISIBLE);
+        } else {
+            viewHolder.messageBody.setVisibility(View.GONE);
+        }
+    }
+
+    private void loadMoreMessages(Conversation conversation) {
+        conversation.setLastClearHistory(0, null);
+        activity.xmppConnectionService.updateConversation(conversation);
+        conversation.setHasMessagesLeftOnServer(true);
+        conversation.setFirstMamReference(null);
+        long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
+        if (timestamp == 0) {
+            timestamp = System.currentTimeMillis();
+        }
+        conversation.messagesLoaded.set(true);
+        MessageArchiveService.Query query =
+                activity.xmppConnectionService
+                        .getMessageArchiveService()
+                        .query(conversation, new MamReference(0), timestamp, false);
+        if (query != null) {
+            Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
+                    .show();
+        } else {
+            Toast.makeText(
+                            activity,
+                            R.string.not_fetching_history_retention_period,
+                            Toast.LENGTH_SHORT)
+                    .show();
+        }
+    }
+
+    @Override
+    public View getView(final int position, View view, final @NonNull ViewGroup parent) {
+        final Message message = getItem(position);
+        final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
+        final boolean isInValidSession =
+                message.isValidInSession() && (!omemoEncryption || message.isTrusted());
+        final Conversational conversation = message.getConversation();
+        final Account account = conversation.getAccount();
+        final List<Element> commands = message.getCommands();
+        final int type = getItemViewType(position);
+        ViewHolder viewHolder;
+        if (view == null) {
+            viewHolder = new ViewHolder();
+            switch (type) {
+                case DATE_SEPARATOR:
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_date_bubble, parent, false);
+                    viewHolder.status_message = view.findViewById(R.id.message_body);
+                    viewHolder.message_box = view.findViewById(R.id.message_box);
+                    break;
+                case RTP_SESSION:
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_rtp_session, parent, false);
+                    viewHolder.status_message = view.findViewById(R.id.message_body);
+                    viewHolder.message_box = view.findViewById(R.id.message_box);
+                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
+                    break;
+                case SENT:
+                    view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false);
+                    viewHolder.status_line = view.findViewById(R.id.status_line);
+                    viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
+                    viewHolder.message_box = view.findViewById(R.id.message_box);
+                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
+                    viewHolder.download_button = view.findViewById(R.id.download_button);
+                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
+                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
+                    viewHolder.image = view.findViewById(R.id.message_image);
+                    viewHolder.messageBody = view.findViewById(R.id.message_body);
+                    viewHolder.time = view.findViewById(R.id.message_time);
+                    viewHolder.subject = view.findViewById(R.id.message_subject);
+                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
+                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+                    viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
+                    break;
+                case RECEIVED:
+                    view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false);
+                    viewHolder.status_line = view.findViewById(R.id.status_line);
+                    viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
+                    viewHolder.message_box = view.findViewById(R.id.message_box);
+                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
+                    viewHolder.download_button = view.findViewById(R.id.download_button);
+                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
+                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
+                    viewHolder.image = view.findViewById(R.id.message_image);
+                    viewHolder.messageBody = view.findViewById(R.id.message_body);
+                    viewHolder.time = view.findViewById(R.id.message_time);
+                    viewHolder.subject = view.findViewById(R.id.message_subject);
+                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
+                    viewHolder.encryption = view.findViewById(R.id.message_encryption);
+                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
+                    viewHolder.commands_list = view.findViewById(R.id.commands_list);
+                    viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
+                    break;
+                case STATUS:
+                    view =
+                            activity.getLayoutInflater()
+                                    .inflate(R.layout.item_message_status, parent, false);
+                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
+                    viewHolder.status_message = view.findViewById(R.id.status_message);
+                    viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
+                    break;
+                default:
+                    throw new AssertionError("Unknown view type");
+            }
+            view.setTag(viewHolder);
+        } else {
+            viewHolder = (ViewHolder) view.getTag();
+            if (viewHolder == null) {
+                return view;
+            }
+        }
+
+        if (viewHolder.messageBody != null) {
+            viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody));
+        }
+
+        if (viewHolder.thread_identicon != null) {
+            viewHolder.thread_identicon.setVisibility(View.GONE);
+            final Element thread = message.getThread();
+            if (thread != null) {
+                final String threadId = thread.getContent();
+                if (threadId != null) {
+                    viewHolder.thread_identicon.setVisibility(View.VISIBLE);
+                    viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
+                    viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
+                }
+            }
+        }
+
+        final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black);
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        final BubbleColor bubbleColor;
+        if (type == RECEIVED) {
+            if (isInValidSession) {
+                bubbleColor = colorfulBackground  || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
+            } else {
+                bubbleColor = BubbleColor.WARNING;
+            }
+        } else {
+            if (!colorfulBackground && black) {
+                bubbleColor = BubbleColor.SECONDARY;
+            } else {
+                bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
+            }
+        }
+
+        if (type == DATE_SEPARATOR) {
+            if (UIHelper.today(message.getTimeSent())) {
+                viewHolder.status_message.setText(R.string.today);
+            } else if (UIHelper.yesterday(message.getTimeSent())) {
+                viewHolder.status_message.setText(R.string.yesterday);
+            } else {
+                viewHolder.status_message.setText(
+                        DateUtils.formatDateTime(
+                                activity,
+                                message.getTimeSent(),
+                                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
+            }
+            if (colorfulBackground) {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
+                setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
+            } else {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
+                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
+            }
+            return view;
+        } else if (type == RTP_SESSION) {
+            final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
+            final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
+            final long duration = rtpSessionStatus.duration;
+            final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
+            if (received) {
+                if (duration > 0) {
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.incoming_call_duration_timestamp,
+                                    TimeFrameUtils.resolve(activity, duration),
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
+                } else if (rtpSessionStatus.successful) {
+                    viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
+                } else {
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.missed_call_timestamp,
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
+                }
+            } else {
+                if (duration > 0) {
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.outgoing_call_duration_timestamp,
+                                    TimeFrameUtils.resolve(activity, duration),
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
+                } else {
+                    viewHolder.status_message.setText(
+                            activity.getString(
+                                    R.string.outgoing_call_timestamp,
+                                    UIHelper.readableTimeDifferenceFull(
+                                            activity, message.getTimeSent())));
+                }
+            }
+            if (colorfulBackground) {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
+                setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
+                setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
+            } else {
+                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
+                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
+                setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
+            }
+            viewHolder.indicatorReceived.setImageResource(
+                    RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
+            return view;
+        } else if (type == STATUS) {
+            if ("LOAD_MORE".equals(message.getBody())) {
+                viewHolder.status_message.setVisibility(View.GONE);
+                viewHolder.contact_picture.setVisibility(View.GONE);
+                viewHolder.load_more_messages.setVisibility(View.VISIBLE);
+                viewHolder.load_more_messages.setOnClickListener(
+                        v -> loadMoreMessages((Conversation) message.getConversation()));
+            } else {
+                viewHolder.status_message.setVisibility(View.VISIBLE);
+                viewHolder.load_more_messages.setVisibility(View.GONE);
+                viewHolder.status_message.setText(message.getBody());
+                boolean showAvatar;
+                if (conversation.getMode() == Conversation.MODE_SINGLE) {
+                    showAvatar = true;
+                    AvatarWorkerTask.loadAvatar(
+                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
+                } else if (message.getCounterpart() != null
+                        || message.getTrueCounterpart() != null
+                        || (message.getCounterparts() != null
+                                && message.getCounterparts().size() > 0)) {
+                    showAvatar = true;
+                    AvatarWorkerTask.loadAvatar(
+                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
+                } else {
+                    showAvatar = false;
+                }
+                if (showAvatar) {
+                    viewHolder.contact_picture.setAlpha(0.5f);
+                    viewHolder.contact_picture.setVisibility(View.VISIBLE);
+                } else {
+                    viewHolder.contact_picture.setVisibility(View.GONE);
+                }
+            }
+            return view;
+        } else {
+            // viewHolder.message_box.setClipToOutline(true); This eats the bubble tails on A14 for some reason
+            AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
+        }
+
+        resetClickListener(viewHolder.message_box, viewHolder.messageBody);
+
+        viewHolder.message_box.setOnClickListener(v -> {
+            if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
+                MessageAdapter.this.mOnMessageBoxClickedListener
+                        .onContactPictureClicked(message);
+            }
+        });
+        SwipeDetector swipeDetector = new SwipeDetector((action) -> {
+            if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) {
+                MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message);
+            }
+        });
+        viewHolder.message_box.setOnTouchListener(swipeDetector);
+        viewHolder.image.setOnTouchListener(swipeDetector);
+        viewHolder.time.setOnTouchListener(swipeDetector);
+
+        // Treat touch-up as click so we don't have to touch twice
+        // (touch twice is because it's waiting to see if you double-touch for text selection)
+        viewHolder.messageBody.setOnTouchListener((v, event) -> {
+            if (event.getAction() == MotionEvent.ACTION_UP) {
+                if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
+                    MessageAdapter.this.mOnMessageBoxClickedListener
+                        .onContactPictureClicked(message);
+                }
+            }
+
+            swipeDetector.onTouch(v, event);
+
+            return false;
+        });
+        viewHolder.messageBody.setOnClickListener(v -> {
+            if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
+                MessageAdapter.this.mOnMessageBoxClickedListener
+                        .onContactPictureClicked(message);
+            }
+        });
+        viewHolder.contact_picture.setOnClickListener(v -> {
+            if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+                MessageAdapter.this.mOnContactPictureClickedListener
+                        .onContactPictureClicked(message);
+            }
+
+        });
+        viewHolder.contact_picture.setOnLongClickListener(v -> {
+            if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+                MessageAdapter.this.mOnContactPictureLongClickedListener
+                        .onContactPictureLongClicked(v, message);
+                return true;
+            } else {
+                return false;
+            }
+        });
+        viewHolder.messageBody.setAccessibilityDelegate(null);
+
+        boolean footerWrap = false;
+
+        final Transferable transferable = message.getTransferable();
+        final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
+
+        final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
+        if (muted) {
+            // Muted MUC participant
+            displayInfoMessage(viewHolder, "Muted", bubbleColor);
+        } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
+            if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
+                displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
+            } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
+                displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
+            } else {
+                displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor);
+            }
+        } else if (message.isFileOrImage()
+                && message.getEncryption() != Message.ENCRYPTION_PGP
+                && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
+            if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
+                displayMediaPreviewMessage(viewHolder, message, bubbleColor, type);
+                if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) {
+                    footerWrap = true;
+                }
+            } else if (message.getFileParams().runtime > 0) {
+                displayAudioMessage(viewHolder, message, bubbleColor, type);
+            } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) {
+                displayWebxdcMessage(viewHolder, message, bubbleColor, type);
+            } else {
+                displayOpenableMessage(viewHolder, message, bubbleColor, type);
+            }
+        } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+            if (account.isPgpDecryptionServiceConnected()) {
+                if (conversation instanceof Conversation
+                        && !account.hasPendingPgpIntent((Conversation) conversation)) {
+                    displayInfoMessage(
+                            viewHolder,
+                            activity.getString(R.string.message_decrypting),
+                            bubbleColor);
+                } else {
+                    displayInfoMessage(
+                            viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
+                }
+            } else {
+                displayInfoMessage(
+                        viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
+                viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
+                viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
+            }
+        } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+            displayInfoMessage(
+                    viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
+        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
+            displayInfoMessage(
+                    viewHolder,
+                    activity.getString(R.string.not_encrypted_for_this_device),
+                    bubbleColor);
+        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
+            displayInfoMessage(
+                    viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
+        } else {
+            if (message.isGeoUri()) {
+                displayLocationMessage(viewHolder, message, bubbleColor, type);
+            } else if (message.treatAsDownloadable()) {
+                try {
+                    final URI uri = message.getOob();
+                    displayDownloadableMessage(viewHolder,
+                            message,
+                            activity.getString(
+                                    R.string.check_x_filesize_on_host,
+                                    UIHelper.getFileDescriptionString(activity, message),
+                                    uri.getHost()),
+                            bubbleColor, type);
+                } catch (Exception e) {
+                    displayDownloadableMessage(
+                            viewHolder,
+                            message,
+                            activity.getString(
+                                    R.string.check_x_filesize,
+                                    UIHelper.getFileDescriptionString(activity, message)),
+                            bubbleColor, type);
+                }
+            } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
+                displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor);
+            } else {
+                displayTextMessage(viewHolder, message, bubbleColor, message.getType());
+            }
+        }
+
+        viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0);
+        LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams();
+        statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
+        viewHolder.status_line.setLayoutParams(statusParams);
+
+        setBackgroundTint(viewHolder.message_box, bubbleColor);
+        setTextColor(viewHolder.messageBody, bubbleColor);
+        viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor));
+
+        if (type == RECEIVED) {
+            if (!muted && commands != null && conversation instanceof Conversation) {
+                CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
+                adapter.addAll(commands);
+                viewHolder.commands_list.setAdapter(adapter);
+                viewHolder.commands_list.setVisibility(View.VISIBLE);
+                viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
+                    final Element command = adapter.getItem(pos);
+                    activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
+                });
+            } else {
+                // It's unclear if we can set this to null...
+                ListAdapter adapter = viewHolder.commands_list.getAdapter();
+                if (adapter instanceof ArrayAdapter) {
+                    ((ArrayAdapter<?>) adapter).clear();
+                }
+                viewHolder.commands_list.setVisibility(View.GONE);
+                viewHolder.commands_list.setOnItemClickListener(null);
+            }
+
+            setTextColor(viewHolder.encryption, bubbleColor);
+
+            if (isInValidSession) {
+                viewHolder.encryption.setVisibility(View.GONE);
+            } else {
+                viewHolder.encryption.setVisibility(View.VISIBLE);
+                if (omemoEncryption && !message.isTrusted()) {
+                    viewHolder.encryption.setText(R.string.not_trusted);
+                } else {
+                    viewHolder.encryption.setText(
+                            CryptoHelper.encryptionTypeToText(message.getEncryption()));
+                }
+            }
+        }
+
+        if (type == RECEIVED || type == SENT) {
+            String subject = message.getSubject();
+            if (subject == null && message.getThread() != null) {
+                final var thread = ((Conversation) message.getConversation()).getThread(message.getThread().getContent());
+                if (thread != null) subject = thread.getSubject();
+            }
+            if (muted || subject == null) {
+                viewHolder.subject.setVisibility(View.GONE);
+            } else {
+                viewHolder.subject.setVisibility(View.VISIBLE);
+                viewHolder.subject.setText(subject);
+            }
+        }
+
+        displayStatus(viewHolder, message, type, bubbleColor);
+
+        viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+            @Override
+            public void sendAccessibilityEvent(View host, int eventType) {
+                super.sendAccessibilityEvent(host, eventType);
+                if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
+                    if (viewHolder.messageBody.hasSelection()) {
+                        selectionUuid = message.getUuid();
+                    } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) {
+                        selectionUuid = null;
+                    }
+                }
+            }
+        });
+
+        return view;
+    }
+
+    private void promptOpenKeychainInstall(View view) {
+        activity.showInstallPgpDialog();
+    }
+
+    public FileBackend getFileBackend() {
+        return activity.xmppConnectionService.getFileBackend();
+    }
+
+    public void stopAudioPlayer() {
+        audioPlayer.stop();
+    }
+
+    public void unregisterListenerInAudioPlayer() {
+        audioPlayer.unregisterListener();
+    }
+
+    public void startStopPending() {
+        audioPlayer.startStopPending();
+    }
+
+    public void openDownloadable(Message message) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+                && ContextCompat.checkSelfPermission(
+                                activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                        != PackageManager.PERMISSION_GRANTED) {
+            ConversationFragment.registerPendingMessage(activity, message);
+            ActivityCompat.requestPermissions(
+                    activity,
+                    new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
+                    ConversationsActivity.REQUEST_OPEN_MESSAGE);
+            return;
+        }
+        final DownloadableFile file =
+                activity.xmppConnectionService.getFileBackend().getFile(message);
+        ViewUtil.view(activity, file);
+    }
+
+    private void showLocation(Message message) {
+        for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
+            if (intent.resolveActivity(getContext().getPackageManager()) != null) {
+                getContext().startActivity(intent);
+                return;
+            }
+        }
+        Toast.makeText(
+                        activity,
+                        R.string.no_application_found_to_display_location,
+                        Toast.LENGTH_SHORT)
+                .show();
+    }
+
+    public void updatePreferences() {
+        final AppSettings appSettings = new AppSettings(activity);
+        this.bubbleDesign =
+                new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
+    }
+
+    public void setHighlightedTerm(List<String> terms) {
+        this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
+    }
+
+    public interface OnContactPictureClicked {
+        void onContactPictureClicked(Message message);
+    }
+
+    public interface OnContactPictureLongClicked {
+        void onContactPictureLongClicked(View v, Message message);
+    }
+
+    public interface OnInlineImageLongClicked {
+        boolean onInlineImageLongClicked(Cid cid);
+    }
+
+    private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
+        view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
+    }
+
+    private static ColorStateList bubbleToColorStateList(
+            final View view, final BubbleColor bubbleColor) {
+        final @AttrRes int colorAttributeResId =
+                switch (bubbleColor) {
+                    case SURFACE -> Activities.isNightMode(view.getContext())
+                            ? com.google.android.material.R.attr.colorSurfaceContainerHigh
+                            : com.google.android.material.R.attr.colorSurfaceContainerLow;
+                    case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
+                            ? com.google.android.material.R.attr.colorSurfaceContainerHighest
+                            : com.google.android.material.R.attr.colorSurfaceContainerHigh;
+                    case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
+                    case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
+                    case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
+                    case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
+                };
+        return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
+    }
+
+    public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
+        ImageViewCompat.setImageTintList(
+                imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
+    }
+
+    public static void setImageTintError(final ImageView imageView) {
+        ImageViewCompat.setImageTintList(
+                imageView,
+                ColorStateList.valueOf(
+                        MaterialColors.getColor(
+                                imageView, com.google.android.material.R.attr.colorError)));
+    }
+
+    public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
+        final var color = bubbleToOnSurfaceColor(textView, bubbleColor);
+        textView.setTextColor(color);
+        if (BubbleColor.SURFACES.contains(bubbleColor)) {
+            textView.setLinkTextColor(
+                    MaterialColors.getColor(
+                            textView, com.google.android.material.R.attr.colorPrimary));
+        } else {
+            textView.setLinkTextColor(color);
+        }
+    }
+
+    private static void setTextSize(final TextView textView, final boolean largeFont) {
+        if (largeFont) {
+            textView.setTextAppearance(
+                    com.google.android.material.R.style.TextAppearance_Material3_TitleLarge);
+        } else {
+            textView.setTextAppearance(
+                    com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
+        }
+    }
+
+    private static @ColorInt int bubbleToOnSurfaceVariant(
+            final View view, final BubbleColor bubbleColor) {
+        final @AttrRes int colorAttributeResId;
+        if (BubbleColor.SURFACES.contains(bubbleColor)) {
+            colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
+        } else {
+            colorAttributeResId = bubbleToOnSurface(bubbleColor);
+        }
+        return MaterialColors.getColor(view, colorAttributeResId);
+    }
+
+    private static @ColorInt int bubbleToOnSurfaceColor(
+            final View view, final BubbleColor bubbleColor) {
+        return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
+    }
+
+    public static ColorStateList bubbleToOnSurfaceColorStateList(
+            final View view, final BubbleColor bubbleColor) {
+        return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
+    }
+
+    private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
+        return switch (bubbleColor) {
+            case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
+            case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
+            case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
+            case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
+            case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
+        };
+    }
+
+    public enum BubbleColor {
+        SURFACE,
+        SURFACE_HIGH,
+        PRIMARY,
+        SECONDARY,
+        TERTIARY,
+        WARNING;
+
+        private static final Collection<BubbleColor> SURFACES =
+                Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH);
+    }
+
+    private static class BubbleDesign {
+        public final boolean colorfulChatBubbles;
+        public final boolean largeFont;
+
+        private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
+            this.colorfulChatBubbles = colorfulChatBubbles;
+            this.largeFont = largeFont;
+        }
+    }
+
+    private static class ViewHolder {
+
+        public MaterialButton load_more_messages;
+        public ImageView edit_indicator;
+        public RelativeLayout audioPlayer;
+        protected View status_line;
+        protected LinearLayout message_box;
+        protected View message_box_inner;
+        protected MaterialButton download_button;
+        protected ShapeableImageView image;
+        protected ImageView indicator;
+        protected ImageView indicatorReceived;
+        protected TextView time;
+        protected TextView subject;
+        protected TextView messageBody;
+        protected ImageView contact_picture;
+        protected TextView status_message;
+        protected TextView encryption;
+        protected ListView commands_list;
+        protected GithubIdenticonView thread_identicon;
+    }
+
+    class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
+        @Override
+        protected Drawable[] doInBackground(DownloadableFile... params) {
+            if (isCancelled()) return null;
+
+            Drawable[] d = new Drawable[params.length];
+            for (int i = 0; i < params.length; i++) {
+                try {
+                    d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
+                } catch (final IOException e) {
+                    d[i] = null;
+                }
+            }
+
+            return d;
+        }
+
+        @Override
+        protected void onPostExecute(final Drawable[] d) {
+            if (isCancelled()) return;
+            activity.xmppConnectionService.updateConversationUi();
+        }
+    }
+}

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java 🔗

@@ -279,8 +279,8 @@ public class ConversationAdapter
         void onConversationClick(View view, Conversation conversation);
     }
 
-    static class ConversationViewHolder extends RecyclerView.ViewHolder {
-        private final ItemConversationBinding binding;
+    public static class ConversationViewHolder extends RecyclerView.ViewHolder {
+        public final ItemConversationBinding binding;
 
         private ConversationViewHolder(final ItemConversationBinding binding) {
             super(binding.getRoot());

src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java 🔗

@@ -45,6 +45,14 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
                     "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                     "text/x-tex",
                     "text/plain");
+
+    private static final List<String> ARCHIVE_MIMES =
+            Arrays.asList(
+                    "application/x-7z-compressed",
+                    "application/zip",
+                    "application/rar",
+                    "application/x-gtar",
+                    "application/x-tar");
     public static final List<String> CODE_MIMES = Arrays.asList("text/html", "text/xml");
 
     private final ArrayList<Attachment> attachments = new ArrayList<>();
@@ -95,7 +103,7 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
             return R.drawable.ic_person_48dp;
         } else if (mime.equals("application/vnd.android.package-archive")) {
             return R.drawable.ic_adb_48dp;
-        } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
+        } else if (ARCHIVE_MIMES.contains(mime)) {
             return R.drawable.ic_archive_48dp;
         } else if (mime.equals("application/epub+zip")
                 || mime.equals("application/vnd.amazon.mobi8-ebook")) {

src/main/java/eu/siacs/conversations/ui/fragment/settings/AttachmentsSettingsFragment.java 🔗

@@ -14,10 +14,13 @@ import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.Nullable;
 import androidx.core.content.ContextCompat;
+import androidx.preference.ListPreference;
+import androidx.preference.PreferenceFragmentCompat;
 
 import com.cheogram.android.DownloadDefaultStickers;
 
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.utils.UIHelper;
 
 public class AttachmentsSettingsFragment extends XmppPreferenceFragment {
 
@@ -71,6 +74,21 @@ public class AttachmentsSettingsFragment extends XmppPreferenceFragment {
             runOnUiThread(() -> Toast.makeText(requireActivity(), "Blocked media will be displayed again", Toast.LENGTH_LONG).show());
             return true;
         });
+
+        final ListPreference autoAcceptFileSize = findPreference("auto_accept_file_size");
+        if (autoAcceptFileSize == null) {
+            throw new IllegalStateException("The preference resource file is missing preferences");
+        }
+        setValues(
+                autoAcceptFileSize,
+                R.array.file_size_values,
+                value -> {
+                    if (value <= 0) {
+                        return getString(R.string.never);
+                    } else {
+                        return UIHelper.filesizeToString(value);
+                    }
+                });
     }
 
     protected void downloadStickers() {

src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java 🔗

@@ -71,16 +71,10 @@ public class BackupSettingsFragment extends XmppPreferenceFragment {
                         R.string.pref_create_backup_summary,
                         FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
         createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
-        final int[] choices = getResources().getIntArray(R.array.recurring_backup_values);
-        final CharSequence[] entries = new CharSequence[choices.length];
-        final CharSequence[] entryValues = new CharSequence[choices.length];
-        for (int i = 0; i < choices.length; ++i) {
-            entryValues[i] = String.valueOf(choices[i]);
-            entries[i] = timeframeValueToName(requireContext(), choices[i]);
-        }
-        recurringBackup.setEntries(entries);
-        recurringBackup.setEntryValues(entryValues);
-        recurringBackup.setSummaryProvider(new TimeframeSummaryProvider());
+        setValues(
+                recurringBackup,
+                R.array.recurring_backup_values,
+                value -> timeframeValueToName(requireContext(), value));
     }
 
     @Override

src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java 🔗

@@ -196,7 +196,12 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
             uri = appSettings().getRingtone();
         }
         Log.i(Config.LOGTAG, "current ringtone: " + uri);
-        this.pickRingtoneLauncher.launch(uri);
+        try {
+            this.pickRingtoneLauncher.launch(uri);
+        } catch (final ActivityNotFoundException e) {
+            Toast.makeText(requireActivity(), R.string.no_application_found, Toast.LENGTH_LONG)
+                    .show();
+        }
     }
 
     private AppSettings appSettings() {

src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java 🔗

@@ -4,6 +4,7 @@ import android.content.DialogInterface;
 import android.os.Bundle;
 import android.widget.Toast;
 
+import androidx.annotation.ArrayRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
@@ -11,7 +12,9 @@ import androidx.preference.ListPreference;
 import androidx.preference.Preference;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.R;
@@ -21,6 +24,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager;
 import java.security.KeyStoreException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.concurrent.Callable;
 
 public class SecuritySettingsFragment extends XmppPreferenceFragment {
 
@@ -36,19 +40,12 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
             throw new IllegalStateException("The preference resource file is missing preferences");
         }
         omemo.setSummaryProvider(new OmemoSummaryProvider());
-        final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values);
-        final CharSequence[] entries = new CharSequence[choices.length];
-        final CharSequence[] entryValues = new CharSequence[choices.length];
-        for (int i = 0; i < choices.length; ++i) {
-            entryValues[i] = String.valueOf(choices[i]);
-            entries[i] = timeframeValueToName(requireContext(), choices[i]);
-        }
-        automaticMessageDeletion.setEntries(entries);
-        automaticMessageDeletion.setEntryValues(entryValues);
-        automaticMessageDeletion.setSummaryProvider(new TimeframeSummaryProvider());
+        setValues(
+                automaticMessageDeletion,
+                R.array.automatic_message_deletion_values,
+                value -> timeframeValueToName(requireContext(), value));
     }
 
-
     @Override
     protected void onSharedPreferenceChanged(@NonNull String key) {
         super.onSharedPreferenceChanged(key);
@@ -151,7 +148,6 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
                 .show();
     }
 
-
     private static class OmemoSummaryProvider
             implements Preference.SummaryProvider<ListPreference> {
 

src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java 🔗

@@ -4,6 +4,7 @@ import android.content.Context;
 import android.content.SharedPreferences;
 import android.util.Log;
 
+import androidx.annotation.ArrayRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.preference.ListPreference;
@@ -13,6 +14,7 @@ import androidx.preference.Preference;
 
 import com.rarepebble.colorpicker.ColorPreference;
 
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
 import com.google.common.primitives.Ints;
 
@@ -111,14 +113,29 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
         }
     }
 
-    protected static class TimeframeSummaryProvider
-            implements Preference.SummaryProvider<ListPreference> {
-
-        @Nullable
-        @Override
-        public CharSequence provideSummary(@NonNull ListPreference preference) {
-            final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
-            return timeframeValueToName(preference.getContext(), value == null ? 0 : value);
+    protected void setValues(
+            final ListPreference listPreference,
+            @ArrayRes int resId,
+            final Function<Integer, String> valueToName) {
+        final int[] choices = getResources().getIntArray(resId);
+        final CharSequence[] entries = new CharSequence[choices.length];
+        final CharSequence[] entryValues = new CharSequence[choices.length];
+        for (int i = 0; i < choices.length; ++i) {
+            final int value = choices[i];
+            entryValues[i] = String.valueOf(choices[i]);
+            entries[i] = valueToName.apply(value);
         }
+        listPreference.setEntries(entries);
+        listPreference.setEntryValues(entryValues);
+        listPreference.setSummaryProvider(
+                new Preference.SummaryProvider<ListPreference>() {
+                    @Nullable
+                    @Override
+                    public CharSequence provideSummary(@NonNull ListPreference preference) {
+                        final Integer value =
+                                Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
+                        return valueToName.apply(value == null ? 0 : value);
+                    }
+                });
     }
 }

src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java 🔗

@@ -10,13 +10,13 @@ import eu.siacs.conversations.entities.MucOptions;
 
 public class MucConfiguration {
 
-    public final @StringRes
-    int title;
+    public final @StringRes int title;
     public final String[] names;
     public final boolean[] values;
     public final Option[] options;
 
-    private MucConfiguration(@StringRes int title, String[] names, boolean[] values, Option[] options) {
+    private MucConfiguration(
+            @StringRes int title, String[] names, boolean[] values, Option[] options) {
         this.title = title;
         this.names = names;
         this.values = values;
@@ -25,52 +25,62 @@ public class MucConfiguration {
 
     public static MucConfiguration get(Context context, boolean advanced, MucOptions mucOptions) {
         if (mucOptions.isPrivateAndNonAnonymous()) {
-            String[] names = new String[]{
-                    context.getString(R.string.allow_participants_to_edit_subject),
-                    context.getString(R.string.allow_participants_to_invite_others)
-            };
-            boolean[] values = new boolean[]{
-                    mucOptions.participantsCanChangeSubject(),
-                    mucOptions.allowInvites()
-            };
-            final Option[] options = new Option[]{
-                    new Option("muc#roomconfig_changesubject"),
-                    new Option("muc#roomconfig_allowinvites")
-            };
+            String[] names =
+                    new String[] {
+                        context.getString(R.string.allow_participants_to_edit_subject),
+                        context.getString(R.string.allow_participants_to_invite_others)
+                    };
+            boolean[] values =
+                    new boolean[] {
+                        mucOptions.participantsCanChangeSubject(), mucOptions.allowInvites()
+                    };
+            final Option[] options =
+                    new Option[] {
+                        new Option("muc#roomconfig_changesubject"),
+                        new Option("muc#roomconfig_allowinvites")
+                    };
             return new MucConfiguration(R.string.conference_options, names, values, options);
         } else {
             final String[] names;
             final boolean[] values;
             final Option[] options;
             if (advanced) {
-                names = new String[]{
-                        context.getString(R.string.non_anonymous),
-                        context.getString(R.string.allow_participants_to_edit_subject),
-                        context.getString(R.string.moderated)
-                };
-                values = new boolean[]{
-                        mucOptions.nonanonymous(),
-                        mucOptions.participantsCanChangeSubject(),
-                        mucOptions.moderated()
-                };
-                options = new Option[]{
-                        new Option("muc#roomconfig_whois", "anyone", "moderators"),
-                        new Option("muc#roomconfig_changesubject"),
-                        new Option("muc#roomconfig_moderatedroom")
-                };
+                names =
+                        new String[] {
+                            context.getString(R.string.non_anonymous),
+                            context.getString(R.string.allow_participants_to_edit_subject),
+                            context.getString(R.string.moderated),
+                            context.getString(R.string.allow_private_messages)
+                        };
+                values =
+                        new boolean[] {
+                            mucOptions.nonanonymous(),
+                            mucOptions.participantsCanChangeSubject(),
+                            mucOptions.moderated(),
+                            mucOptions.allowPmRaw()
+                        };
+                options =
+                        new Option[] {
+                            new Option("muc#roomconfig_whois", "anyone", "moderators"),
+                            new Option("muc#roomconfig_changesubject"),
+                            new Option("muc#roomconfig_moderatedroom"),
+                            new Option("muc#roomconfig_allowpm", "anyone", "moderators"),
+                        };
             } else {
-                names = new String[]{
-                        context.getString(R.string.non_anonymous),
-                        context.getString(R.string.allow_participants_to_edit_subject),
-                };
-                values = new boolean[]{
-                        mucOptions.nonanonymous(),
-                        mucOptions.participantsCanChangeSubject()
-                };
-                options = new Option[]{
-                        new Option("muc#roomconfig_whois", "anyone", "moderators"),
-                        new Option("muc#roomconfig_changesubject")
-                };
+                names =
+                        new String[] {
+                            context.getString(R.string.non_anonymous),
+                            context.getString(R.string.allow_participants_to_edit_subject),
+                        };
+                values =
+                        new boolean[] {
+                            mucOptions.nonanonymous(), mucOptions.participantsCanChangeSubject()
+                        };
+                options =
+                        new Option[] {
+                            new Option("muc#roomconfig_whois", "anyone", "moderators"),
+                            new Option("muc#roomconfig_changesubject")
+                        };
             }
             return new MucConfiguration(R.string.channel_options, names, values, options);
         }
@@ -108,9 +118,9 @@ public class MucConfiguration {
 
     public Bundle toBundle(boolean[] values) {
         Bundle bundle = new Bundle();
-        for(int i = 0; i < values.length; ++i) {
+        for (int i = 0; i < values.length; ++i) {
             final Option option = options[i];
-            bundle.putString(option.name,option.values[values[i] ? 0 : 1]);
+            bundle.putString(option.name, option.values[values[i] ? 0 : 1]);
         }
         return bundle;
     }
@@ -121,13 +131,12 @@ public class MucConfiguration {
 
         private Option(String name) {
             this.name = name;
-            this.values = new String[]{"1","0"};
+            this.values = new String[] {"1", "0"};
         }
 
         private Option(String name, String on, String off) {
             this.name = name;
-            this.values = new String[]{on,off};
+            this.values = new String[] {on, off};
         }
     }
-
 }

src/main/java/eu/siacs/conversations/utils/AccountUtils.java 🔗

@@ -38,23 +38,27 @@ public class AccountUtils {
         return false;
     }
 
-    public static String publicDeviceId(final Account account) {
+    public static String publicDeviceId(final Account account, final long installationId) {
         final UUID uuid;
         try {
             uuid = UUID.fromString(account.getUuid());
         } catch (final IllegalArgumentException e) {
             return account.getUuid();
         }
+        return createUuid4(uuid.getMostSignificantBits(), installationId).toString();
+    }
+
+    public static UUID createUuid4(long mostSigBits, long leastSigBits) {
         final byte[] bytes =
                 Bytes.concat(
-                        Longs.toByteArray(uuid.getLeastSignificantBits()),
-                        Longs.toByteArray(uuid.getLeastSignificantBits()));
+                        Longs.toByteArray(mostSigBits),
+                        Longs.toByteArray(leastSigBits));
         bytes[6] &= 0x0f; /* clear version        */
         bytes[6] |= 0x40; /* set to version 4     */
         bytes[8] &= 0x3f; /* clear variant        */
         bytes[8] |= 0x80; /* set to IETF variant  */
         final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
-        return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString();
+        return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
     }
 
     public static List<String> getEnabledAccounts(final XmppConnectionService service) {

src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java 🔗

@@ -1,36 +1,59 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import eu.siacs.conversations.BuildConfig;
+import eu.siacs.conversations.services.NotificationService;
+
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.io.Writer;
 import java.lang.Thread.UncaughtExceptionHandler;
-
-import eu.siacs.conversations.services.NotificationService;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
 
 public class ExceptionHandler implements UncaughtExceptionHandler {
 
-	private final UncaughtExceptionHandler defaultHandler;
-	private final Context context;
-
-	ExceptionHandler(final Context context) {
-		this.context = context;
-		this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
-	}
-
-	@Override
-	public void uncaughtException(@NonNull Thread thread, final Throwable throwable) {
-		NotificationService.cancelIncomingCallNotification(context);
-		final Writer stringWriter = new StringWriter();
-		final PrintWriter printWriter = new PrintWriter(stringWriter);
-		throwable.printStackTrace(printWriter);
-		final String stacktrace = stringWriter.toString();
-		printWriter.close();
-		ExceptionHelper.writeToStacktraceFile(context, stacktrace);
-		this.defaultHandler.uncaughtException(thread, throwable);
-	}
-
+    private static final SimpleDateFormat DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ENGLISH);
+
+    private final UncaughtExceptionHandler defaultHandler;
+    private final Context context;
+
+    ExceptionHandler(final Context context) {
+        this.context = context;
+        this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+    }
+
+    @Override
+    public void uncaughtException(@NonNull Thread thread, final Throwable throwable) {
+        NotificationService.cancelIncomingCallNotification(context);
+        final String stacktrace;
+        try (final StringWriter stringWriter = new StringWriter();
+                final PrintWriter printWriter = new PrintWriter(stringWriter)) {
+            throwable.printStackTrace(printWriter);
+            stacktrace = stringWriter.toString();
+        } catch (final IOException e) {
+            return;
+        }
+        final List<String> report =
+                ImmutableList.of(
+                        String.format(
+                                "Version: %s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME),
+                        String.format("Manufacturer: %s", Strings.nullToEmpty(Build.MANUFACTURER)),
+                        String.format("Device: %s", Strings.nullToEmpty(Build.DEVICE)),
+                        String.format("Timestamp: %s", DATE_FORMAT.format(new Date())),
+                        stacktrace);
+        ExceptionHelper.writeToStacktraceFile(context, Joiner.on("\n").join(report));
+        this.defaultHandler.uncaughtException(thread, throwable);
+    }
 }

src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java 🔗

@@ -1,12 +1,12 @@
 package eu.siacs.conversations.utils;
 
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.Signature;
 import android.util.Log;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Charsets;
+import com.google.common.io.CharSink;
+import com.google.common.io.Files;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
@@ -17,20 +17,16 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.XmppActivity;
 
-import java.io.BufferedReader;
-import java.io.FileInputStream;
+import java.io.File;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.lang.ClassNotFoundException;
 import java.text.SimpleDateFormat;
-import java.util.Date;
 import java.util.Locale;
 
 public class ExceptionHelper {
 
     private static final String FILENAME = "stacktrace.txt";
-    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
 
     public static void init(final Context context) {
         if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) {
@@ -45,72 +41,65 @@ public class ExceptionHelper {
             return false;
         } catch (final ClassNotFoundException e) { }
 
+        final XmppConnectionService service =
+                activity == null ? null : activity.xmppConnectionService;
+        if (service == null) {
+            return false;
+        }
+        final AppSettings appSettings = new AppSettings(activity);
+        if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) {
+            return false;
+        }
+        final Account account = AccountUtils.getFirstEnabled(service);
+        if (account == null) {
+            return false;
+        }
+        final var file = new File(activity.getCacheDir(), FILENAME);
+        if (!file.exists()) {
+            return false;
+        }
+        final String report;
         try {
-            final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
-            if (service == null) {
-                return false;
-            }
-            final AppSettings appSettings = new AppSettings(activity);
-            if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) {
-                return false;
-            }
-            final Account account = AccountUtils.getFirstEnabled(service);
-            if (account == null) {
-                return false;
-            }
-            final FileInputStream file = activity.openFileInput(FILENAME);
-            final InputStreamReader inputStreamReader = new InputStreamReader(file);
-            final BufferedReader stacktrace = new BufferedReader(inputStreamReader);
-            final StringBuilder report = new StringBuilder();
-            final PackageManager pm = activity.getPackageManager();
-            final PackageInfo packageInfo;
-            try {
-                packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES);
-                final String versionName = packageInfo.versionName;
-                final int versionCode = packageInfo.versionCode;
-                final int version = versionCode > 10000 ? (versionCode / 100) : versionCode;
-                report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n');
-                report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n');
-                Signature[] signatures = packageInfo.signatures;
-                if (signatures != null && signatures.length >= 1) {
-                    report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n');
-                }
-                report.append('\n');
-            } catch (final Exception e) {
-                return false;
-            }
-            String line;
-            while ((line = stacktrace.readLine()) != null) {
-                report.append(line);
-                report.append('\n');
-            }
-            file.close();
-            activity.deleteFile(FILENAME);
-            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
-            builder.setTitle(activity.getString(R.string.crash_report_title, activity.getString(R.string.app_name)));
-            builder.setMessage(activity.getString(R.string.crash_report_message, activity.getString(R.string.app_name)));
-            builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> {
-
-                Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace");
-                Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
-                Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE);
-                service.sendMessage(message);
-            });
-            builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> appSettings.setSendCrashReports(false));
-            builder.create().show();
-            return true;
-        } catch (final IOException ignored) {
+            report = Files.asCharSource(file, Charsets.UTF_8).read();
+        } catch (final IOException e) {
             return false;
         }
+        if (file.delete()) {
+            Log.d(Config.LOGTAG, "deleted crash report file");
+        }
+        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
+        builder.setTitle(
+                activity.getString(
+                        R.string.crash_report_title, activity.getString(R.string.app_name)));
+        builder.setMessage(
+                activity.getString(
+                        R.string.crash_report_message, activity.getString(R.string.app_name)));
+        builder.setPositiveButton(
+                activity.getText(R.string.send_now),
+                (dialog, which) -> {
+                    Log.d(
+                            Config.LOGTAG,
+                            "using account="
+                                    + account.getJid().asBareJid()
+                                    + " to send in stack trace");
+                    Conversation conversation =
+                            service.findOrCreateConversation(
+                                    account, Config.BUG_REPORTS, false, true);
+                    Message message = new Message(conversation, report, Message.ENCRYPTION_NONE);
+                    service.sendMessage(message);
+                });
+        builder.setNegativeButton(
+                activity.getText(R.string.send_never),
+                (dialog, which) -> appSettings.setSendCrashReports(false));
+        builder.create().show();
+        return true;
     }
 
-    static void writeToStacktraceFile(Context context, String msg) {
+    static void writeToStacktraceFile(final Context context, final String msg) {
         try {
-            OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE);
-            os.write(msg.getBytes());
-            os.flush();
-            os.close();
-        } catch (IOException ignored) {
+            Files.asCharSink(new File(context.getCacheDir(), FILENAME), Charsets.UTF_8).write(msg);
+        } catch (IOException e) {
+            Log.w(Config.LOGTAG, "could not write stack trace to file", e);
         }
     }
 }

src/main/java/eu/siacs/conversations/utils/MimeUtils.java 🔗

@@ -141,6 +141,7 @@ public final class MimeUtils {
         add("application/vnd.sun.xml.writer.global", "sxg");
         add("application/vnd.sun.xml.writer.template", "stw");
         add("application/vnd.visio", "vsd");
+        add("application/x-7z-compressed","7z");
         add("application/x-abiword", "abw");
         add("application/x-apple-diskimage", "dmg");
         add("application/x-bcpio", "bcpio");

src/main/java/eu/siacs/conversations/utils/Resolver.java 🔗

@@ -6,24 +6,56 @@ import android.util.Log;
 
 import androidx.annotation.NonNull;
 
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
 import com.google.common.net.InetAddresses;
 import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.xmpp.Jid;
+
+import org.minidns.dnsmessage.Question;
+import org.minidns.dnsname.DnsName;
+import org.minidns.dnsname.InvalidDnsNameException;
+import org.minidns.dnsqueryresult.DnsQueryResult;
+import org.minidns.record.A;
+import org.minidns.record.AAAA;
+import org.minidns.record.CNAME;
+import org.minidns.record.Data;
+import org.minidns.record.InternetAddressRR;
+import org.minidns.record.Record;
+import org.minidns.record.SRV;
 
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
 
-//import de.gultsch.minidns.AndroidDNSClient;
 import org.minidns.AbstractDnsClient;
 import org.minidns.DnsCache;
 import org.minidns.DnsClient;
@@ -44,13 +76,35 @@ import org.minidns.record.Data;
 import org.minidns.record.InternetAddressRR;
 import org.minidns.record.Record;
 import org.minidns.record.SRV;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xmpp.Jid;
 
 public class Resolver {
 
+    private static final Comparator<Result> RESULT_COMPARATOR =
+            (left, right) -> {
+                if (left.priority == right.priority) {
+                    if (left.directTls == right.directTls) {
+                        if (left.ip == null && right.ip == null) {
+                            return 0;
+                        } else if (left.ip != null && right.ip != null) {
+                            if (left.ip instanceof Inet4Address
+                                    && right.ip instanceof Inet4Address) {
+                                return 0;
+                            } else {
+                                return left.ip instanceof Inet4Address ? -1 : 1;
+                            }
+                        } else {
+                            return left.ip != null ? -1 : 1;
+                        }
+                    } else {
+                        return left.directTls ? -1 : 1;
+                    }
+                } else {
+                    return left.priority - right.priority;
+                }
+            };
+
+    private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12);
+
     public static final int DEFAULT_PORT_XMPP = 5222;
 
     private static final String DIRECT_TLS_SERVICE = "_xmpps-client";
@@ -203,7 +257,7 @@ public class Resolver {
         try {
             DnsName.from(hostname);
             return false;
-        } catch (IllegalArgumentException e) {
+        } catch (final InvalidDnsNameException | IllegalArgumentException e) {
             return true;
         }
     }
@@ -224,206 +278,234 @@ public class Resolver {
         }
     }
 
-
     public static boolean useDirectTls(final int port) {
         return port == 443 || port == 5223;
     }
 
     public static List<Result> resolve(final String domain) {
-        final  List<Result> ipResults = fromIpAddress(domain);
-        if (ipResults.size() > 0) {
+        final List<Result> ipResults = fromIpAddress(domain);
+        if (!ipResults.isEmpty()) {
             return ipResults;
         }
-        final List<Result> results = new ArrayList<>();
-        final List<Result> fallbackResults = new ArrayList<>();
-        final Thread[] threads = new Thread[3];
-        threads[0] = new Thread(() -> {
-            try {
-                final List<Result> list = resolveSrv(domain, true);
-                synchronized (results) {
-                    results.addAll(list);
-                }
-            } catch (final Throwable throwable) {
-                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
-                }
-            }
-        });
-        threads[1] = new Thread(() -> {
-            try {
-                final List<Result> list = resolveSrv(domain, false);
-                synchronized (results) {
-                    results.addAll(list);
-                }
-            } catch (final Throwable throwable) {
-                if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
-                }
-            }
-        });
-        threads[2] = new Thread(() -> {
-            List<Result> list = resolveNoSrvRecords(DnsName.from(domain), true);
-            synchronized (fallbackResults) {
-                fallbackResults.addAll(list);
-            }
-        });
-        for (final Thread thread : threads) {
-            thread.start();
-        }
+
+        final var startTls = resolveSrvAsFuture(domain, false);
+        final var directTls = resolveSrvAsFuture(domain, true);
+
+        final var combined = merge(ImmutableList.of(startTls, directTls));
+
+        final var combinedWithFallback =
+                Futures.transformAsync(
+                        combined,
+                        results -> {
+                            if (results.isEmpty()) {
+                                return resolveNoSrvAsFuture(DnsName.from(domain), true);
+                            } else {
+                                return Futures.immediateFuture(results);
+                            }
+                        },
+                        MoreExecutors.directExecutor());
+        final var orderedFuture =
+                Futures.transform(
+                        combinedWithFallback,
+                        all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all),
+                        MoreExecutors.directExecutor());
         try {
-            threads[0].join();
-            threads[1].join();
-            if (results.size() > 0) {
-                threads[2].interrupt();
-                synchronized (results) {
-                    Collections.sort(results);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
-                    return results;
-                }
-            } else {
-                threads[2].join();
-                synchronized (fallbackResults) {
-                    Collections.sort(fallbackResults);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
-                    return fallbackResults;
-                }
-            }
-        } catch (InterruptedException e) {
-            for (Thread thread : threads) {
-                thread.interrupt();
-            }
+            final var ordered = orderedFuture.get();
+            Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered);
+            return ordered;
+        } catch (final ExecutionException e) {
+            Log.d(Config.LOGTAG, "error resolving DNS", e);
+            return Collections.emptyList();
+        } catch (final InterruptedException e) {
+            Log.d(Config.LOGTAG, "DNS resolution interrupted");
             return Collections.emptyList();
         }
     }
 
-    private static List<Result> fromIpAddress(String domain) {
-        if (!IP.matches(domain)) {
-            return Collections.emptyList();
-        }
-        try {
-            Result result = new Result();
-            result.ip = InetAddress.getByName(domain);
+    private static List<Result> fromIpAddress(final String domain) {
+        if (IP.matches(domain)) {
+            final InetAddress inetAddress;
+            try {
+                inetAddress = InetAddress.getByName(domain);
+            } catch (final UnknownHostException e) {
+                return Collections.emptyList();
+            }
+            final Result result = new Result();
+            result.ip = inetAddress;
             result.port = DEFAULT_PORT_XMPP;
-            result.authenticated = true;
             return Collections.singletonList(result);
-        } catch (UnknownHostException e) {
+        } else {
             return Collections.emptyList();
         }
     }
 
-    private static List<Result> resolveSrv(String domain, final boolean directTls) throws IOException {
-        final String dnsNameS = (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain;
-        DnsName dnsName = DnsName.from(dnsNameS);
-        ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
-        final List<Result> results = new ArrayList<>();
-        final List<Thread> threads = new ArrayList<>();
-        for (SRV record : result.getAnswersOrEmptySet()) {
-            if (record.name.length() == 0 && record.priority == 0) {
-                continue;
-            }
-            final boolean authentic = result.isAuthenticData() || record.target.toString().equals(knownSRV.get(dnsNameS));
-            threads.add(new Thread(() -> {
-                final List<Result> ipv4s = resolveIp(record, A.class, authentic, directTls);
-                if (ipv4s.size() == 0) {
-                    Result resolverResult = Result.fromRecord(record, directTls);
-                    resolverResult.authenticated = result.isAuthenticData();
-                    ipv4s.add(resolverResult);
-                }
-                synchronized (results) {
-                    results.addAll(ipv4s);
-                }
+    private static ListenableFuture<List<Result>> resolveSrvAsFuture(
+            final String domain, final boolean directTls) {
+        final DnsName dnsName =
+                DnsName.from(
+                        (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
+        final var resultFuture = resolveAsFuture(dnsName, SRV.class);
+        return Futures.transformAsync(
+                resultFuture,
+                result -> resolveIpsAsFuture(result, directTls),
+                MoreExecutors.directExecutor());
+    }
 
-            }));
-            threads.add(new Thread(() -> {
-                final List<Result> ipv6s = resolveIp(record, AAAA.class, authentic, directTls);
-                synchronized (results) {
-                    results.addAll(ipv6s);
-                }
-            }));
-        }
-        for (Thread thread : threads) {
-            thread.start();
-        }
-        for (Thread thread : threads) {
-            try {
-                thread.join();
-            } catch (InterruptedException e) {
-                return Collections.emptyList();
+    @NonNull
+    private static ListenableFuture<List<Result>> resolveIpsAsFuture(
+            final ResolverResult<SRV> srvResolverResult, final boolean directTls) {
+        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
+                new ImmutableList.Builder<>();
+        for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) {
+            if (record.target.length() == 0 && record.priority == 0) {
+                continue;
             }
+            final var ipv4sRaw =
+                    resolveIpsAsFuture(
+                            record, A.class, srvResolverResult.isAuthenticData(), directTls);
+            final var ipv4s =
+                    Futures.transform(
+                            ipv4sRaw,
+                            results -> {
+                                if (results.isEmpty()) {
+                                    final Result resolverResult =
+                                            Result.fromRecord(record, directTls);
+                                    resolverResult.authenticated =
+                                            srvResolverResult.isAuthenticData();
+                                    return Collections.singletonList(resolverResult);
+                                } else {
+                                    return results;
+                                }
+                            },
+                            MoreExecutors.directExecutor());
+            final var ipv6s =
+                    resolveIpsAsFuture(
+                            record, AAAA.class, srvResolverResult.isAuthenticData(), directTls);
+            futuresBuilder.add(ipv4s);
+            futuresBuilder.add(ipv6s);
         }
-        return results;
+        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
+        return merge(futures);
     }
 
-    private static <D extends InternetAddressRR> List<Result> resolveIp(SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
-        List<Result> list = new ArrayList<>();
-        try {
-            ResolverResult<D> results = resolveWithFallback(srv.target, type);
-            for (D record : results.getAnswersOrEmptySet()) {
-                Result resolverResult = Result.fromRecord(srv, directTls);
-                resolverResult.authenticated = results.isAuthenticData() && authenticated;
-                resolverResult.ip = record.getInetAddress();
-                list.add(resolverResult);
-            }
-        } catch (Throwable t) {
-            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage());
-        }
-        return list;
+    private static ListenableFuture<List<Result>> merge(
+            final Collection<ListenableFuture<List<Result>>> futures) {
+        return Futures.transform(
+                Futures.successfulAsList(futures),
+                lists -> {
+                    final var builder = new ImmutableList.Builder<Result>();
+                    for (final Collection<Result> list : lists) {
+                        if (list == null) {
+                            continue;
+                        }
+                        builder.addAll(list);
+                    }
+                    return builder.build();
+                },
+                MoreExecutors.directExecutor());
     }
 
-    private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
-        final List<Result> results = new ArrayList<>();
-        try {
-            ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
-            for (A a : aResult.getAnswersOrEmptySet()) {
-                Result r = Result.createDefault(dnsName, a.getInetAddress());
-                r.authenticated = aResult.isAuthenticData();
-                results.add(r);
-            }
-            ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
-            for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
-                Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
-                r.authenticated = aaaaResult.isAuthenticData();
-                results.add(r);
-            }
-            if (results.size() == 0 && withCnames) {
-                ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
-                for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
-                    for (Result r : resolveNoSrvRecords(cname.name, false)) {
-                        r.authenticated = r.authenticated && cnameResult.isAuthenticData();
-                        results.add(r);
+    private static <D extends InternetAddressRR<?>>
+            ListenableFuture<List<Result>> resolveIpsAsFuture(
+                    final SRV srv, Class<D> type, boolean authenticated, boolean directTls) {
+        final var resultFuture = resolveAsFuture(srv.target, type);
+        return Futures.transform(
+                resultFuture,
+                result -> {
+                    final var builder = new ImmutableList.Builder<Result>();
+                    for (D record : result.getAnswersOrEmptySet()) {
+                        Result resolverResult = Result.fromRecord(srv, directTls);
+                        resolverResult.authenticated =
+                                result.isAuthenticData()
+                                        && authenticated; // TODO technically it does not matter if
+                        // the IP
+                        // was authenticated
+                        resolverResult.ip = record.getInetAddress();
+                        builder.add(resolverResult);
                     }
-                }
-            }
-        } catch (final Throwable throwable) {
-            if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
-            }
-        }
-        results.add(Result.createDefault(dnsName));
-        return results;
+                    return builder.build();
+                },
+                MoreExecutors.directExecutor());
     }
 
-    private static <D extends Data> ResolverResult<D> resolveWithFallback(DnsName dnsName, Class<D> type) throws IOException {
-        final Question question = new Question(dnsName, Record.TYPE.getType(type));
-        if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
-            try {
-                ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
-                if (result.wasSuccessful() && !result.isAuthenticData()) {
-                    Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
-                }
-                return result;
-            } catch (DnssecValidationFailedException e) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
-            } catch (IOException e) {
-                throw e;
-            } catch (Throwable throwable) {
-                Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
-            }
+    private static ListenableFuture<List<Result>> resolveNoSrvAsFuture(
+            final DnsName dnsName, boolean cName) {
+        final ImmutableList.Builder<ListenableFuture<List<Result>>> futuresBuilder =
+                new ImmutableList.Builder<>();
+        ListenableFuture<List<Result>> aRecordResults =
+                Futures.transform(
+                        resolveAsFuture(dnsName, A.class),
+                        result ->
+                                Lists.transform(
+                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                        a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())),
+                        MoreExecutors.directExecutor());
+        futuresBuilder.add(aRecordResults);
+        ListenableFuture<List<Result>> aaaaRecordResults =
+                Futures.transform(
+                        resolveAsFuture(dnsName, AAAA.class),
+                        result ->
+                                Lists.transform(
+                                        ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                        aaaa ->
+                                                Result.createDefault(
+                                                        dnsName, aaaa.getInetAddress(), result.isAuthenticData())),
+                        MoreExecutors.directExecutor());
+        futuresBuilder.add(aaaaRecordResults);
+        if (cName) {
+            ListenableFuture<List<Result>> cNameRecordResults =
+                    Futures.transformAsync(
+                            resolveAsFuture(dnsName, CNAME.class),
+                            result -> {
+                                Collection<ListenableFuture<List<Result>>> test =
+                                        Lists.transform(
+                                                ImmutableList.copyOf(result.getAnswersOrEmptySet()),
+                                                cname -> resolveNoSrvAsFuture(cname.target, false));
+                                return merge(test);
+                            },
+                            MoreExecutors.directExecutor());
+            futuresBuilder.add(cNameRecordResults);
         }
-        return ResolverApi.INSTANCE.resolve(question);
+        final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
+        final var noSrvFallbacks = merge(futures);
+        return Futures.transform(
+                noSrvFallbacks,
+                results -> {
+                    if (results.isEmpty()) {
+                        return Collections.singletonList(Result.createDefault(dnsName));
+                    } else {
+                        return results;
+                    }
+                },
+                MoreExecutors.directExecutor());
+    }
+
+    private static <D extends Data> ListenableFuture<ResolverResult<D>> resolveAsFuture(
+            final DnsName dnsName, final Class<D> type) {
+        return Futures.submit(
+                () -> {
+                    final Question question = new Question(dnsName, Record.TYPE.getType(type));
+                    if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) {
+                        try {
+                            ResolverResult<D> result = DnssecResolverApi.INSTANCE.resolve(question);
+                            if (result.wasSuccessful() && !result.isAuthenticData()) {
+                                Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons());
+                            }
+                            return result;
+                        } catch (DnssecValidationFailedException e) {
+                            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
+                        } catch (IOException e) {
+                            throw e;
+                        } catch (Throwable throwable) {
+                            Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable);
+                        }
+                    }
+                    return ResolverApi.INSTANCE.resolve(question);
+                },
+                DNS_QUERY_EXECUTOR);
     }
 
-    public static class Result implements Comparable<Result> {
+    public static class Result {
         public static final String DOMAIN = "domain";
         public static final String IP = "ip";
         public static final String HOSTNAME = "hostname";
@@ -438,40 +520,42 @@ public class Resolver {
         private boolean authenticated = false;
         private int priority;
 
-        static Result fromRecord(SRV srv, boolean directTls) {
-            Result result = new Result();
+        static Result fromRecord(final SRV srv, final boolean directTls) {
+            final Result result = new Result();
             result.port = srv.port;
-            result.hostname = srv.name;
+            result.hostname = srv.target;
             result.directTls = directTls;
             result.priority = srv.priority;
             return result;
         }
 
-        static Result createDefault(DnsName hostname, InetAddress ip) {
+        static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) {
             Result result = new Result();
             result.port = DEFAULT_PORT_XMPP;
             result.hostname = hostname;
             result.ip = ip;
+            result.authenticated = authenticated;
             return result;
         }
 
-        static Result createDefault(DnsName hostname) {
-            return createDefault(hostname, null);
+        static Result createDefault(final DnsName hostname) {
+            return createDefault(hostname, null, false);
         }
 
-        public static Result fromCursor(Cursor cursor) {
+        public static Result fromCursor(final Cursor cursor) {
             final Result result = new Result();
             try {
-                result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP)));
-            } catch (UnknownHostException e) {
+                result.ip =
+                        InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP)));
+            } catch (final UnknownHostException e) {
                 result.ip = null;
             }
-            final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
+            final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME));
             result.hostname = hostname == null ? null : DnsName.from(hostname);
-            result.port = cursor.getInt(cursor.getColumnIndex(PORT));
-            result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY));
-            result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
-            result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
+            result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT));
+            result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY));
+            result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0;
+            result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0;
             return result;
         }
 
@@ -479,26 +563,18 @@ public class Resolver {
         public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
-
             Result result = (Result) o;
-
-            if (port != result.port) return false;
-            if (directTls != result.directTls) return false;
-            if (authenticated != result.authenticated) return false;
-            if (priority != result.priority) return false;
-            if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false;
-            return hostname != null ? hostname.equals(result.hostname) : result.hostname == null;
+            return port == result.port
+                    && directTls == result.directTls
+                    && authenticated == result.authenticated
+                    && priority == result.priority
+                    && Objects.equal(ip, result.ip)
+                    && Objects.equal(hostname, result.hostname);
         }
 
         @Override
         public int hashCode() {
-            int result = ip != null ? ip.hashCode() : 0;
-            result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
-            result = 31 * result + port;
-            result = 31 * result + (directTls ? 1 : 0);
-            result = 31 * result + (authenticated ? 1 : 0);
-            result = 31 * result + priority;
-            return result;
+            return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority);
         }
 
         public InetAddress getIp() {
@@ -522,38 +598,16 @@ public class Resolver {
         }
 
         @Override
+        @NonNull
         public String toString() {
-            return "Result{" +
-                    "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' +
-                    ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' +
-                    ", port=" + port +
-                    ", directTls=" + directTls +
-                    ", authenticated=" + authenticated +
-                    ", priority=" + priority +
-                    '}';
-        }
-
-        @Override
-        public int compareTo(@NonNull Result result) {
-            if (result.priority == priority) {
-                if (directTls == result.directTls) {
-                    if (ip == null && result.ip == null) {
-                        return 0;
-                    } else if (ip != null && result.ip != null) {
-                        if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) {
-                            return 0;
-                        } else {
-                            return ip instanceof Inet4Address ? -1 : 1;
-                        }
-                    } else {
-                        return ip != null ? -1 : 1;
-                    }
-                } else {
-                    return directTls ? 1 : -1;
-                }
-            } else {
-                return priority - result.priority;
-            }
+            return MoreObjects.toStringHelper(this)
+                    .add("ip", ip)
+                    .add("hostname", hostname)
+                    .add("port", port)
+                    .add("directTls", directTls)
+                    .add("authenticated", authenticated)
+                    .add("priority", priority)
+                    .toString();
         }
 
         public ContentValues toContentValues() {
@@ -626,5 +680,4 @@ public class Resolver {
             return result;
         }
     }
-
 }

src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java 🔗

@@ -18,6 +18,7 @@ import android.util.Log;
 import androidx.annotation.NonNull;
 import androidx.core.app.NotificationCompat;
 import androidx.work.ForegroundInfo;
+import androidx.work.WorkManager;
 import androidx.work.Worker;
 import androidx.work.WorkerParameters;
 
@@ -35,7 +36,6 @@ import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.receiver.WorkManagerEventReceiver;
 import eu.siacs.conversations.utils.BackupFileHeader;
 import eu.siacs.conversations.utils.Compatibility;
 
@@ -99,6 +99,7 @@ public class ExportBackupWorker extends Worker {
     @NonNull
     @Override
     public Result doWork() {
+        setForegroundAsync(getForegroundInfo());
         final List<File> files;
         try {
             files = export();
@@ -227,18 +228,14 @@ public class ExportBackupWorker extends Worker {
                         IV,
                         salt);
         final var notification = getNotification();
-        if (!recurringBackup) {
-            final var cancel = new Intent(context, WorkManagerEventReceiver.class);
-            cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP);
-            final var cancelPendingIntent =
-                    PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS);
-            notification.addAction(
-                    new NotificationCompat.Action.Builder(
-                                    R.drawable.ic_cancel_24dp,
-                                    context.getString(R.string.cancel),
-                                    cancelPendingIntent)
-                            .build());
-        }
+        final var cancelPendingIntent =
+                WorkManager.getInstance(context).createCancelPendingIntent(getId());
+        notification.addAction(
+                new NotificationCompat.Action.Builder(
+                                R.drawable.ic_cancel_24dp,
+                                context.getString(R.string.cancel),
+                                cancelPendingIntent)
+                        .build());
         final Progress progress = new Progress(notification, max, count);
         final File directory = file.getParentFile();
         if (directory != null && directory.mkdirs()) {

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

@@ -5,7 +5,9 @@ import androidx.annotation.NonNull;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+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.Collection;
@@ -16,7 +18,7 @@ import java.util.stream.Collectors;
 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 implements Node {
 	private final String name;
@@ -141,6 +143,10 @@ public class Element implements Node {
 		return ImmutableList.copyOf(this.children);
 	}
 
+	public void setAttribute(final String name, final boolean value) {
+		this.setAttribute(name, value ? "1" : "0");
+	}
+
 	// Deprecated: you probably want bindTo or replaceChildren
 	public Element setChildren(List<Element> children) {
 		this.childNodes = new ArrayList(children);
@@ -165,6 +171,31 @@ public class Element implements Node {
 		return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining());
 	}
 
+	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) {
+			return Optional.absent();
+		}
+		return Optional.fromNullable(Ints.tryParse(value));
+	}
+
+	public Jid getAttributeAsJid(String name) {
+		final String jid = this.getAttribute(name);
+		if (jid != null && !jid.isEmpty()) {
+			try {
+				return Jid.ofEscaped(jid);
+			} catch (final IllegalArgumentException e) {
+				return InvalidJid.of(jid, this instanceof Message);
+			}
+		}
+		return null;
+	}
+
 	public Element setAttribute(String name, String value) {
 		if (name != null && value != null) {
 			this.attributes.put(name, value);
@@ -224,7 +255,7 @@ public class Element implements Node {
 		return result;
 	}
 
-	public Element removeAttribute(String name) {
+	public Element removeAttribute(final String name) {
 		this.attributes.remove(name);
 		return this;
 	}
@@ -242,26 +273,6 @@ public class Element implements Node {
 		}
 	}
 
-	public Optional<Integer> getOptionalIntAttribute(final String name) {
-		final String value = getAttribute(name);
-		if (value == null) {
-			return Optional.absent();
-		}
-		return Optional.fromNullable(Ints.tryParse(value));
-	}
-
-	public Jid getAttributeAsJid(String name) {
-		final String jid = this.getAttribute(name);
-		if (jid != null && !jid.isEmpty()) {
-			try {
-				return Jid.ofEscaped(jid);
-			} catch (final IllegalArgumentException e) {
-				return InvalidJid.of(jid, this instanceof MessagePacket);
-			}
-		}
-		return null;
-	}
-
 	public Hashtable<String, String> getAttributes() {
 		return this.attributes;
 	}

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 VCARD4 = "urn:ietf:params:xml:ns:vcard-4.0";
     public static final String REPORTING = "urn:xmpp:reporting:1";
@@ -80,4 +110,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;
@@ -90,8 +94,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 🔗

@@ -15,7 +15,6 @@ import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.core.util.Consumer;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
@@ -70,6 +69,7 @@ import javax.net.ssl.X509KeyManager;
 import javax.net.ssl.X509TrustManager;
 
 import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
@@ -83,6 +83,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;
@@ -105,18 +108,42 @@ 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.AuthenticationFailure;
+import im.conversations.android.xmpp.model.AuthenticationRequest;
+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.Failure;
+import im.conversations.android.xmpp.model.sasl.Mechanisms;
+import im.conversations.android.xmpp.model.sasl.Response;
+import im.conversations.android.xmpp.model.sasl.SaslError;
+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.sasl2.UserAgent;
+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;
 
@@ -151,6 +178,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;
@@ -163,46 +191,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, Pair<OnIqPacketReceived, ScheduledFuture>>> packetCallbacks =
-            new Hashtable<>();
+    private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
+    private final Hashtable<String, Pair<Iq, Pair<Consumer<Iq>, ScheduledFuture>>> packetCallbacks = new Hashtable<>();
     private final Set<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners =
             new HashSet<>();
     private final AppSettings appSettings;
@@ -215,8 +209,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;
@@ -233,12 +227,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;
@@ -254,7 +249,11 @@ public class XmppConnection implements Runnable {
     public XmppConnection(final Account account, final XmppConnectionService service) {
         this.account = account;
         this.mXmppConnectionService = service;
-        this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
+        this.appSettings = mXmppConnectionService.getAppSettings();
+        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) {
@@ -412,7 +411,7 @@ public class XmppConnection implements Runnable {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
                     return;
                 }
-                if (results.size() == 0) {
+                if (results.isEmpty()) {
                     Log.e(
                             Config.LOGTAG,
                             account.getJid().asBareJid() + ": Resolver results were empty");
@@ -664,7 +663,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)
@@ -677,8 +676,13 @@ public class XmppConnection implements Runnable {
                 if (processSuccess(success)) {
                     break;
                 }
-            } else if (nextTag.isStart("failure")) {
-                final Element failure = tagReader.readElement(nextTag);
+            } else if (nextTag.isStart("failure", Namespace.SASL)) {
+                final var failure = tagReader.readElement(nextTag, Failure.class);
+                processFailure(failure);
+            } else if (nextTag.isStart("failure", Namespace.SASL_2)) {
+                final var failure =
+                        tagReader.readElement(
+                                nextTag, im.conversations.android.xmpp.model.sasl2.Failure.class);
                 processFailure(failure);
             } else if (nextTag.isStart("continue", Namespace.SASL_2)) {
                 // two step sasl2 - we don’t support this yet
@@ -690,10 +694,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);
@@ -709,7 +713,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);
@@ -720,7 +724,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;
@@ -747,11 +751,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 {
@@ -787,11 +791,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);
         }
@@ -811,26 +815,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));
@@ -844,47 +845,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) {
@@ -911,8 +889,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) {
@@ -931,7 +908,8 @@ public class XmppConnection implements Runnable {
                             account.getJid().asBareJid()
                                     + ": successfully enabled carbons (via Bind 2.0)");
                     features.carbonsEnabled = true;
-                } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
+                } else if (currentLoginInfo.inlineBindFeatures != null
+                        && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
                     negotiatedCarbons = true;
                     Log.d(
                             Config.LOGTAG,
@@ -997,7 +975,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(
@@ -1007,7 +985,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);
                 }
@@ -1031,7 +1009,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()
@@ -1047,7 +1027,7 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private void processFailure(final Element failure) throws IOException {
+    private void processFailure(final AuthenticationFailure failure) throws IOException {
         final SaslMechanism.Version version;
         try {
             version = SaslMechanism.Version.of(failure);
@@ -1061,10 +1041,21 @@ public class XmppConnection implements Runnable {
             account.resetFastToken();
             mXmppConnectionService.databaseBackend.updateAccount(account);
         }
-        if (failure.hasChild("temporary-auth-failure")) {
+        final var errorCondition = failure.getErrorCondition();
+        if (errorCondition instanceof SaslError.InvalidMechanism
+                || errorCondition instanceof SaslError.MechanismTooWeak) {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": invalid or too weak mechanism. resetting quick start");
+            if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) {
+                mXmppConnectionService.databaseBackend.updateAccount(account);
+            }
+            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+        } else if (errorCondition instanceof SaslError.TemporaryAuthFailure) {
             throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
-        } else if (failure.hasChild("account-disabled")) {
-            final String text = failure.findChildContent("text");
+        } else if (errorCondition instanceof SaslError.AccountDisabled) {
+            final String text = failure.getText();
             if (Strings.isNullOrEmpty(text)) {
                 throw new StateChangingException(Account.State.UNAUTHORIZED);
             }
@@ -1101,22 +1092,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 {
@@ -1129,16 +1106,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();
@@ -1146,7 +1137,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) {
@@ -1169,8 +1160,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(),
@@ -1189,8 +1180,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,
@@ -1237,8 +1228,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) {
@@ -1253,29 +1245,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");
@@ -1287,25 +1259,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='"
@@ -1321,9 +1287,9 @@ 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 var callback = getIqPacketReceivedCallback(packet);
@@ -1338,7 +1304,7 @@ public class XmppConnection implements Runnable {
             final ScheduledFuture timeoutFuture = callback.second;
             try {
                 if (timeoutFuture == null || timeoutFuture.cancel(false)) {
-                    callback.first.onIqPacketReceived(account, packet);
+                    callback.first.accept(packet);
                 }
             } catch (final StateChangingError error) {
                 throw new StateChangingException(error.state);
@@ -1346,10 +1312,10 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private Pair<OnIqPacketReceived, ScheduledFuture> getIqPacketReceivedCallback(final IqPacket stanza)
+    private Pair<Consumer<Iq>, ScheduledFuture> 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 new Pair<>(this.unregisteredIqListener, null);
@@ -1389,8 +1355,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='"
@@ -1407,12 +1374,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='"
@@ -1429,17 +1396,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;
@@ -1511,11 +1476,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()
@@ -1524,8 +1491,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()
@@ -1543,8 +1509,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)) {
@@ -1561,15 +1526,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
@@ -1581,7 +1546,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);
@@ -1609,9 +1574,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);
@@ -1623,13 +1588,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);
@@ -1641,26 +1606,28 @@ public class XmppConnection implements Runnable {
         final String firstMessage =
                 saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
         final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
-        final Element authenticate;
+        final AuthenticationRequest authenticate;
+        final LoginInfo loginInfo;
         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());
+            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 =
@@ -1678,7 +1645,7 @@ public class XmppConnection implements Runnable {
                     return;
                 }
             }
-            this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
+            loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
             this.hashTokenRequest = hashTokenRequest;
             authenticate =
                     generateAuthenticationRequest(
@@ -1686,7 +1653,7 @@ public class XmppConnection implements Runnable {
         } else {
             throw new AssertionError("Missing implementation for " + version);
         }
-
+        this.loginInfo = loginInfo;
         if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
             mXmppConnectionService.databaseBackend.updateAccount(account);
         }
@@ -1697,17 +1664,17 @@ public class XmppConnection implements Runnable {
                         + ": Authenticating with "
                         + version
                         + "/"
-                        + LoginInfo.mechanism(this.loginInfo).getMechanism());
-        authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism());
+                        + LoginInfo.mechanism(loginInfo).getMechanism());
+        authenticate.setMechanism(LoginInfo.mechanism(loginInfo));
         synchronized (this.mStanzaQueue) {
             this.stanzasSentBeforeAuthentication = this.stanzasSent;
             tagWriter.writeElement(authenticate);
         }
     }
 
-    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(
@@ -1721,7 +1688,7 @@ public class XmppConnection implements Runnable {
                             + mechanisms);
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
-        validateRequireChannelBinding(saslMechanism);
+        checkRequireChannelBinding(saslMechanism);
         if (SaslMechanism.hashedToken(saslMechanism)) {
             return;
         }
@@ -1740,7 +1707,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) {
@@ -1751,31 +1718,56 @@ 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 AuthenticationRequest generateAuthenticationRequest(
             final String firstMessage, final boolean usingFast) {
         return generateAuthenticationRequest(
                 firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
     }
 
-    private Element generateAuthenticationRequest(
+    private AuthenticationRequest 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);
         }
-        final Element userAgent = authenticate.addChild("user-agent");
-        userAgent.setAttribute("id", AccountUtils.publicDeviceId(account));
-        userAgent
-                .addChild("software")
-                .setContent(mXmppConnectionService.getString(R.string.app_name));
+        final var userAgent =
+                authenticate.addExtension(
+                        new UserAgent(
+                                AccountUtils.publicDeviceId(
+                                        account, appSettings.getInstallationId())));
+        userAgent.setSoftware(
+                String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
         if (!PhoneHelper.isEmulator()) {
-            userAgent
-                    .addChild("device")
-                    .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
+            userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
         }
         // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
         // (because we would rather just do a normal SM/resume)
@@ -1784,31 +1776,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);
-        bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name));
+        final var bind = new Bind();
+        bind.setTag(BuildConfig.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;
     }
@@ -1816,12 +1806,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();

src/main/java/eu/siacs/conversations/xmpp/forms/Data.java 🔗

@@ -59,8 +59,8 @@ public class Data extends Element {
 		field.setValues(values);
 	}
 
-	public void submit(Bundle options) {
-		for (Field field : getFields()) {
+	public void submit(final Bundle options) {
+		for (final Field field : getFields()) {
 			if (options.containsKey(field.getFieldName())) {
 				field.setValue(options.getString(field.getFieldName()));
 			}

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;
@@ -165,14 +166,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) {
@@ -265,11 +266,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");
@@ -440,7 +441,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);
@@ -494,10 +495,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");
@@ -722,7 +724,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     rtpSessionProposal.sessionId,
                     RtpEndUserState.RETRACTED);
         }
-        final MessagePacket messagePacket =
+        final var messagePacket =
                 mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
         writeLogMissedOutgoing(
                 account,
@@ -791,7 +793,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;
@@ -801,7 +803,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);
@@ -843,7 +845,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;
@@ -869,7 +871,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()) {
@@ -880,11 +882,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;
                     }
@@ -895,7 +897,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) {
@@ -946,7 +948,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 🔗

@@ -45,13 +45,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.DtmfSender;
 import org.webrtc.EglBase;
@@ -145,24 +145,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()));
             }
         }
     }
@@ -193,9 +194,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
         return webRTCWrapper.applyDtmfTone(tone);
     }
 
-    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,
@@ -224,7 +226,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(
@@ -235,7 +237,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,
@@ -265,7 +267,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();
@@ -304,17 +306,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;
@@ -330,12 +332,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,
@@ -349,12 +351,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);
@@ -406,10 +408,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(
@@ -494,14 +497,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();
@@ -604,10 +607,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,
@@ -660,10 +663,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(
@@ -697,8 +700,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));
         }
     }
 
@@ -723,10 +726,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,
@@ -782,16 +785,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()
@@ -885,7 +888,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());
@@ -897,9 +900,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() {
@@ -913,20 +916,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;
@@ -1005,7 +1008,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     private boolean applyIceRestart(
-            final JinglePacket jinglePacket,
+            final Iq jinglePacket,
             final RtpContentMap restartContentMap,
             final boolean isOffer)
             throws ExecutionException, InterruptedException {
@@ -1106,7 +1109,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) {
@@ -1149,12 +1152,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<>() {
@@ -1173,7 +1176,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);
@@ -1233,13 +1236,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<>() {
@@ -1264,7 +1267,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();
@@ -1409,7 +1412,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;
         }
@@ -1480,8 +1483,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);
     }
 
@@ -1951,8 +1954,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);
     }
 
@@ -2020,9 +2023,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() {
@@ -2340,7 +2343,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
         this.jingleConnectionManager.ensureConnectionIsRegistered(this);
         this.webRTCWrapper.setup(this.xmppConnectionService);
         this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
-        this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled());
+        // this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled());
+        this.webRTCWrapper.setMicrophoneEnabledOrThrow(true);
     }
 
     private void acceptCallFromProposed() {
@@ -2375,8 +2379,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
@@ -2397,7 +2401,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);
@@ -2556,34 +2560,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);
     }
@@ -2604,7 +2608,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());
@@ -2612,21 +2616,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");
@@ -2634,7 +2638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
                         }
                         handleIqErrorResponse(response);
                     }
-                    if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
                         handleIqTimeoutResponse(response);
                     }
                 });
@@ -2782,7 +2786,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
 
     @Override
     public void onCallIntegrationMicrophoneEnabled(final boolean enabled) {
-        this.webRTCWrapper.setMicrophoneEnabled(enabled);
+        // this is called every time we switch audio devices. Thus it would re-enable a microphone
+        // that was previous disabled by the user. A proper implementation would probably be to
+        // track user choice and enable the microphone with a userEnabled() &&
+        // callIntegration.isMicrophoneEnabled() condition
+        Log.d(Config.LOGTAG, "ignoring onCallIntegrationMicrophoneEnabled(" + enabled + ")");
+        // this.webRTCWrapper.setMicrophoneEnabled(enabled);
     }
 
     @Override
@@ -2827,13 +2836,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.HashMap;
@@ -39,7 +39,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)) {
@@ -53,7 +53,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,98 +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) {
-		removeChild(findChild("body"));
-		prependChild(new Element("body").setContent(text));
-	}
-
-	public void setAxolotlMessage(Element axolotlMessage) {
-		removeChild(findChild("body"));
-		prependChild(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/AuthenticationFailure.java 🔗

@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model;
+
+import im.conversations.android.xmpp.model.sasl.SaslError;
+
+public abstract class AuthenticationFailure extends StreamElement {
+
+    protected AuthenticationFailure(Class<? extends AuthenticationFailure> clazz) {
+        super(clazz);
+    }
+
+    public SaslError getErrorCondition() {
+        return this.getExtension(SaslError.class);
+    }
+
+    public String getText() {
+        return this.findChildContent("text");
+    }
+}

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

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model;
+
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+
+public abstract class AuthenticationRequest extends StreamElement{
+
+
+    protected AuthenticationRequest(Class<? extends AuthenticationRequest> clazz) {
+        super(clazz);
+    }
+
+    public abstract void setMechanism(final SaslMechanism mechanism);
+}

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(getChildren(), clazz::isInstance);
+    }
+
+    public <E extends Extension> E getExtension(final Class<E> clazz) {
+        final var extension = Iterables.find(getChildren(), 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(getChildren(), clazz::isInstance), clazz::cast);
+    }
+
+    public Collection<ExtensionFactory.Id> getExtensionIds() {
+        return Collections2.transform(
+                getChildren(), 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,28 @@
+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);
+    }
+
+    public void setTag(final String tag) {
+        this.addExtension(new Tag(tag));
+    }
+}

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/bind2/Tag.java 🔗

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

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 : getChildren()) {
+            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,19 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.AuthenticationRequest;
+import im.conversations.android.xmpp.model.StreamElement;
+
+@XmlElement
+public class Auth extends AuthenticationRequest {
+
+    public Auth() {
+        super(Auth.class);
+    }
+
+    @Override
+    public void setMechanism(final SaslMechanism mechanism) {
+        this.setAttribute("mechanism", mechanism.getMechanism());
+    }
+}

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

@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.AuthenticationFailure;
+
+@XmlElement
+public class Failure extends AuthenticationFailure {
+    public Failure() {
+        super(Failure.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/SaslError.java 🔗

@@ -0,0 +1,89 @@
+package im.conversations.android.xmpp.model.sasl;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+public class SaslError extends Extension {
+
+    private SaslError(final Class<? extends SaslError> clazz) {
+        super(clazz);
+    }
+
+    @XmlElement
+    public static class Aborted extends SaslError {
+        public Aborted() {
+            super(Aborted.class);
+        }
+    }
+
+    @XmlElement
+    public static class AccountDisabled extends SaslError {
+        public AccountDisabled() {
+            super(AccountDisabled.class);
+        }
+    }
+
+    @XmlElement
+    public static class CredentialsExpired extends SaslError {
+        public CredentialsExpired() {
+            super(CredentialsExpired.class);
+        }
+    }
+
+    @XmlElement
+    public static class EncryptionRequired extends SaslError {
+        public EncryptionRequired() {
+            super(EncryptionRequired.class);
+        }
+    }
+
+    @XmlElement
+    public static class IncorrectEncoding extends SaslError {
+        public IncorrectEncoding() {
+            super(IncorrectEncoding.class);
+        }
+    }
+
+    @XmlElement
+    public static class InvalidAuthzid extends SaslError {
+        public InvalidAuthzid() {
+            super(InvalidAuthzid.class);
+        }
+    }
+
+    @XmlElement
+    public static class InvalidMechanism extends SaslError {
+        public InvalidMechanism() {
+            super(InvalidMechanism.class);
+        }
+    }
+
+    @XmlElement
+    public static class MalformedRequest extends SaslError {
+        public MalformedRequest() {
+            super(MalformedRequest.class);
+        }
+    }
+
+    @XmlElement
+    public static class MechanismTooWeak extends SaslError {
+        public MechanismTooWeak() {
+            super(MechanismTooWeak.class);
+        }
+    }
+
+    @XmlElement
+    public static class NotAuthorized extends SaslError {
+
+        public NotAuthorized() {
+            super(NotAuthorized.class);
+        }
+    }
+
+    @XmlElement
+    public static class TemporaryAuthFailure extends SaslError {
+        public TemporaryAuthFailure() {
+            super(TemporaryAuthFailure.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,19 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.AuthenticationRequest;
+
+@XmlElement
+public class Authenticate extends AuthenticationRequest {
+
+    public Authenticate() {
+        super(Authenticate.class);
+    }
+
+    @Override
+    public void setMechanism(final SaslMechanism mechanism) {
+        this.setAttribute("mechanism", mechanism.getMechanism());
+    }
+}

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/Device.java 🔗

@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+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 Device(final String device) {
+        this();
+        this.setContent(device);
+    }
+}

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

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

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/Software.java 🔗

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

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/sasl2/UserAgent.java 🔗

@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.sasl2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class UserAgent extends Extension {
+
+    public UserAgent() {
+        super(UserAgent.class);
+    }
+
+    public UserAgent(final String userAgentId) {
+        this();
+        this.setAttribute("id", userAgentId);
+    }
+
+    public void setSoftware(final String software) {
+        this.addExtension(new Software(software));
+    }
+
+    public void setDevice(final String device) {
+        this.addExtension(new Device(device));
+    }
+}

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) {
+        removeChild(findChild("body"));
+        prependChild(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/main/res/drawable/ic_new_releases_24dp.xml 🔗

@@ -1,5 +1,10 @@
-<vector android:height="24dp" android:tint="#FFFFFF"
-    android:viewportHeight="24" android:viewportWidth="24"
-    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="@android:color/white" android:pathData="M23,12l-2.44,-2.78 0.34,-3.68 -3.61,-0.82 -1.89,-3.18L12,3 8.6,1.54 6.71,4.72l-3.61,0.81 0.34,3.68L1,12l2.44,2.78 -0.34,3.69 3.61,0.82 1.89,3.18L12,21l3.4,1.46 1.89,-3.18 3.61,-0.82 -0.34,-3.68L23,12zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M23,12l-2.44,-2.78 0.34,-3.68 -3.61,-0.82 -1.89,-3.18L12,3 8.6,1.54 6.71,4.72l-3.61,0.81 0.34,3.68L1,12l2.44,2.78 -0.34,3.69 3.61,0.82 1.89,3.18L12,21l3.4,1.46 1.89,-3.18 3.61,-0.82 -0.34,-3.68L23,12zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
 </vector>

src/main/res/layout/activity_edit_account.xml 🔗

@@ -107,8 +107,8 @@
                                     android:id="@+id/hostname_layout"
                                     android:layout_width="0dp"
                                     android:layout_height="wrap_content"
-                                    android:layout_weight="0.7"
                                     android:layout_marginEnd="4sp"
+                                    android:layout_weight="0.7"
                                     android:hint="@string/account_settings_hostname">
 
                                     <EditText
@@ -123,8 +123,8 @@
                                     android:id="@+id/port_layout"
                                     android:layout_width="0dp"
                                     android:layout_height="wrap_content"
-                                    android:layout_weight="0.3"
                                     android:layout_marginStart="4sp"
+                                    android:layout_weight="0.3"
                                     android:hint="@string/account_settings_port">
 
                                     <EditText
@@ -154,7 +154,8 @@
                     android:layout_marginTop="@dimen/activity_vertical_margin"
                     android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginBottom="@dimen/activity_vertical_margin"
-                    android:visibility="gone">
+                    android:visibility="gone"
+                    tools:visibility="visible">
 
                     <LinearLayout
                         android:layout_width="match_parent"
@@ -210,7 +211,8 @@
                     android:layout_marginTop="@dimen/activity_vertical_margin"
                     android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginBottom="@dimen/activity_vertical_margin"
-                    android:visibility="gone">
+                    android:visibility="gone"
+                    tools:visibility="visible">
 
                     <LinearLayout
                         android:layout_width="match_parent"
@@ -242,7 +244,36 @@
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:layout_gravity="end"
-                                    android:paddingLeft="4dp"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+                            </TableRow>
+
+                        </TableLayout>
+
+                        <TableLayout
+                            android:id="@+id/server_info_login_mechanism"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:shrinkColumns="0"
+                            android:stretchColumns="1"
+                            android:visibility="gone">
+
+                            <TableRow
+                                android:layout_width="fill_parent"
+                                android:layout_height="wrap_content">
+
+                                <TextView
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:ellipsize="end"
+                                    android:singleLine="true"
+                                    android:text="@string/server_info_login_mechanism"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+
+                                <TextView
+                                    android:id="@+id/login_mechanism"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium" />
                             </TableRow>
 
@@ -272,8 +303,7 @@
                                     android:id="@+id/server_info_pep"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -294,8 +324,7 @@
                                     android:id="@+id/server_info_blocking"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -316,8 +345,7 @@
                                     android:id="@+id/server_info_sm"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -338,8 +366,7 @@
                                     android:id="@+id/server_info_external_service"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -360,8 +387,7 @@
                                     android:id="@+id/server_info_roster_version"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -382,8 +408,7 @@
                                     android:id="@+id/server_info_carbons"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -404,8 +429,7 @@
                                     android:id="@+id/server_info_mam"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -426,8 +450,7 @@
                                     android:id="@+id/server_info_csi"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium"
                                     tools:ignore="RtlHardcoded" />
                             </TableRow>
@@ -449,8 +472,7 @@
                                     android:id="@+id/server_info_push"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium" />
                             </TableRow>
 
@@ -459,7 +481,6 @@
                                 android:layout_height="wrap_content">
 
                                 <TextView
-                                    android:id="@+id/server_info_http_upload_description"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:ellipsize="end"
@@ -471,10 +492,50 @@
                                     android:id="@+id/server_info_http_upload"
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
-                                    android:layout_gravity="right"
-                                    android:paddingLeft="4dp"
+                                    android:layout_gravity="end"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+                            </TableRow>
+
+                            <TableRow
+                                android:layout_width="fill_parent"
+                                android:layout_height="wrap_content">
+
+                                <TextView
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:ellipsize="end"
+                                    android:singleLine="true"
+                                    android:text="@string/server_info_bind2"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+
+                                <TextView
+                                    android:id="@+id/server_info_bind2"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:layout_gravity="end"
                                     android:textAppearance="?textAppearanceBodyMedium" />
                             </TableRow>
+
+                            <TableRow
+                                android:layout_width="fill_parent"
+                                android:layout_height="wrap_content">
+
+                                <TextView
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:ellipsize="end"
+                                    android:singleLine="true"
+                                    android:text="@string/server_info_sasl2"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+
+                                <TextView
+                                    android:id="@+id/server_info_sasl2"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:layout_gravity="end"
+                                    android:textAppearance="?textAppearanceBodyMedium" />
+                            </TableRow>
+
                         </TableLayout>
 
                         <RelativeLayout
@@ -547,6 +608,7 @@
                                 android:layout_alignParentEnd="true"
                                 android:layout_centerVertical="true"
                                 android:background="?attr/selectableItemBackgroundBorderless"
+                                android:contentDescription="@string/edit_nick"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="@drawable/ic_edit_24dp"
                                 android:visibility="visible" />
@@ -728,6 +790,7 @@
                                 android:layout_alignParentEnd="true"
                                 android:layout_centerVertical="true"
                                 android:background="?attr/selectableItemBackgroundBorderless"
+                                android:contentDescription="@string/delete_pgp_key"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="@drawable/ic_delete_24dp"
                                 android:visibility="visible" />
@@ -774,7 +837,7 @@
                                     android:layout_width="wrap_content"
                                     android:layout_height="wrap_content"
                                     android:background="?attr/selectableItemBackgroundBorderless"
-                                    android:contentDescription="@string/copy_omemo_clipboard_description"
+                                    android:contentDescription="@string/show_qr_code"
                                     android:padding="@dimen/image_button_padding"
                                     android:src="@drawable/ic_qr_code_24dp"
                                     android:visibility="visible" />
@@ -802,7 +865,8 @@
                     android:layout_marginTop="@dimen/activity_vertical_margin"
                     android:layout_marginRight="@dimen/activity_horizontal_margin"
                     android:layout_marginBottom="@dimen/activity_vertical_margin"
-                    android:visibility="gone">
+                    android:visibility="gone"
+                    tools:visibility="visible">
 
                     <LinearLayout
                         android:layout_width="match_parent"

src/main/res/layout/activity_muc_details.xml 🔗

@@ -189,6 +189,7 @@
                                 android:layout_centerVertical="true"
                                 android:layout_gravity="center_horizontal"
                                 android:background="?attr/selectableItemBackgroundBorderless"
+                                android:contentDescription="@string/edit_configuration"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="@drawable/ic_settings_24dp" />
                         </RelativeLayout>
@@ -356,6 +357,7 @@
                                 android:layout_alignParentEnd="true"
                                 android:layout_centerVertical="true"
                                 android:background="?attr/selectableItemBackgroundBorderless"
+                                android:contentDescription="@string/edit_nick"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="@drawable/ic_edit_24dp" />
                         </RelativeLayout>
@@ -383,6 +385,7 @@
                                 android:layout_centerVertical="true"
                                 android:layout_gravity="center_horizontal"
                                 android:background="?attr/selectableItemBackgroundBorderless"
+                                android:contentDescription="@string/change_notification_settings"
                                 android:padding="@dimen/image_button_padding"
                                 android:src="@drawable/ic_notifications_24dp" />
                         </RelativeLayout>

src/main/res/layout/activity_publish_profile_picture.xml 🔗

@@ -45,7 +45,8 @@
                     <ImageView
                         android:id="@+id/account_image"
                         android:layout_width="@dimen/publish_avatar_size"
-                        android:layout_height="@dimen/publish_avatar_size" />
+                        android:layout_height="@dimen/publish_avatar_size"
+                        android:contentDescription="@string/your_avatar_tap_to_select_new_avatar" />
                 </FrameLayout>
 
                 <TextView

src/main/res/layout/activity_uri_handler.xml 🔗

@@ -4,7 +4,7 @@
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:background="?colorPrimaryContainer"
+        android:background="?colorSurface"
         android:padding="24dp">
 
         <ProgressBar
@@ -13,7 +13,7 @@
             android:layout_height="wrap_content"
             android:layout_centerInParent="true"
             android:indeterminate="true"
-            android:indeterminateTint="?colorOnPrimaryContainer" />
+            android:indeterminateTint="?colorPrimary" />
 
         <TextView
             android:id="@+id/error"
@@ -24,7 +24,7 @@
             android:layout_marginTop="16dp"
             android:gravity="center_horizontal"
             android:textAppearance="?textAppearanceBodyMedium"
-            android:textColor="?colorOnPrimaryContainer"
+            android:textColor="?colorError"
             android:visibility="invisible" />
 
     </RelativeLayout>

src/main/res/layout/item_message_content.xml 🔗

@@ -85,6 +85,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
+            android:layout_marginHorizontal="10dp"
             android:divider="@android:color/transparent"
             android:dividerHeight="0dp"></ListView>
 

src/main/res/values-ar/strings.xml 🔗

@@ -528,7 +528,6 @@
     <string name="message_copied_to_clipboard">تم نسخ الرسالة إلى الحافظة</string>
     <string name="message">رسالة</string>
     <string name="private_messages_are_disabled">الرسائل الخاصة معطلة</string>
-    <string name="huawei_protected_apps">التطبيقات المُؤمَّنَة</string>
     <string name="mtm_accept_cert">تقبُّل الشهادات المجهولة ؟</string>
     <string name="mtm_trust_anchor">إنّ شهادة الخادوم غير مُوقَّعَة مِن طرف هيئة شهادات معروفة.</string>
     <string name="mtm_connect_anyway">بالرغم مِن ذلك هل تريد مواصلة الإتصال ؟</string>
@@ -635,7 +634,6 @@
     <string name="please_enter_password">يرجى إدخال الكلمة السرية للحساب</string>
     <string name="rtp_state_declined_or_busy">مشغول</string>
     <string name="more_options">خيارات أخرى</string>
-    <string name="huawei_protected_apps_summary">للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف تطبيق Conversations إلى قائمة التطبيقات المحميّة.</string>
     <string name="account_status_regis_not_sup">إنشاء الحسابات غير مدعومة مِن طرف الخادم</string>
     <string name="error_publish_avatar_no_server_support">خادمك لا يدعم نشر الصور الرمزية</string>
     <string name="server_info_external_service_discovery">XEP-0215: استكشاف خدمة خارجية</string>

src/main/res/values-bg/strings.xml 🔗

@@ -663,8 +663,6 @@
     <string name="message_copied_to_clipboard">Съобщението е копирано</string>
     <string name="message">Съобщение</string>
     <string name="private_messages_are_disabled">Личните съобщения са изключени</string>
-    <string name="huawei_protected_apps">Защитени приложения</string>
-    <string name="huawei_protected_apps_summary">Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите „Conversations“ към списъка от защитени приложения.</string>
     <string name="mtm_accept_cert">Приемане на непознатия сертификат?</string>
     <string name="mtm_trust_anchor">Сървърният сертификат не е подписан от познат център за сертификация.</string>
     <string name="mtm_accept_servername">Приемане на несъвпадащото име на сървъра?</string>

src/main/res/values-bn-rIN/strings.xml 🔗

@@ -26,8 +26,8 @@
     <string name="minute_ago">এক মিনিট আগে</string>
     <string name="minutes_ago">%d মিনিট আগে</string>
     <plurals name="x_unread_conversations">
-        <item quantity="one">%dটাই কথোপকথন পড়া বাকি</item>
-        <item quantity="other">%dকথোেকথন পড়া হয়নি</item>
+        <item quantity="one">%dটি চ্যাট পড়া হয়নি</item>
+        <item quantity="other">%dটি চ্যাট পড়া হয়নি</item>
     </plurals>
     <string name="sending">পাঠানো হচ্ছে...</string>
     <string name="message_decrypting">অপেক্ষা করুন, সাঙ্কেতিক সন্দেশ পঠিত হচ্ছে...</string>
@@ -39,7 +39,7 @@
     <string name="moderator">নির্ধারক</string>
     <string name="participant">অংশগ্রহণকারী</string>
     <string name="visitor">অতিথি</string>
-    <string name="remove_contact_text">আপনি কি আপনার পরিচিতি তালিকা থেকে %s-কে অপসারণ করতে চান? এই যোগাযোগের সাথে কথোপকথনগুলি সরানো হবে না।</string>
+    <string name="remove_contact_text">আপনি কি আপনার পরিচিতিদের তালিকা থেকে %s কে মুছে ফেলতে চান? এই পরিচিতির চ্যাট মোছা হবে না।</string>
     <string name="block_contact_text">%s-কে বার্তা পাঠানো থেকে ব্লক করতে চান?</string>
     <string name="unblock_contact_text">আপনি কি %s-কে আনব্লক করতে চান এবং তাদের আপনাকে বার্তা পাঠানোর অনুমতি দিতে চান?</string>
     <string name="contact_blocked">ব্যক্তিটিকে ব্লক্ করা হয়েছে</string>
@@ -74,7 +74,7 @@
     <string name="preparing_images">ছবিগুলি পাঠানোর জন্য তৈরী করা হচ্ছে</string>
     <string name="sharing_files_please_wait">ফাইলগুলো শেয়ার করা হচ্ছে, অপেক্ষা করুন</string>
     <string name="action_clear_history">প্রতিলিপি মুছে ফেলা যাক</string>
-    <string name="clear_conversation_history">Conversation-এর সব প্রতিলিপি মুছে ফেলা যাক</string>
+    <string name="clear_conversation_history">চ্যাটের ইতিহাস মুছুন</string>
     <string name="clear_histor_msg">এই কথোপকথনের সবকটি বার্তাই কি মুছে ফেলতে চান?
 \n‌
 \n<b>সতর্ক থাকবেন:</b> সার্ভার বা অন্য যন্ত্রে থাকা বার্তা কিন্তু অপরিআর্তিতই থাকবে।</string>
@@ -104,4 +104,12 @@
     <string name="create_private_group_chat">ব্যক্তিগত গ্রুপ চ্যাট তৈরি করুন</string>
     <string name="create_public_channel">পাবলিক চ্যানেল তৈরি করা যাক</string>
     <string name="discover_channels">বর্তমান চ্যানেলগুলির মধ্যে থেকে খোঁজা যাক</string>
+    <string name="remove_bookmark">আপনি কি %s-এর জন্য বুকমার্কটি মুছে দিতে চান?</string>
+    <string name="block_domain_text">%s থেকে সবাইকে ব্লক করতে চান?</string>
+    <string name="crash_report_message">আপনার XMPP একাউন্ট থেকে স্ট্যাক ট্রেস গুলি পাঠালে %1$s-এর উন্নয়নে সাহায্য হয়।</string>
+    <string name="remove_bookmark_and_close">আপনি কি %s-এর জন্য বুকমার্কটি মুছে দিতে এবং ওটার চ্যাট আর্কাইভ করে দিতে চান?</string>
+    <string name="title_activity_share_with">শেয়ার করুন এদের সঙ্গে…</string>
+    <string name="action_archive_chat">চ্যাট আর্কাইভ করুন</string>
+    <string name="title_activity_new_chat">নতুন চ্যাট</string>
+    <string name="unblock_domain_text">%s থেকে সবাইকে আনব্লক করতে চান?</string>
 </resources>

src/main/res/values-ca/strings.xml 🔗

@@ -647,9 +647,6 @@
     <string name="message_copied_to_clipboard">Missatge copiat al portapapers</string>
     <string name="message">Missatge</string>
     <string name="private_messages_are_disabled">Els missatges privats estan desactivats</string>
-    <string name="huawei_protected_apps">Aplicacions protegides</string>
-    <string name="huawei_protected_apps_summary">Per continuar rebent notificacions, fins i tot quan la pantalla està apagada, heu 
-d\'afegir Converses a la llista d\'aplicacions protegides.</string>
     <string name="mtm_accept_cert">Voleu acceptar un certificat desconegut?</string>
     <string name="mtm_trust_anchor">El certificat del servidor no està signat per una autoritat de certificació coneguda.</string>
     <string name="mtm_accept_servername">Voleu acceptar el nom del servidor associat?</string>

src/main/res/values-cs/strings.xml 🔗

@@ -26,9 +26,9 @@
     <string name="minute_ago">před minutou</string>
     <string name="minutes_ago">před %d minutami</string>
     <plurals name="x_unread_conversations">
-        <item quantity="one">%d nepřečtená konverzace</item>
-        <item quantity="few">%d nepřečtené konverzace</item>
-        <item quantity="other">%d nepřečtených konverzací</item>
+        <item quantity="one">%d nepřečtený chat</item>
+        <item quantity="few">%d nepřečtené chaty</item>
+        <item quantity="other">%d nepřečtených chatů</item>
     </plurals>
     <string name="sending">odesílám…</string>
     <string name="message_decrypting">Dešifrování zprávy. Chvíli strpení…</string>
@@ -40,7 +40,7 @@
     <string name="moderator">Moderátor</string>
     <string name="participant">Účastník</string>
     <string name="visitor">Návštěvník</string>
-    <string name="remove_contact_text">Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny.</string>
+    <string name="remove_contact_text">Přejete si odstranit %s ze seznamu kontaktů? Předešlé chaty s tímto kontaktem nebudou odstraněny.</string>
     <string name="block_contact_text">Chcete zablokovat příjem zpráv od %s?</string>
     <string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string>
     <string name="block_domain_text">Zablokovat všechny kontakty z %s?</string>
@@ -78,7 +78,7 @@
     <string name="preparing_images">Připravuji odeslání obrázků</string>
     <string name="sharing_files_please_wait">Sdílení souborů. Chvíli strpení…</string>
     <string name="action_clear_history">Smazat historii</string>
-    <string name="clear_conversation_history">Smaže historii konverzací</string>
+    <string name="clear_conversation_history">Smazat historii chatů</string>
     <string name="clear_histor_msg">Opravdu chcete smazat všechny zprávy v této konverzace?
 \n
 \n<b>Varování</b>Toto neovlivní zprávy uložené na jiných zařízeních či serverech.</string>
@@ -174,7 +174,7 @@
     <string name="unpublish_pgp_message">Skutečně chcete odstranit Váš současný veřejný OpenPGP klíč?\nVaše kontakty Vám nebudou moci nadále posílat zprávy šifrované pomocí OpenPGP. </string>
     <string name="openpgp_has_been_published">OpenPGP veřejný klíč zveřejněn.</string>
     <string name="mgmt_account_enable">Povolit účet</string>
-    <string name="mgmt_account_delete_confirm_text">Opravdu chcete svůj účet smazat? Smazáním Vašeho účtu dojde k vymazání celé Vaší historie konverzací</string>
+    <string name="mgmt_account_delete_confirm_text">Opravdu chcete svůj účet smazat? Smazáním Vašeho účtu dojde k vymazání celé Vaší chatové historie</string>
     <string name="attach_record_voice">Nahrát hlas</string>
     <string name="account_settings_jabber_id">Adresa XMPP</string>
     <string name="block_jabber_id">Blokovat XMPP adresu</string>
@@ -272,7 +272,7 @@
     <string name="ignore">Ignorovat</string>
     <string name="without_mutual_presence_updates"><b>Varování:</b> Odeslání bez povolení vzájemného informování o změně stavu může způsobit nečekané potíže.\n\n<small>Jděte do \"Detaily kontaktu\" a ověřte nastavení aktualizace stavu.</small></string>
     <string name="pref_security_settings">Zabezpečení</string>
-    <string name="pref_allow_message_correction">Povolit opravu zpráv</string>
+    <string name="pref_allow_message_correction">Oprava zpráv</string>
     <string name="pref_allow_message_correction_summary">Povolí kontaktům zpětné upravování jejich zpráv</string>
     <string name="pref_expert_options">Expertní nastavení</string>
     <string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string>
@@ -461,8 +461,8 @@
     <string name="pref_dnd_on_silent_mode_summary">Při ztišeném vyzvánění označí váš stav jako \"nedostupný\"</string>
     <string name="pref_treat_vibrate_as_silent">Vibrační mód brát stejně jako tichý</string>
     <string name="pref_treat_vibrate_as_dnd_summary">Při nastavení pouze na vibrace označí váš stav jako \"nedostupný\"</string>
-    <string name="pref_show_connection_options">Rozšířená nastavení připojení</string>
-    <string name="pref_show_connection_options_summary">Zobrazovat nastavení hostname a port při vytváření účtu</string>
+    <string name="pref_show_connection_options">Jméno hostitele a port</string>
+    <string name="pref_show_connection_options_summary">Zobrazit rozšířené možnosti připojení při nastavování účtu</string>
     <string name="hostname_example">xmpp.server.cz</string>
     <string name="action_add_account_with_certificate">Přihlásit se pomocí certifikátu</string>
     <string name="unable_to_parse_certificate">Nelze analyzovat certifikát</string>
@@ -500,10 +500,10 @@
     <string name="no_storage_permission">Povolit %1$s přístup k externímu úložišti</string>
     <string name="no_camera_permission">Povolit %1$s přístup ke kameře</string>
     <string name="sync_with_contacts">Synchronizovat s kontakty</string>
-    <string name="sync_with_contacts_long">%1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.
-\nU kontaktů se pak zobrazí celé jméno a avatar.
+    <string name="sync_with_contacts_long">%1$s zpracovává Váš seznam kontaktů místně na Vašem zařízení za účelem zobrazení jmen a profilových obrázků odpovídajících kontaktů na XMPP.
 \n
-\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server.</string>
+\n
+\nŽádná data kontaktů nikdy neopouštějí Vaše zařízení!</string>
     <string name="notify_on_all_messages">Upozorňovat na všechny zprávy</string>
     <string name="notify_only_when_highlighted">Upozornit pouze, když mě někdo zmíní</string>
     <string name="notify_never">Upozornění vypnuta</string>
@@ -523,7 +523,7 @@
     <string name="this_field_is_required">Toto pole je vyžadováno</string>
     <string name="correct_message">Opravit zprávu</string>
     <string name="send_corrected_message">Odeslat opravenou zprávu</string>
-    <string name="no_keys_just_confirm">Tento osobní otisk byl již bezpečně ověřen. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu.</string>
+    <string name="no_keys_just_confirm">Tento osobní otisk byl již označen jako důvěryhodný. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu.</string>
     <string name="this_account_is_disabled">Tento účet byl vypnut</string>
     <string name="security_error_invalid_file_access">Bezpečnostní chyba: Neplatný přístup k souboru!</string>
     <string name="no_application_to_share_uri">Nebyla nalezena aplikace umožňující sdílení URI</string>
@@ -554,8 +554,8 @@
     <string name="gp_short">Krátký</string>
     <string name="gp_medium">Střední</string>
     <string name="gp_long">Dlouhý</string>
-    <string name="pref_broadcast_last_activity">Informovat o používání</string>
-    <string name="pref_broadcast_last_activity_summary">Tato možnost dává vědět Vašim kontaktům, kdy používáte Conversations</string>
+    <string name="pref_broadcast_last_activity">Naposledy spatřen</string>
+    <string name="pref_broadcast_last_activity_summary">Umožnit svým kontaktům vidět, kdy jste naposledy použili aplikaci</string>
     <string name="pref_privacy">Soukromí</string>
     <string name="pref_theme_options">Vzhled</string>
     <string name="pref_theme_options_summary">Vybrat paletu barev</string>
@@ -601,7 +601,7 @@
     <string name="pref_blind_trust_before_verification">Slepě důvěřovat před ověřením</string>
     <string name="pref_blind_trust_before_verification_summary">Důvěřovat novým zařízením neověřených kontaktů, ale požadovat ruční potvrzení nových zařízení u ověřených kontaktů.</string>
     <string name="not_trusted">Nedůvěryhodný</string>
-    <string name="invalid_barcode">Neplatný 2D kód</string>
+    <string name="invalid_barcode">Neplatný QR kód</string>
     <string name="pref_clean_cache_summary">Vyčistit složku dočasných souborů (užitých aplikací fotoaparátu)</string>
     <string name="pref_clean_cache">Vyčistit dočasné soubory</string>
     <string name="pref_clean_private_storage">Vyčistit soukromé úložiště</string>
@@ -679,8 +679,6 @@
     <string name="message_copied_to_clipboard">Zpráva zkopírována do schránky</string>
     <string name="message">Zpráva</string>
     <string name="private_messages_are_disabled">Soukromé zprávy jsou zakázány</string>
-    <string name="huawei_protected_apps">Chráněné aplikace</string>
-    <string name="huawei_protected_apps_summary">Abyste mohli dostávat upozornění i při vypnuté obrazovce, musíte přidat Conversations mezi chráněné aplikace.</string>
     <string name="mtm_accept_cert">Přijmout neznámý certifikát?</string>
     <string name="mtm_trust_anchor">Certifikát není podepsaný žádnou známou certifikační autoritou.</string>
     <string name="mtm_accept_servername">Přijmout nesouhlasící jméno serveru?</string>
@@ -698,14 +696,14 @@
     <string name="error_trustkey_device_list">Nelze získat seznam zařízení</string>
     <string name="error_trustkey_bundle">Nelze získat šifrovací klíče</string>
     <string name="error_trustkey_hint_mutual">Tip: V některých případech může být řešení vzájemné přidání kontaktů do seznamu kontaktů.</string>
-    <string name="disable_encryption_message">Opravdu chcete vypnout OMEMO šifrování pro tuto konverzaci?
+    <string name="disable_encryption_message">Opravdu chcete vypnout OMEMO šifrování pro tento chat?
 \nTím umožníte správci Vašeho serveru číst Vaše zprávy. Zároveň to však může být jediný způsob, jak komunikovat s kontakty, které používají zastaralé verze klientů.</string>
     <string name="disable_now">Vypnout hned</string>
     <string name="draft">Koncept:</string>
     <string name="pref_omemo_setting">OMEMO šifrování</string>
     <string name="pref_omemo_setting_summary_always">OMEMO bude vždy použito k šifrování zpráv v jednotlivých konverzacích i v soukromých skupinách.</string>
-    <string name="pref_omemo_setting_summary_default_on">OMEMO bude použito jako výchozí pro nové konverzace.</string>
-    <string name="pref_omemo_setting_summary_default_off">OMEMO bude nutné zapnout ručně pro každou novou konverzaci.</string>
+    <string name="pref_omemo_setting_summary_default_on">OMEMO bude použito jako výchozí pro nové chaty.</string>
+    <string name="pref_omemo_setting_summary_default_off">OMEMO bude nutné zapnout ručně pro každý nový chat.</string>
     <string name="create_shortcut">Vytvořit zástupce</string>
     <string name="default_on">Zapnuto jako výchozí</string>
     <string name="default_off">Vypnuto jako výchozí</string>
@@ -724,14 +722,14 @@
     <string name="no_microphone_permission">Povolit %1$s přístup k mikrofonu</string>
     <string name="search_messages">Prohledat zprávy</string>
     <string name="gif">GIF</string>
-    <string name="view_conversation">Zobrazit konverzaci</string>
+    <string name="view_conversation">Zobrazit chat</string>
     <string name="pref_use_share_location_plugin">Plugin pro sdílení pozice</string>
     <string name="pref_use_share_location_plugin_summary">Použít Plugin pro sdílení pozice namísto interní mapy</string>
     <string name="copy_link">Kopírovat webovou adresu</string>
     <string name="copy_jabber_id">Kopírovat XMPP adresu</string>
     <string name="p1_s3_filetransfer">HTTP sdílení souborů pro S3</string>
     <string name="pref_start_search">Přímé vyhledávání</string>
-    <string name="pref_start_search_summary">Na úvodní obrazovce otevřít klávesnici a umístit kurzor do vyhledávacího pole</string>
+    <string name="pref_start_search_summary">Na obrazovce \'Nový chat\' otevřít klávesnici a umístit kurzor do vyhledávacího pole</string>
     <string name="group_chat_avatar">Avatar skupinového chatu</string>
     <string name="host_does_not_support_group_chat_avatars">Hostitel nepodporuje avatary pro skupinový chat</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Pouze vlastník může změnit avatar skupinového chatu</string>
@@ -782,19 +780,19 @@
     <string name="verify_x">Ověřit %s</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[Poslali jsme Vám SMS na <b>%s</b>.]]></string>
     <string name="we_have_sent_you_another_sms">Poslali jsme Vám další SMS se 6místným kódem.</string>
-    <string name="please_enter_pin_below">Prosím, vložte 6místný pin.</string>
+    <string name="please_enter_pin_below">Prosím, vložte 6místný PIN.</string>
     <string name="resend_sms">Poslat SMS znovu</string>
     <string name="resend_sms_in">Poslat SMS znovu (%s)</string>
     <string name="wait_x">Chvíli strpení (%s)</string>
-    <string name="back">zpět</string>
-    <string name="possible_pin">Automaticky vložen pravděpodobný pin ze schránky.</string>
-    <string name="please_enter_pin">Prosím, vložte svůj 6místný pin.</string>
+    <string name="back">Zpět</string>
+    <string name="possible_pin">Automaticky vložen pravděpodobný PIN ze schránky.</string>
+    <string name="please_enter_pin">Prosím, vložte svůj 6místný PIN.</string>
     <string name="abort_registration_procedure">Opravdu si přejete přerušit registraci?</string>
     <string name="yes">Ano</string>
     <string name="no">Ne</string>
     <string name="verifying">Ověřuji…</string>
-    <string name="incorrect_pin">Pin, který jste zadali, je nesprávný.</string>
-    <string name="pin_expired">Pin, který jsme Vám poslali, vypršel.</string>
+    <string name="incorrect_pin">Zadaný PIN je nesprávný.</string>
+    <string name="pin_expired">Platnost PINu, který jsme Vám poslali, vypršela.</string>
     <string name="unknown_api_error_network">Neznámá chyba sítě.</string>
     <string name="unknown_api_error_response">Neznámá odpověď serveru.</string>
     <string name="unable_to_connect_to_server">Nebylo možné se připojit k serveru.</string>
@@ -917,8 +915,8 @@
     <string name="remove_from_favorites">Odepnout shora</string>
     <string name="gpx_track">GPX trasa</string>
     <string name="could_not_correct_message">Nebylo možné opravit zprávu</string>
-    <string name="search_all_conversations">Všechny konverzace</string>
-    <string name="search_this_conversation">Tato konverzace</string>
+    <string name="search_all_conversations">Všechny chaty</string>
+    <string name="search_this_conversation">Tento chat</string>
     <string name="your_avatar">Váš avatar</string>
     <string name="avatar_for_x">Avatar uživatele %s</string>
     <string name="encrypted_with_omemo">Šifrováno pomocí OMEMO</string>
@@ -1001,4 +999,93 @@
     <string name="no_xmpp_adddress_found">Nebyla nalezena žádná XMPP adresa</string>
     <string name="delete_avatar">Smazat avatar</string>
     <string name="account_status_temporary_auth_failure">Dočasné selhání autentizace</string>
+    <string name="audiobook">Audiokniha</string>
+    <string name="account_state_logged_out">Odhlášen</string>
+    <string name="unverified_devices">Používáte neověřená zařízení. Naskenujte QR kód vašich dalších zařízení, abyste provedli ověření a zabránili tak aktivním MITM útokům.</string>
+    <string name="pref_send_crash_reports">Odesílat zprávy o pádu</string>
+    <string name="welcome_header_quicksy">Vítejte v Quicksy!</string>
+    <string name="no_permission_to_place_call">Chybí povolení k telefonnímu hovoru</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">Kód neobsahuje otisky k tomuto chatu.</string>
+    <string name="channel_discover_opt_in_message">Funkce Objevování kanálů využívá službu třetí strany nazvanou &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Použitím této funkce odešlete Vaši IP adresu a hledané fráze do této služby. Pro více informací si přečtěte &lt;a href=https://search.jabber.network/privacy&gt;Zásady ochrany osobních údajů&lt;/a&gt;.</string>
+    <string name="outdated_backup_file_format">Pokoušíte se naimportovat zálohu v zastaralém formátu</string>
+    <string name="pref_up_push_server_title">Push server</string>
+    <string name="log_out">Odhlásit se</string>
+    <string name="no_account_deactivated">Žádný (deaktivováno)</string>
+    <string name="decline">Odmítnout</string>
+    <string name="delete_from_server">Odstranit účet ze serveru</string>
+    <string name="report_spam_and_block">Nahlásit spam a blokovat jeho odesílatele</string>
+    <string name="pref_title_interface">Rozhraní</string>
+    <string name="delete_and_close">Smazat a archivovat chat</string>
+    <string name="start_chat">Zahájit chat</string>
+    <string name="no_certificate_selected">Nebyl vybrán žádný klientský certifikát!</string>
+    <string name="unified_push_summary">Služba oznámení pro další aplikace kompatibilní s UnifiedPush</string>
+    <string name="notifications">Oznámení</string>
+    <string name="pref_attachments_summary">Velikost souborů, komprese obrázků, kvalita videa</string>
+    <string name="pref_notifications_summary">Časová lhůta, zvuk, vibrace, neznámí uživatelé</string>
+    <string name="pref_automatic_download">Automatické stahování</string>
+    <string name="appearance">Vzhled</string>
+    <string name="pref_light_dark_mode">Světlý/tmavý vzhled</string>
+    <string name="pref_allow_screenshots">Povolit snímky obrazovky</string>
+    <string name="pref_allow_screenshots_summary">Zobrazit obsah aplikace v přepínači aplikací a povolit snímky obrazovky</string>
+    <string name="pref_category_e2ee">Koncové šifrování</string>
+    <string name="pref_title_trust_system_ca_store">Certifikační autority</string>
+    <string name="detect_mim">Požadovat channel binding</string>
+    <string name="pref_title_trust_system_ca_store_summary">Důvěřovat systémovým CA certifikátům</string>
+    <string name="pref_privacy_summary">Oznámení o psaní, naposledy spatřen, dostupnost</string>
+    <string name="privacy_policy">Zásady ochrany osobních údajů</string>
+    <string name="contact_list_integration_not_available">Propojení se seznamem kontaktů není k dispozici</string>
+    <string name="quicksy_wants_your_consent">Quicksy žádá o souhlas s použitím Vašich dat</string>
+    <string name="remove_bookmark_and_close">Přejete si odstranit záložku pro %s a archivovat chat?</string>
+    <string name="pref_autojoin_summary">Nastavit příznak \"autojoin\" při vstupu či opuštění skupiny a reagovat na změny provedené jinými klienty.</string>
+    <string name="this_account_is_logged_out">Odhlásili jste se z tohoto účtu</string>
+    <string name="reconnect_on_other_host">Znovu připojit k jinému hostiteli</string>
+    <string name="requesting_sms">Žádost o SMS…</string>
+    <string name="restore_warning_continued">Neobnovujte zálohy, které jste sami nevytvořili!</string>
+    <string name="switch_to_chat">Přepnout na chat</string>
+    <string name="audio_video_disabled_tor">Hovory nejsou dostupné při použití Toru</string>
+    <string name="unified_push_distributor">Distributor UnifiedPush</string>
+    <string name="pref_up_push_account_title">XMPP účet</string>
+    <string name="pref_up_push_account_summary">Účet, který bude použit pro přijímání zpráv push.</string>
+    <string name="hide_notification">Skrýt oznámení</string>
+    <string name="report_spam">Nahlásit spam</string>
+    <string name="pref_title_security">Zabezpečení</string>
+    <string name="pref_summary_security">E2E šifrování, slepá důvěra před ověřením, detekce MITM</string>
+    <string name="detect_mim_summary">Channel binding může detekovat některé útoky typu machine-in-the-middle</string>
+    <string name="pref_summary_appearance">Téma, barvy, snímky obrazovky, vstup</string>
+    <string name="title_activity_share_with">Sdílet s…</string>
+    <string name="pref_use_colorful_bubbles">Barevné bubliny chatu</string>
+    <string name="pref_use_colorful_bubbles_summary">Odlišit barvy odeslaných a přijatých zpráv</string>
+    <string name="pref_dynamic_colors">Dynamické barvy</string>
+    <string name="pref_dynamic_colors_summary">Systémové barvy (Material You)</string>
+    <string name="rtp_state_content_add">Přidat další stopy?</string>
+    <string name="remove_bookmark">Přejete si odstranit záložku pro %s?</string>
+    <string name="could_not_delete_account_from_server">Nebylo možné odstranit účet ze serveru</string>
+    <string name="log_in">Přihlásit se</string>
+    <string name="contact_uses_unverified_keys">Váš kontakt používá neověřená zařízení. Naskenujte QR kód kontaktu, abyste provedli ověření a zabránili tak aktivním MITM útokům.</string>
+    <string name="pref_keyboard_options">Klávesnice</string>
+    <string name="action_archive_chat">Archivovat chat</string>
+    <string name="title_activity_new_chat">Nový chat</string>
+    <string name="archive_this_chat">Poté smazat chat</string>
+    <string name="send_encrypted_message">Odeslat šifrovanou zprávu</string>
+    <string name="corresponding_chats_closed">Příslušné chaty archivovány.</string>
+    <string name="rtp_state_contact_offline">Kontakt není dostupný</string>
+    <string name="title_undo_swipe_out_chat">Chat archivován</string>
+    <string name="invalid_user_input">Neplatný uživatelský vstup</string>
+    <string name="call_integration_not_available">Integrace hovorů není k dispozici!</string>
+    <string name="pref_connection_summary_w_cd">Název hostitele a port, Tor, objevování kanálů</string>
+    <string name="pref_connection_summary">Název hostitele a port, Tor</string>
+    <string name="pref_backup_summary">Vytvořit jednou, naplánovat další</string>
+    <string name="pref_up_long_summary">Pokud je funkce distribuce UnifiedPush povolena, spolehlivé a na baterii nenáročné XMPP spojení bude využíváno k probouzení jiných aplikací kompatibilních s UnifiedPush, jako jsou např. Tusky, Ltt.rs, FluffyChat a další.</string>
+    <string name="pref_category_application">Aplikace</string>
+    <string name="pref_category_interaction">Interakce</string>
+    <string name="pref_category_on_this_device">Na tomto zařízení</string>
+    <string name="pref_create_backup_one_off_summary">Vytvořit jednorázovou zálohu</string>
+    <string name="pref_backup_recurring">Opakovaná záloha</string>
+    <string name="pref_fullscreen_notification">Oznámení na celou obrazovku</string>
+    <string name="unsupported_operation">Nepodporovaná operace</string>
+    <string name="pref_fullscreen_notification_summary">Povolit této aplikaci zobrazit oznámení o příchozích hovorech přes celou obrazovku, když je zařízení uzamčeno.</string>
+    <string name="pref_accept_invites_from_strangers">Pozvánky od neznámých</string>
+    <string name="pref_accept_invites_from_strangers_summary">Přijímat pozvánky do skupinových chatů od neznámých kontaktů</string>
+    <string name="pref_large_font">Velké písmo</string>
+    <string name="pref_large_font_summary">Zvětšit písmo v chatových bublinách</string>
 </resources>

src/main/res/values-da-rDK/strings.xml 🔗

@@ -673,8 +673,6 @@
     <string name="message_copied_to_clipboard">Besked kopieret til udklipsholder</string>
     <string name="message">Besked</string>
     <string name="private_messages_are_disabled">Private beskeder er deaktiveret</string>
-    <string name="huawei_protected_apps">Beskyttet apps</string>
-    <string name="huawei_protected_apps_summary">For at modtage underretninger, selv når skærmen er slukket, skal du tilføje Conversations til listen over beskyttede apps.</string>
     <string name="mtm_accept_cert">Accepter ukendt certifikat?</string>
     <string name="mtm_trust_anchor">Serverens certifikat er ikke underskrevet af en kendt Certifikat Autoritet.</string>
     <string name="mtm_accept_servername">Accepter fejlbehæftet servernavn?</string>

src/main/res/values-de/strings.xml 🔗

@@ -674,8 +674,6 @@
     <string name="message_copied_to_clipboard">Nachricht in die Zwischenablage kopiert</string>
     <string name="message">Nachricht</string>
     <string name="private_messages_are_disabled">Private Nachrichten sind deaktiviert</string>
-    <string name="huawei_protected_apps">Geschützte Apps</string>
-    <string name="huawei_protected_apps_summary">Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Conversations zur Liste der geschützten Apps hinzufügen.</string>
     <string name="mtm_accept_cert">Unbekanntes Zertifikat akzeptieren?</string>
     <string name="mtm_trust_anchor">Das Serverzertifikat wurde nicht von einer bekannten Zertifizierungsstelle signiert.</string>
     <string name="mtm_accept_servername">Nicht übereinstimmenden Servernamen akzeptieren?</string>
@@ -1071,4 +1069,24 @@
     <string name="unsupported_operation">Nicht unterstützte Operation</string>
     <string name="pref_fullscreen_notification_summary">Erlaube dieser App, Benachrichtigungen über eingehende Anrufe anzuzeigen, die den gesamten Bildschirm einnehmen, wenn das Gerät gesperrt ist.</string>
     <string name="pref_create_backup_one_off_summary">Einmalige Sicherung erstellen</string>
+    <string name="allow_private_messages">Private Nachrichten erlauben</string>
+    <string name="could_not_disable_video">Video konnte nicht deaktiviert werden.</string>
+    <string name="delete_pgp_key">OpenPGP-Schlüssel löschen</string>
+    <string name="change_notification_settings">Benachrichtigungseinstellungen ändern</string>
+    <string name="call_is_using_wired_headset">Anruf erfolgt über ein kabelgebundenes Headset.</string>
+    <string name="call_is_using_bluetooth">Anruf erfolgt über Bluetooth.</string>
+    <string name="flip_camera">Kamera wechseln</string>
+    <string name="video_is_enabled_tap_to_disable">Video ist aktiviert. Zum Deaktivieren antippen.</string>
+    <string name="edit_nick">Nickname bearbeiten</string>
+    <string name="call_is_using_earpiece">Anruf erfolgt über den oberen Lautsprecher.</string>
+    <string name="call_is_using_speaker">Aufruf erfolgt über den unteren Lautsprecher.</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Dein Profilbild. Antippen, um ein neues Profilbild aus der Galerie auszuwählen.</string>
+    <string name="video_is_disabled_tap_to_enable">Video ist deaktiviert. Zum Aktivieren antippen.</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Anruf erfolgt über den oberen Lautsprecher. Antippen, um zum unteren Lautsprecher zu wechseln.</string>
+    <string name="edit_configuration">Konfiguration ändern</string>
+    <string name="edit_name_and_topic">Name und Thema bearbeiten</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Anruf erfolgt über den unteren Lautsprecher. Antippen, um zum oberen Lautsprecher zu wechseln.</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
+    <string name="server_info_sasl2">XEP-0388: Extensible SASL Profile</string>
+    <string name="server_info_login_mechanism">Anmeldeverfahren</string>
 </resources>

src/main/res/values-el/strings.xml 🔗

@@ -664,8 +664,6 @@
     <string name="message_copied_to_clipboard">Το μήνυμα αντιγράφηκε στο πρόχειρο</string>
     <string name="message">Μήνυμα</string>
     <string name="private_messages_are_disabled">Τα ιδιωτικά μηνύματα είναι απενεργοποιημένα</string>
-    <string name="huawei_protected_apps">Προστατευμένες εφαρμογές</string>
-    <string name="huawei_protected_apps_summary">Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Conversations στον κατάλογο με τις προστατευμένες εφαρμογές.</string>
     <string name="mtm_accept_cert">Αποδοχή άγνωστου πιστοποιητικού;</string>
     <string name="mtm_trust_anchor">Το πιστοποιητικό του διακομιστή δεν είναι υπογεγραμμένο από κάποια γνωστή Αρχή Πιστοποίησης.</string>
     <string name="mtm_accept_servername">Αποδοχή αναντίστοιχου ονόματος διακομιστή;</string>

src/main/res/values-es/strings.xml 🔗

@@ -683,8 +683,6 @@
     <string name="message_copied_to_clipboard">Mensaje copiado en el portapapeles</string>
     <string name="message">Mensaje</string>
     <string name="private_messages_are_disabled">Los mensajes privados están deshabilitados</string>
-    <string name="huawei_protected_apps">Aplicaciones protegidas</string>
-    <string name="huawei_protected_apps_summary">Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Conversations a la lista de aplicaciones protegidas.</string>
     <string name="mtm_accept_cert">¿Aceptar certificado desconocido?</string>
     <string name="mtm_trust_anchor">El certificado del servidor no está firmado por una Autoridad Certificadora conocida.</string>
     <string name="mtm_accept_servername">¿Aceptar nombre del servidor no coincidente?</string>
@@ -1030,7 +1028,7 @@
     <string name="title_activity_share_with">Compartir con…</string>
     <string name="action_archive_chat">Archivar chat</string>
     <string name="title_activity_new_chat">Chat nuevo</string>
-    <string name="archive_this_chat">Guardar este chat</string>
+    <string name="archive_this_chat">Eliminar este chat después</string>
     <string name="title_undo_swipe_out_chat">Chat guardado</string>
     <string name="welcome_header">Unirse a Conversation</string>
     <string name="pref_use_colorful_bubbles">Burbujas de chat de colores</string>
@@ -1085,4 +1083,5 @@
     <string name="unsupported_operation">Operación no soportada</string>
     <string name="pref_fullscreen_notification">Notificaciones a pantalla completa</string>
     <string name="pref_fullscreen_notification_summary">Permite que esta aplicación muestre notificaciones de llamadas entrantes que ocupan toda la pantalla cuando el dispositivo está bloqueado.</string>
+    <string name="allow_private_messages">Permitir mensajes privados</string>
 </resources>

src/main/res/values-eu/strings.xml 🔗

@@ -541,8 +541,6 @@
     <string name="message_copied_to_clipboard">Mezua arbelera kopiatu da</string>
     <string name="message">Mezua</string>
     <string name="private_messages_are_disabled">Mezu pribatuak ezgaituta daude</string>
-    <string name="huawei_protected_apps">Babestutako aplikazioak</string>
-    <string name="huawei_protected_apps_summary">Jakinarazpenak jasotzen jarraitu nahi naduzu, baita pantaila itzalita dagoenean ere, Conversations babestutako aplikazioen zerrendan gehitu behar duzu.</string>
     <string name="mtm_accept_cert">Ziurtagiri ezezaguna onartu?</string>
     <string name="mtm_trust_anchor">Zerbitzariaren ziurtagiria ez dago ezaguna den Ziurtagiri jaulkitzaile batez sinatuta.</string>
     <string name="mtm_accept_servername">Bat ez datorren zerbitzari izena onartu?</string>

src/main/res/values-fa-rIR/strings.xml 🔗

@@ -672,14 +672,12 @@
     <string name="invalid_barcode">بارکد دوبعدی نامعتبر</string>
     <string name="allow_participants_to_invite_others">بگذارید همه دیگران را دعوت کنند</string>
     <string name="install_orbot">نصب Orbot</string>
-    <string name="huawei_protected_apps">برنامه‌های محافظت‌شده</string>
     <string name="pref_clean_cache">کش را خالی کن</string>
     <string name="copy_fingerprint">کپی اثر انگشت دیجیتال</string>
     <string name="pref_headsup_notifications">اعلان‌های شناور</string>
     <string name="choose_account">انتخاب حساب</string>
     <string name="no_users_hint_group_chat">این گفتگوی گروهی خصوصی هیچ عضوی ندارد.</string>
     <string name="restore_backup">بازیابی پشتیبان</string>
-    <string name="huawei_protected_apps_summary">برای دریافت اعلان‌ها حتی وقتی که صفحه خاموش است باید این برنامه را به فهرست برنامه‌های محافظت‌شده بیفزایید.</string>
     <string name="use_camera_icon_to_scan_barcode">بارکد را به کمک دوربین اسکن کنید</string>
     <string name="no_name_set_instructions">برای تنظیم نام دکمهٔ ویرایش را به‌کار ببرید.</string>
     <string name="pref_omemo_setting_summary_always">همیشه برای گفتگوهای تکی و گروهای خصوصی OMEMO به کار برود.</string>

src/main/res/values-fi/strings.xml 🔗

@@ -638,8 +638,6 @@
     <string name="message_copied_to_clipboard">Viesti kopioitu leikepöydälle</string>
     <string name="message">Viesti</string>
     <string name="private_messages_are_disabled">Yksityisviestit on poistettu käytöstä</string>
-    <string name="huawei_protected_apps">Suojatut sovellukset</string>
-    <string name="huawei_protected_apps_summary">Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Conversations pitää lisätä suojattujen sovellusten luetteloon.</string>
     <string name="mtm_accept_cert">Hyväksytäänkö tuntematon varmenne?</string>
     <string name="mtm_trust_anchor">Palvelimen varmenne ei ole luotetun myöntäjän allekirjoittama.</string>
     <string name="mtm_accept_servername">Hyväksytäänkö eriävä palvelimen nimi?</string>

src/main/res/values-fr/strings.xml 🔗

@@ -5,7 +5,7 @@
     <string name="action_account">Gérer le compte</string>
     <string name="action_contact_details">Détails du contact</string>
     <string name="action_muc_details">Détails du groupe</string>
-    <string name="channel_details">Détails du canal</string>
+    <string name="channel_details">Détails du salon</string>
     <string name="action_add_account">Ajouter un compte</string>
     <string name="action_edit_contact">Modifier le nom</string>
     <string name="action_add_phone_book">Ajouter au carnet d\'adresses</string>
@@ -14,14 +14,14 @@
     <string name="action_unblock_contact">Débloquer le contact</string>
     <string name="action_block_domain">Bloquer le domaine</string>
     <string name="action_unblock_domain">Débloquer le domaine</string>
-    <string name="action_block_participant">Bloquer le participant</string>
-    <string name="action_unblock_participant">Débloquer le participant</string>
+    <string name="action_block_participant">Bloquer le·a participant·e</string>
+    <string name="action_unblock_participant">Débloquer le·a participant·e</string>
     <string name="title_activity_manage_accounts">Gestion des comptes</string>
     <string name="title_activity_settings">Paramètres</string>
     <string name="title_activity_choose_contact">Choisir un contact</string>
     <string name="title_activity_choose_contacts">Choisir les contacts</string>
     <string name="title_activity_share_via_account">Partager via le compte</string>
-    <string name="title_activity_block_list">Bloquer la liste</string>
+    <string name="title_activity_block_list">Comptes bloqués</string>
     <string name="just_now">À l\'instant</string>
     <string name="minute_ago">Il y a 1 minute</string>
     <string name="minutes_ago">Il y a %d minutes</string>
@@ -35,11 +35,11 @@
     <string name="pgp_message">Message chiffré avec OpenPGP</string>
     <string name="nick_in_use">Cet identifiant est déjà utilisé</string>
     <string name="invalid_muc_nick">Identifiant non valide</string>
-    <string name="admin">Administrateur</string>
+    <string name="admin">Administrateur·ice</string>
     <string name="owner">Propriétaire</string>
-    <string name="moderator">Modérateur</string>
-    <string name="participant">Participant</string>
-    <string name="visitor">Visiteur</string>
+    <string name="moderator">Modérateur·ice</string>
+    <string name="participant">Participant·e</string>
+    <string name="visitor">Visiteur·ice</string>
     <string name="remove_contact_text">Voulez-vous supprimer %s de votre liste de contacts ? Les conversations associées à ce contact ne seront pas supprimées.</string>
     <string name="block_contact_text">Voulez-vous bloquer %s pour l\'empêcher de vous envoyer des messages ?</string>
     <string name="unblock_contact_text">Voulez-vous débloquer %s et lui permettre de vous envoyer des messages ?</string>
@@ -95,15 +95,15 @@
     <string name="send_unencrypted">Envoyer en clair</string>
     <string name="decryption_failed">Échec du déchiffrement. Avez-vous la bonne clé privée ?</string>
     <string name="openkeychain_required">OpenKeychain</string>
-    <string name="openkeychain_required_long"><![CDATA[%1$s utilise <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n<small>(Veuillez redémarrer %1$s après l\'installation de l\'application.)</small>]]></string>
+    <string name="openkeychain_required_long">%1$s utilise &lt;b&gt;OpenKeychain&lt;/b&gt; pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.&lt;br&gt;&lt;br&gt;OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.&lt;br&gt;&lt;br&gt;&lt;small&gt;(Veuillez redémarrer %1$s après l\'installation de l\'application.)&lt;/small&gt;</string>
     <string name="restart">Redémarrer</string>
     <string name="install">Installer</string>
     <string name="openkeychain_not_installed">Veuillez installer OpenKeychain</string>
     <string name="offering">Proposition…</string>
     <string name="waiting">Patientez…</string>
-    <string name="no_pgp_key">Aucune clé OpenPGP trouvée.</string>
+    <string name="no_pgp_key">Aucune clé OpenPGP trouvée</string>
     <string name="contact_has_no_pgp_key">Impossible de chiffrer vos messages car votre contact n\'a pas communiqué sa clé publique.\n\n<small>Demandez-lui de configurer OpenPGP.</small></string>
-    <string name="no_pgp_keys">Aucune clé OpenPGP n\'a été trouvée.</string>
+    <string name="no_pgp_keys">Aucune clé OpenPGP n\'a été trouvée</string>
     <string name="contacts_have_no_pgp_keys">Impossible de chiffrer votre message car vos contacts ne communiquent pas leur clé publique.\n\n<small>Demandez-leur de configurer OpenPGP.</small></string>
     <string name="pref_general">Général</string>
     <string name="pref_accept_files">Accepter les fichiers</string>
@@ -137,7 +137,7 @@
     <string name="ask_for_presence_updates">Demander les màj de disponibilité</string>
     <string name="attach_choose_picture">Choisir une image</string>
     <string name="attach_take_picture">Prendre une photo</string>
-    <string name="preemptively_grant">Accepter par avance les demandes de publication.</string>
+    <string name="preemptively_grant">Accepter par avance les demandes de publication</string>
     <string name="error_not_an_image_file">Le fichier choisi n\'est pas une image</string>
     <string name="error_compressing_image">Impossible de convertir l\'image</string>
     <string name="error_file_not_found">Impossible de trouver le fichier</string>
@@ -171,7 +171,7 @@
     <string name="encryption_choice_omemo">OMEMO</string>
     <string name="mgmt_account_delete">Supprimer</string>
     <string name="mgmt_account_disable">Désactiver temporairement</string>
-    <string name="mgmt_account_publish_avatar">Publier un avatar</string>
+    <string name="mgmt_account_publish_avatar">Publier une image de profil</string>
     <string name="mgmt_account_publish_pgp">Publier la clé publique OpenPGP</string>
     <string name="unpublish_pgp">Supprimer la clé publique OpenPGP</string>
     <string name="unpublish_pgp_message">Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP.</string>
@@ -181,10 +181,10 @@
     <string name="attach_record_voice">Enregistrer un son</string>
     <string name="account_settings_jabber_id">Adresse XMPP</string>
     <string name="block_jabber_id">Bloquer l\'adresse XMPP</string>
-    <string name="account_settings_example_jabber_id">nom@exemple.com</string>
+    <string name="account_settings_example_jabber_id">nom@example.com</string>
     <string name="password">Mot de passe</string>
     <string name="invalid_jid">Ce n\'est pas une adresse XMPP valide</string>
-    <string name="error_out_of_memory">Plus de mémoire disponible. L\'image est trop volumineuse.</string>
+    <string name="error_out_of_memory">Plus de mémoire disponible. L\'image est trop volumineuse</string>
     <string name="add_phone_book_text">Voulez-vous ajouter %s à votre carnet d\'adresses ?</string>
     <string name="server_info_show_more">Infos sur le serveur</string>
     <string name="server_info_mam">XEP-0313 : MAM</string>
@@ -208,7 +208,7 @@
     <string name="last_seen_day">en ligne hier</string>
     <string name="last_seen_days">en ligne il y a %d jours</string>
     <string name="install_openkeychain">Message chiffré. Veuillez installer OpenKeychain pour le déchiffrer.</string>
-    <string name="openpgp_messages_found">Nouveaux messages chiffrés avec OpenPGP détectés.</string>
+    <string name="openpgp_messages_found">Nouveaux messages chiffrés avec OpenPGP détectés</string>
     <string name="openpgp_key_id">ID de clé OpenPGP</string>
     <string name="omemo_fingerprint">Empreinte OMEMO</string>
     <string name="omemo_fingerprint_x509">v\\Empreinte OMEMO</string>
@@ -228,18 +228,18 @@
     <string name="select">Sélectionner</string>
     <string name="contact_already_exists">Le contact existe déjà</string>
     <string name="join">Rejoindre</string>
-    <string name="channel_full_jid_example">canal@conference.example.com/surnom</string>
-    <string name="channel_bare_jid_example">canal@conference.example.com</string>
+    <string name="channel_full_jid_example">salon@conference.example.com/surnom</string>
+    <string name="channel_bare_jid_example">salon@conference.example.com</string>
     <string name="save_as_bookmark">Enregistrer comme favori</string>
     <string name="delete_bookmark">Supprimer le favori</string>
     <string name="destroy_room">Détruire le groupe</string>
-    <string name="destroy_channel">Détruire le canal</string>
+    <string name="destroy_channel">Détruire le salon</string>
     <string name="destroy_room_dialog">Voulez-vous vraiment détruire ce groupe ?\n\n<b>Avertissement :</b> le groupe sera complètement supprimé du serveur.</string>
-    <string name="destroy_channel_dialog">Êtes-vous sûr de vouloir détruire ce canal public \?
-\n
-\n<b>Attention :</b> Le canal sera totalement supprimé du serveur.</string>
+    <string name="destroy_channel_dialog">Êtes-vous sûr de vouloir détruire ce salon public ? 
+\n 
+\n<b>Attention :</b> Le salon sera totalement supprimé du serveur.</string>
     <string name="could_not_destroy_room">Impossible de détruire le groupe</string>
-    <string name="could_not_destroy_channel">Impossible de détruire le canal</string>
+    <string name="could_not_destroy_channel">Impossible de détruire le salon</string>
     <string name="action_edit_subject">Modifier le sujet du groupe</string>
     <string name="topic">Sujet</string>
     <string name="joining_conference">Rejoindre le groupe…</string>
@@ -251,13 +251,13 @@
     <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s+%2$d autres ont tout lu jusqu\'ici</string>
     <string name="everyone_has_read_up_to_this_point">Tout le monde a lu jusqu\'ici</string>
     <string name="publish">Publier</string>
-    <string name="touch_to_choose_picture">Appuyer sur l\'avatar pour choisir une image depuis la galerie</string>
+    <string name="touch_to_choose_picture">Appuyer sur l\'image de profil pour choisir une image depuis la galerie</string>
     <string name="publishing">Mise à jour…</string>
     <string name="error_publish_avatar_server_reject">Le serveur a rejeté votre publication</string>
     <string name="error_publish_avatar_converting">Impossible de convertir votre image</string>
-    <string name="error_saving_avatar">Impossible de stocker l\'avatar sur le disque</string>
+    <string name="error_saving_avatar">Impossible de stocker l\'image de profil sur le disque</string>
     <string name="or_long_press_for_default">(Un appui long réinitialise le paramètre)</string>
-    <string name="error_publish_avatar_no_server_support">Votre serveur ne supporte pas la publication d\'avatars</string>
+    <string name="error_publish_avatar_no_server_support">Votre serveur ne supporte pas la publication d\'image de profil</string>
     <string name="private_message">chuchoté</string>
     <string name="private_message_to">pour %s</string>
     <string name="send_private_message_to">Envoyer un message privé à %s</string>
@@ -280,13 +280,13 @@
     <string name="pref_allow_message_correction">Correction des messages</string>
     <string name="pref_allow_message_correction_summary">Permet à vos contacts d\'éditer leurs messages rétroactivement</string>
     <string name="pref_expert_options">Paramètres avancés</string>
-    <string name="pref_expert_options_summary">À utiliser avec précaution.</string>
+    <string name="pref_expert_options_summary">À utiliser avec précaution</string>
     <string name="title_activity_about_x">À propos de %s</string>
     <string name="title_pref_quiet_hours">Heures tranquilles</string>
     <string name="title_pref_quiet_hours_start_time">Heure de début</string>
     <string name="title_pref_quiet_hours_end_time">Heure de fin</string>
     <string name="title_pref_enable_quiet_hours">Activer les heures tranquilles</string>
-    <string name="pref_quiet_hours_summary">Les notifications seront muettes pendant les heures tranquilles.</string>
+    <string name="pref_quiet_hours_summary">Les notifications seront muettes pendant les heures tranquilles</string>
     <string name="pref_expert_options_other">Autres</string>
     <string name="toast_message_omemo_fingerprint">Empreinte OMEMO copiée dans le presse-papier</string>
     <string name="conference_banned">Vous êtes bannis de ce groupe</string>
@@ -298,7 +298,7 @@
     <string name="using_account">avec le compte %s</string>
     <string name="hosted_on">hébergé sur %s</string>
     <string name="checking_x">Vérification de %s sur l\'hôte HTTP</string>
-    <string name="not_connected_try_again">Vous n\'êtes pas connecté. Essayez plus tard.</string>
+    <string name="not_connected_try_again">Vous n\'êtes pas connecté. Essayez plus tard</string>
     <string name="check_x_filesize">Vérification de la taille de %s</string>
     <string name="check_x_filesize_on_host">Vérification de la taille de %1$s sur %2$s</string>
     <string name="message_options">Options du message</string>
@@ -310,15 +310,15 @@
     <string name="url_copied_to_clipboard">URL copiée dans le presse-papier</string>
     <string name="jabber_id_copied_to_clipboard">Adresse XMPP copiée dans le presse-papiers</string>
     <string name="error_message_copied_to_clipboard">Message d\'erreur copié dans le presse-papier</string>
-    <string name="web_address">adresse internet</string>
+    <string name="web_address">adresse web</string>
     <string name="scan_qr_code">Scanner le QR code</string>
     <string name="show_qr_code">Montrer le QR code</string>
-    <string name="show_block_list">Afficher la liste des contacts bloqués</string>
+    <string name="show_block_list">Afficher la liste des comptes bloqués</string>
     <string name="account_details">Détails du compte</string>
     <string name="confirm">Confirmer</string>
     <string name="try_again">Réessayer</string>
     <string name="pref_keep_foreground_service">Service au premier plan</string>
-    <string name="pref_keep_foreground_service_summary">Évite que le système ne ferme votre connexion.</string>
+    <string name="pref_keep_foreground_service_summary">Évite que le système ne ferme votre connexion</string>
     <string name="pref_create_backup">Créer une sauvegarde</string>
     <string name="pref_create_backup_summary">La sauvegarde sera stockée dans %s</string>
     <string name="notification_create_backup_title">Création des fichiers de sauvegarde</string>
@@ -344,13 +344,13 @@
     <string name="no_application_found_to_open_link">Aucune application disponible pour ouvrir le lien</string>
     <string name="no_application_found_to_view_contact">Aucune application disponible pour afficher le contact</string>
     <string name="pref_show_dynamic_tags">Tags dynamiques</string>
-    <string name="pref_show_dynamic_tags_summary">Afficher des tags en lecture seule en dessous des contacts.</string>
+    <string name="pref_show_dynamic_tags_summary">Afficher des tags en lecture seule en dessous des contacts</string>
     <string name="enable_notifications">Activer les notifications</string>
     <string name="no_conference_server_found">Serveur de groupe non trouvé</string>
     <string name="conference_creation_failed">Impossible de créer le groupe</string>
-    <string name="account_image_description">Avatar du compte</string>
+    <string name="account_image_description">Image de profil du compte</string>
     <string name="copy_omemo_clipboard_description">Copier l\'empreinte OMEMO dans le presse-papier</string>
-    <string name="regenerate_omemo_key">Régénérer l\'empreinte OMEMO</string>
+    <string name="regenerate_omemo_key">Régénérer la clé OMEMO</string>
     <string name="clear_other_devices">Supprimer les appareils</string>
     <string name="clear_other_devices_desc">Êtes-vous sûr de vouloir supprimer les autres appareils de l\'annonce OMEMO ? Ils s\'annonceront de nouveau à leur prochaine connexion, mais ils peuvent ne pas recevoir les messages envoyés entre temps.</string>
     <string name="error_no_keys_to_trust_server_error">Aucune clé utilisable n\'est disponible pour ce contact. \nImpossible d\'obtenir de nouvelles clés depuis le serveur. Pourrait-il y avoir un problème avec le serveur de votre contact ?</string>
@@ -372,26 +372,26 @@
     <string name="no_role">Hors ligne</string>
     <string name="outcast">Banni</string>
     <string name="member">Membre</string>
-    <string name="advanced_mode">Mode expert</string>
+    <string name="advanced_mode">Mode avancé</string>
     <string name="grant_membership">Accorder des privilèges aux membres</string>
     <string name="remove_membership">Révoquer les privilèges des membres</string>
-    <string name="grant_admin_privileges">Accorder des privilèges d\'administrateur</string>
-    <string name="remove_admin_privileges">Révoquer des privilèges d\'administrateur</string>
+    <string name="grant_admin_privileges">Accorder des privilèges d\'administrateur·ice</string>
+    <string name="remove_admin_privileges">Révoquer des privilèges d\'administrateur·ice</string>
     <string name="grant_owner_privileges">Accorder des privilèges de propriétaire</string>
     <string name="remove_owner_privileges">Révoquer les privilèges du propriétaire</string>
     <string name="remove_from_room">Supprimer du groupe</string>
-    <string name="remove_from_channel">Supprimer du canal</string>
+    <string name="remove_from_channel">Supprimer du salon</string>
     <string name="could_not_change_affiliation">Impossible de changer l\'affiliation de %s</string>
     <string name="ban_from_conference">Bannir du groupe</string>
-    <string name="ban_from_channel">Bannir du canal</string>
-    <string name="removing_from_public_conference">Vous essayez de supprimer %s d\'un canal public. La seule façon de le faire est de bannir cet utilisateur pour toujours.</string>
+    <string name="ban_from_channel">Bannir du salon</string>
+    <string name="removing_from_public_conference">Vous essayez de supprimer %s d\'un salon public. La seule façon de le faire est de bannir cet·te utilisateur·ice pour toujours.</string>
     <string name="ban_now">Bannir maintenant</string>
     <string name="could_not_change_role">Impossible de changer le rôle de %s</string>
-    <string name="conference_options">Configuration du groupe</string>
-    <string name="channel_options">Configuration du canal public</string>
+    <string name="conference_options">Configuration du groupe privé</string>
+    <string name="channel_options">Configuration du salon public</string>
     <string name="members_only">Privé, membres uniquement</string>
     <string name="non_anonymous">Rendre les adresses XMPP visibles à tout le monde</string>
-    <string name="moderated">Rendre le canal modéré</string>
+    <string name="moderated">Rendre le salon modéré</string>
     <string name="you_are_not_participating">Vous ne participez pas</string>
     <string name="modified_conference_options">Options du groupe modifiées !</string>
     <string name="could_not_modify_conference_options">Impossible de modifier les options du groupe</string>
@@ -404,14 +404,14 @@
     <string name="pref_enter_is_send">Touche Entrée pour envoyer</string>
     <string name="pref_enter_is_send_summary">Utilisez la touche Entrée pour envoyer un message. Vous pourrez toujours utiliser la combinaison Ctrl+Entrée pour envoyer un message, même si cette option est désactivée.</string>
     <string name="pref_display_enter_key">Afficher la touche Entrée</string>
-    <string name="pref_display_enter_key_summary">Remplacer la touche Émoticônes par la touche Entrée.</string>
+    <string name="pref_display_enter_key_summary">Remplacer la touche Émoticônes par la touche Entrée</string>
     <string name="audio">audio</string>
     <string name="video">vidéo</string>
     <string name="image">image</string>
     <string name="pdf_document">document PDF</string>
     <string name="apk">Application Android</string>
     <string name="vcard">Contact</string>
-    <string name="avatar_has_been_published">L\'avatar a été publié !</string>
+    <string name="avatar_has_been_published">L\'image de profil a été publiée !</string>
     <string name="sending_x_file">%s en cours d\'envoi</string>
     <string name="offering_x_file">En train de proposer un(e) %s</string>
     <string name="hide_offline">Cacher contacts hors-ligne</string>
@@ -426,11 +426,11 @@
     <string name="no_application_found_to_display_location">Aucune application trouvée pour afficher le lieu</string>
     <string name="location">Position</string>
     <string name="title_undo_swipe_out_group_chat">Quitter le groupe privé</string>
-    <string name="title_undo_swipe_out_channel">Quitte le canal public</string>
+    <string name="title_undo_swipe_out_channel">Quitte le salon public</string>
     <string name="pref_dont_trust_system_cas_title">Ne pas utiliser les CAs système</string>
-    <string name="pref_dont_trust_system_cas_summary">Tous les certificats doivent être approuvés manuellement.</string>
+    <string name="pref_dont_trust_system_cas_summary">Tous les certificats doivent être approuvés manuellement</string>
     <string name="pref_remove_trusted_certificates_title">Retirer les certificats</string>
-    <string name="pref_remove_trusted_certificates_summary">Supprimer les certificats approuvés manuellement.</string>
+    <string name="pref_remove_trusted_certificates_summary">Supprimer les certificats approuvés manuellement</string>
     <string name="toast_no_trusted_certs">Aucun certificat approuvé manuellement</string>
     <string name="dialog_manage_certs_title">Retirer les certificats</string>
     <string name="dialog_manage_certs_positivebutton">Supprimer la sélection</string>
@@ -485,7 +485,7 @@
     <string name="device_does_not_support_certificates">Votre appareil ne supporte pas la sélection de certificats client !</string>
     <string name="pref_connection_options">Connexion</string>
     <string name="pref_use_tor">Connexion via Tor</string>
-    <string name="pref_use_tor_summary">Rediriger toutes les connexions vers le réseau Tor. Nécessite Orbot.</string>
+    <string name="pref_use_tor_summary">Rediriger toutes les connexions vers le réseau Tor. Nécessite Orbot</string>
     <string name="account_settings_hostname">Nom d\'hôte</string>
     <string name="account_settings_port">Port</string>
     <string name="hostname_or_onion">Adresse du serveur (ou .onion)</string>
@@ -524,18 +524,18 @@
     <string name="this_field_is_required">Ce champ est requis</string>
     <string name="correct_message">Corriger le message</string>
     <string name="send_corrected_message">Envoyer le message corrigé</string>
-    <string name="no_keys_just_confirm">Vous avez déjà fait confiance à l\'empreinte de cette personne pour accorder votre confiance. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe.</string>
+    <string name="no_keys_just_confirm">Vous avez déjà fait confiance à l\'empreinte de cette personne. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe.</string>
     <string name="this_account_is_disabled">Vous avez désactivé ce compte</string>
     <string name="security_error_invalid_file_access">Erreur de sécurité : accès invalide au fichier !</string>
     <string name="no_application_to_share_uri">Aucune application disponible pour partager l\'URI</string>
     <string name="share_uri_with">Partager l\'URI avec…</string>
     <string name="agree_and_continue">Accepter et continuer</string>
     <string name="magic_create_text">Nous vous guiderons tout au long du processus de création d\'un compte sur conversations.im.
-\nLorsque vous sélectionnerez conversations.im en tant que fournisseur, vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur fournissant votre adresse XMPP complète.</string>
+\nLorsque vous sélectionnerez conversations.im en tant que fournisseur, vous pourrez communiquer avec les utilisateur·ices d\'autres fournisseurs en leur fournissant votre adresse XMPP complète.</string>
     <string name="your_full_jid_will_be">Votre adresse XMPP complète sera : %s</string>
     <string name="create_account">Créer un compte</string>
     <string name="use_own_provider">Utiliser votre propre fournisseur</string>
-    <string name="pick_your_username">Choisissez votre nom d\'utilisateur</string>
+    <string name="pick_your_username">Choisissez votre nom d\'utilisateur·ice</string>
     <string name="pref_manually_change_presence">Gérer l\'état de disponibilité manuellement</string>
     <string name="pref_manually_change_presence_summary">Définir votre disponibilité lors de l\'édition de votre statut.</string>
     <string name="status_message">Message de statut</string>
@@ -548,7 +548,7 @@
     <string name="device_does_not_support_battery_op">Les optimisations de batterie ne peuvent pas être désactivées sur votre appareil</string>
     <string name="registration_please_wait">Échec de l\'inscription : Réessayer plus tard</string>
     <string name="registration_password_too_weak">Échec de l\'inscription : le mot de passe n\'est pas assez fort</string>
-    <string name="choose_participants">Choisir les participants</string>
+    <string name="choose_participants">Choisir les participant·es</string>
     <string name="creating_conference">Création d\'un groupe…</string>
     <string name="invite_again">Inviter à nouveau</string>
     <string name="gp_disable">Désactiver</string>
@@ -568,7 +568,7 @@
     <string name="type_pc">Ordinateur</string>
     <string name="type_phone">Smartphone</string>
     <string name="type_tablet">Tablette</string>
-    <string name="type_web">Navigateur Internet</string>
+    <string name="type_web">Navigateur web</string>
     <string name="type_console">Console</string>
     <string name="payment_required">Paiement requis</string>
     <string name="missing_internet_permission">Autoriser à accéder à internet</string>
@@ -583,7 +583,7 @@
     <string name="pref_delete_omemo_identities">Effacer les identités OMEMO</string>
     <string name="pref_delete_omemo_identities_summary">Régénérer vos clés OMEMO. Tous vos contacts devront vous vérifier à nouveau. À n\'utiliser qu\'en dernier recours.</string>
     <string name="delete_selected_keys">Supprimer les clés sélectionnées</string>
-    <string name="error_publish_avatar_offline">Vous devez être connecté pour publier votre avatar.</string>
+    <string name="error_publish_avatar_offline">Vous devez être connecté pour publier votre image de profil.</string>
     <string name="show_error_message">Afficher le message d\'erreur</string>
     <string name="error_message">Message d\'erreur</string>
     <string name="data_saver_enabled">Économiseur de données activé</string>
@@ -645,16 +645,16 @@
         <item quantity="many">%d mois</item>
         <item quantity="other">%d mois</item>
     </plurals>
-    <string name="pref_automatically_delete_messages">Suppression messages auto</string>
+    <string name="pref_automatically_delete_messages">Suppression automatique des messages</string>
     <string name="pref_automatically_delete_messages_description">Efface automatiquement de cet appareil les messages plus anciens que l\'intervalle choisi.</string>
     <string name="encrypting_message">Chiffrement du message en cours</string>
     <string name="not_fetching_history_retention_period">Aucun message récupéré, en raison de la configuration de rétention de l\'appareil.</string>
     <string name="transcoding_video">Compression de la vidéo en cours</string>
     <string name="contact_blocked_past_tense">Contact bloqué.</string>
-    <string name="pref_notifications_from_strangers">Notifications d\'inconnus</string>
-    <string name="pref_notifications_from_strangers_summary">Notifier les messages et appels reçus d\'inconnus.</string>
-    <string name="received_message_from_stranger">Message d\'un inconnu reçu</string>
-    <string name="block_stranger">Bloquer l\'inconnu</string>
+    <string name="pref_notifications_from_strangers">Notifications d\'inconnu·es</string>
+    <string name="pref_notifications_from_strangers_summary">Notifier les messages et appels reçus d\'inconnu·es.</string>
+    <string name="received_message_from_stranger">Message d\'un·e inconnu·e reçu</string>
+    <string name="block_stranger">Bloquer l\'inconnu·e</string>
     <string name="block_entire_domain">Bloquer le domaine entier</string>
     <string name="online_right_now">En ligne actuellement</string>
     <string name="retry_decryption">Nouvelle tentative de déchiffrement</string>
@@ -676,8 +676,6 @@
     <string name="message_copied_to_clipboard">Message copié dans le presse-papier</string>
     <string name="message">Message</string>
     <string name="private_messages_are_disabled">Les messages privés sont désactivés</string>
-    <string name="huawei_protected_apps">Applications protégées</string>
-    <string name="huawei_protected_apps_summary">Pour recevoir les notifications, même lorsque l\'écran est éteint, vous devez ajouter Conversations à la liste des applications protégées.</string>
     <string name="mtm_accept_cert">Accepter les certificats inconnus ?</string>
     <string name="mtm_trust_anchor">Le certificat du serveur n\'est pas signé par une Autorité de Certification connue.</string>
     <string name="mtm_accept_servername">Accepter un nom de serveur qui ne correspond pas ?</string>
@@ -688,7 +686,7 @@
     <string name="qr_code_scanner_needs_access_to_camera">La lecture d\'un QR code nécessite l\'accès à l\'appareil photo</string>
     <string name="pref_scroll_to_bottom">Faire défiler l\'écran jusqu\'en bas</string>
     <string name="pref_scroll_to_bottom_summary">Faire défiler l\'écran jusqu\'en bas après avoir envoyé un message</string>
-    <string name="edit_status_message_title">Modifier le message de l\'état</string>
+    <string name="edit_status_message_title">Modifier le message d\'état</string>
     <string name="edit_status_message">Modifier le message de statut</string>
     <string name="disable_encryption">Désactiver le chiffrement</string>
     <string name="error_trustkey_general">%1$s n\'est pas capable d\'envoyer un message chiffré à %2$s. Ceci peut être lié au fait que votre contact utilise un serveur obsolète ou un client qui ne gère par OMEMO. </string>
@@ -709,7 +707,7 @@
     <string name="not_encrypted_for_this_device">Le message n\'était pas chiffré pour cet appareil.</string>
     <string name="omemo_decryption_failed">Échec de déchiffrement du message OMEMO.</string>
     <string name="undo">annuler</string>
-    <string name="location_disabled">Le partage de positionnement est désactivé.</string>
+    <string name="location_disabled">Le partage de positionnement est désactivé</string>
     <string name="action_fix_to_location">Figer la position</string>
     <string name="action_unfix_from_location">Débloquer la position</string>
     <string name="action_copy_location">Copier la position</string>
@@ -726,14 +724,14 @@
     <string name="view_conversation">Voir la conversation</string>
     <string name="pref_use_share_location_plugin">Plugin de partage de localisation</string>
     <string name="pref_use_share_location_plugin_summary">Utilisez le plugin Share Location à la place de la carte intégrée</string>
-    <string name="copy_link">Copier l\'adresse internet</string>
+    <string name="copy_link">Copier l\'adresse web</string>
     <string name="copy_jabber_id">Copier l\'adresse XMPP</string>
     <string name="p1_s3_filetransfer">Partage de fichier HTTP pour S3</string>
     <string name="pref_start_search">Recherche directe</string>
     <string name="pref_start_search_summary">Lors de l\'ajout de conversations, afficher le clavier et placer le curseur sur le champ de recherche</string>
-    <string name="group_chat_avatar">Avatar du groupe</string>
-    <string name="host_does_not_support_group_chat_avatars">Le serveur ne prend pas en charge les avatars pour les groupes</string>
-    <string name="only_the_owner_can_change_group_chat_avatar">Seul le propriétaire peut changer l\'avatar d\'un groupe</string>
+    <string name="group_chat_avatar">Image de profil du groupe</string>
+    <string name="host_does_not_support_group_chat_avatars">Le serveur ne prend pas en charge les images de profil pour les groupes</string>
+    <string name="only_the_owner_can_change_group_chat_avatar">Seul le propriétaire peut changer l\'image de profil d\'un groupe</string>
     <string name="contact_name">Nom du contact</string>
     <string name="nickname">Surnom</string>
     <string name="group_chat_name">Nom</string>
@@ -759,7 +757,7 @@
     <string name="pref_more_notification_settings_summary">Importance, son, vibration</string>
     <string name="video_compression_channel_name">Compression vidéo</string>
     <string name="view_media">Voir les média</string>
-    <string name="group_chat_members">Participants</string>
+    <string name="group_chat_members">Participant·es</string>
     <string name="media_browser">Navigateur de média</string>
     <string name="security_violation_not_attaching_file">Fichier omis en raison d\'une violation de la sécurité.</string>
     <string name="pref_video_compression">Qualité des vidéos</string>
@@ -801,10 +799,10 @@
     <string name="unable_to_establish_secure_connection">Impossible d\'établir une connexion sécurisée.</string>
     <string name="unable_to_find_server">Impossible de trouver le serveur.</string>
     <string name="something_went_wrong_processing_your_request">Une erreur est survenue pendant le traitement de votre requête.</string>
-    <string name="invalid_user_input">Entrée utilisateur incorrecte</string>
+    <string name="invalid_user_input">Entrée utilisateur·ice incorrecte</string>
     <string name="temporarily_unavailable">Temporairement indisponible. Réessayez plus tard.</string>
     <string name="no_network_connection">Pas de connexion réseau.</string>
-    <string name="try_again_in_x">Veuillez réessayer dans%s</string>
+    <string name="try_again_in_x">Veuillez réessayer dans %s</string>
     <string name="rate_limited">Vous êtes à taux limité</string>
     <string name="too_many_attempts">Trop de tentatives</string>
     <string name="the_app_is_out_of_date">Vous utilisez une version obsolète de cette application.</string>
@@ -817,8 +815,8 @@
     <string name="reject_request">Rejeter la demande</string>
     <string name="install_orbot">Installer Orbot</string>
     <string name="start_orbot">Démarrer Orbot</string>
-    <string name="no_market_app_installed">Aucune application de marché installée.</string>
-    <string name="group_chat_will_make_your_jabber_id_public">Ce canal rendra votre adresse XMPP publique</string>
+    <string name="no_market_app_installed">Aucun magasin d\'applications installée.</string>
+    <string name="group_chat_will_make_your_jabber_id_public">Ce salon rendra votre adresse XMPP publique</string>
     <string name="ebook">e-book</string>
     <string name="video_original">Original (non compressé)</string>
     <string name="open_with">Ouvrir avec…</string>
@@ -827,48 +825,48 @@
     <string name="restore_backup">Restaurer la sauvegarde</string>
     <string name="restore">Restaurer</string>
     <string name="enter_password_to_restore">Entrez votre mot de passe pour que le compte %s restaure la sauvegarde.</string>
-    <string name="restore_warning">N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte du périphérique d’origine.</string>
+    <string name="restore_warning">N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte de l\'appareil d’origine.</string>
     <string name="unable_to_restore_backup">Impossible de restaurer la sauvegarde.</string>
     <string name="unable_to_decrypt_backup">Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ?</string>
     <string name="backup_channel_name">Sauvegarde &amp; restauration</string>
     <string name="enter_jabber_id">Entrez l\'adresse XMPP</string>
     <string name="create_group_chat">Créer un groupe</string>
-    <string name="join_public_channel">Rejoindre le canal public</string>
+    <string name="join_public_channel">Rejoindre le salon public</string>
     <string name="create_private_group_chat">Créer un groupe privé</string>
-    <string name="create_public_channel">Créer un canal public</string>
-    <string name="create_dialog_channel_name">Nom du canal</string>
+    <string name="create_public_channel">Créer un salon public</string>
+    <string name="create_dialog_channel_name">Nom du salon</string>
     <string name="xmpp_address">Adresse XMPP</string>
-    <string name="please_enter_name">Veuillez donner un nom au canal</string>
+    <string name="please_enter_name">Veuillez donner un nom au salon</string>
     <string name="please_enter_xmpp_address">Veuillez renseigner une adresse XMPP</string>
     <string name="this_is_an_xmpp_address">Ceci est une adresse XMPP. Veuillez renseigner un nom.</string>
-    <string name="creating_channel">Création d\'un canal public…</string>
-    <string name="channel_already_exists">Ce canal existe déjà</string>
-    <string name="joined_an_existing_channel">Vous avez rejoint un canal existant</string>
-    <string name="unable_to_set_channel_configuration">Impossible de sauvegarder la configuration du canal</string>
+    <string name="creating_channel">Création d\'un salon public…</string>
+    <string name="channel_already_exists">Ce salon existe déjà</string>
+    <string name="joined_an_existing_channel">Vous avez rejoint un salon existant</string>
+    <string name="unable_to_set_channel_configuration">Impossible de sauvegarder la configuration du salon</string>
     <string name="allow_participants_to_edit_subject">Autoriser quiconque à éditer le sujet</string>
     <string name="allow_participants_to_invite_others">Permettre à quiconque d\'inviter d\'autres personnes</string>
     <string name="anyone_can_edit_subject">N\'importe qui peut éditer le sujet.</string>
     <string name="owners_can_edit_subject">Les propriétaires peuvent éditer le sujet.</string>
-    <string name="admins_can_edit_subject">Les administrateurs peuvent modifier le sujet.</string>
+    <string name="admins_can_edit_subject">Les administrateur·ices peuvent modifier le sujet.</string>
     <string name="owners_can_invite_others">Les propriétaires peuvent inviter d\'autres personnes.</string>
     <string name="anyone_can_invite_others">N\'importe qui peut inviter d\'autres personnes.</string>
-    <string name="jabber_ids_are_visible_to_admins">Les adresses XMPP sont visibles par les administrateurs.</string>
+    <string name="jabber_ids_are_visible_to_admins">Les adresses XMPP sont visibles par les administrateur·ices.</string>
     <string name="jabber_ids_are_visible_to_anyone">Les adresses XMPP sont visibles par tous.</string>
-    <string name="no_users_hint_channel">Ce canal public n\'a pas de participants. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP.</string>
-    <string name="no_users_hint_group_chat">Ce groupe privé n\'a aucun participant.</string>
+    <string name="no_users_hint_channel">Ce salon public n\'a pas de participant·es. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP.</string>
+    <string name="no_users_hint_group_chat">Ce groupe privé n\'a aucun·e participant·e.</string>
     <string name="manage_permission">Gérer les privilèges</string>
-    <string name="search_participants">Rechercher des participants</string>
+    <string name="search_participants">Rechercher des participant·es</string>
     <string name="file_too_large">Fichier trop volumineux</string>
     <string name="attach">Joindre</string>
-    <string name="discover_channels">Découverte des canaux</string>
-    <string name="search_channels">Recherche des canaux</string>
+    <string name="discover_channels">Découverte des salons</string>
+    <string name="search_channels">Recherche des salons</string>
     <string name="channel_discovery_opt_in_title">Violation possible de la confidentialité !</string>
     <string name="i_already_have_an_account">J\'ai déjà un compte</string>
     <string name="add_existing_account">Ajouter un compte existant</string>
     <string name="register_new_account">Enregistrer un nouveau compte</string>
     <string name="this_looks_like_a_domain">Ceci ressemble à une adresse de domaine</string>
     <string name="add_anway">Ajouter quand même</string>
-    <string name="this_looks_like_channel">Ceci ressemble à une adresse de canal</string>
+    <string name="this_looks_like_channel">Ceci ressemble à une adresse de salon</string>
     <string name="share_backup_files">Partager les fichiers de sauvegardes</string>
     <string name="conversations_backup">Sauvegarder les conversations</string>
     <string name="event">Événement </string>
@@ -877,13 +875,13 @@
     <string name="account_already_setup">Ce compte a déjà été configuré</string>
     <string name="please_enter_password">Veuillez saisir le mot de passe pour ce compte</string>
     <string name="unable_to_perform_this_action">Impossible de réaliser cette action</string>
-    <string name="open_join_dialog">Rejoindre le canal public…</string>
+    <string name="open_join_dialog">Rejoindre le salon public…</string>
     <string name="sharing_application_not_grant_permission">L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier.</string>
-    <string name="group_chats_and_channels"><![CDATA[Canaux et groupes de discussion]]></string>
+    <string name="group_chats_and_channels">Salons et groupes de discussion</string>
     <string name="jabber_network">jabber.network</string>
     <string name="local_server">Serveur local</string>
-    <string name="pref_channel_discovery_summary">La plupart des utilisateurs devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP.</string>
-    <string name="pref_channel_discovery">Méthode de découverte des canaux</string>
+    <string name="pref_channel_discovery_summary">La plupart des utilisateur·ices devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP.</string>
+    <string name="pref_channel_discovery">Méthode de découverte des salons</string>
     <string name="backup">Sauvegarde</string>
     <string name="category_about">À propos</string>
     <string name="please_enable_an_account">Veuillez activer votre compte</string>
@@ -928,8 +926,8 @@
     <string name="could_not_correct_message">Impossible de corriger le message</string>
     <string name="search_all_conversations">Toutes les conversations</string>
     <string name="search_this_conversation">Cette conversation</string>
-    <string name="your_avatar">Votre avatar</string>
-    <string name="avatar_for_x">Avatar pour %s</string>
+    <string name="your_avatar">Votre image de profil</string>
+    <string name="avatar_for_x">Image de profil pour %s</string>
     <string name="encrypted_with_omemo">Chiffré avec OMEMO</string>
     <string name="encrypted_with_openpgp">Chiffré avec OpenPGP</string>
     <string name="not_encrypted">Non chiffré</string>
@@ -951,7 +949,7 @@
     <string name="failed_deliveries">Échec lors de la livraison</string>
     <string name="more_options">Plus d\'options</string>
     <string name="no_application_found">Aucune application trouvée</string>
-    <string name="invite_to_app">Inviter à Conversations</string>
+    <string name="invite_to_app">Inviter sur Conversations</string>
     <string name="unable_to_parse_invite">Impossible de lire l\'invitation</string>
     <string name="server_does_not_support_easy_onboarding_invites">Le serveur ne prend pas en charge la génération d\'invitations</string>
     <string name="no_active_accounts_support_this">Aucun compte actif ne prend en charge cette fonctionalité</string>
@@ -995,7 +993,7 @@
     <string name="pref_autojoin">Synchroniser les favoris</string>
     <string name="pref_autojoin_summary">Activer \"Rejoindre automatiquement\" en entrant ou sortant d\'un groupe et réagir aux modifications apportées par d\'autres clients.</string>
     <string name="vector_graphic">graphique vectoriel</string>
-    <string name="incoming_call_duration_timestamp">Appel entrant (%s) - %s</string>
+    <string name="incoming_call_duration_timestamp">Appel entrant (%s) · %s</string>
     <string name="outgoing_call_timestamp">Appel sortant · %s</string>
     <plurals name="n_missed_calls_from_x">
         <item quantity="one">%1$d appel manqué de %2$s</item>
@@ -1008,7 +1006,7 @@
     <string name="could_not_delete_account_from_server">Impossible de supprimer le compte du serveur</string>
     <string name="plain_text_document">Document texte</string>
     <string name="account_status_temporary_auth_failure">Échec temporaire de l\'authentification</string>
-    <string name="delete_avatar">Supprimer l\'avatar</string>
+    <string name="delete_avatar">Supprimer l\'image de profil</string>
     <string name="outdated_backup_file_format">Vous essayez d\'importer un format de fichier de sauvegarde obsolète</string>
     <string name="audiobook">Livre audio</string>
     <string name="unified_push_distributor">Distributeur UnifiedPush</string>
@@ -1016,7 +1014,7 @@
     <string name="report_spam">Signaler un spam</string>
     <string name="privacy_policy">Politique de confidentialité</string>
     <string name="quicksy_wants_your_consent">Quicksy vous demande votre consentement pour utiliser vos données</string>
-    <string name="report_spam_and_block">Signaler un spam et bloquer son auteur</string>
+    <string name="report_spam_and_block">Signaler un spam et bloquer son auteur·ice</string>
     <string name="account_state_logged_out">Déconnecté</string>
     <string name="log_in">S\'identifier</string>
     <string name="this_account_is_logged_out">Vous vous êtes déconnecté⸱e de ce compte</string>
@@ -1034,11 +1032,11 @@
     <string name="pref_light_dark_mode">Thème clair/sombre</string>
     <string name="pref_allow_screenshots">Autoriser les captures d\'écran</string>
     <string name="pref_category_e2ee">Chiffrement de bout en bout</string>
-    <string name="pref_allow_screenshots_summary">Afficher le contenu de l\'application dans le sélecteur d\'applications et autorise les captures d\'écran</string>
+    <string name="pref_allow_screenshots_summary">Afficher le contenu de l\'application dans le sélecteur d\'applications et autoriser les captures d\'écran</string>
     <string name="pref_title_trust_system_ca_store">Autorités de certification</string>
     <string name="pref_title_trust_system_ca_store_summary">Faire confiance aux certificats de l\'autorité de certification du système</string>
-    <string name="detect_mim">Oblige la liaison du canal</string>
-    <string name="detect_mim_summary">La liaison du canal peut permettre de détecter les attaques de l\'homme du milieu</string>
+    <string name="detect_mim">Oblige la liaison du salon</string>
+    <string name="detect_mim_summary">La liaison du salon peut permettre de détecter les attaques de l\'homme du milieu</string>
     <string name="pref_keyboard_options">Clavier</string>
     <string name="pref_create_backup_one_off_summary">Créer une sauvegarde ponctuelle</string>
     <string name="pref_backup_recurring">Créer une sauvegarde récurrente</string>
@@ -1051,7 +1049,7 @@
     <string name="welcome_header">Rejoignez la Conversation</string>
     <string name="pref_summary_security">Chiffrement de bout en bout, confiance aveugle avant vérification, détection des attaques de l\'homme du milieu</string>
     <string name="pref_category_operating_system">Système d\'exploitation</string>
-    <string name="unsupported_operation">Opération pas encore supportée</string>
+    <string name="unsupported_operation">Opération non supportée</string>
     <string name="pref_category_engagement_notifications">Notifications d\'utilisation</string>
     <string name="pref_up_long_summary">En agissant en tant que distributeur UnifiedPush, la connexion XMPP persistante, fiable et économique en batterie sera utilisée pour communiquer avec d\'autres applications compatibles comme Tusky, Ltt.rs, FluffyChat et d\'autres encore.</string>
     <string name="pref_fullscreen_notification">Notifications en plein écran</string>
@@ -1062,18 +1060,18 @@
     <string name="pref_use_colorful_bubbles_summary">Afficher un fond différent pour distinguer les messages envoyés et reçus</string>
     <string name="pref_dynamic_colors">Couleurs dynamiques</string>
     <string name="pref_dynamic_colors_summary">Couleurs systèmes (Material You)</string>
-    <string name="channel_discover_opt_in_message">La découverte de canaux utilise un service de tierce partie appelé &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt; Utiliser cette fonctionnalité va transmettre votre adresse IP et votre recherche à ce service. Voir leur &lt;a href=https://search.jabber.network/privacy&gt;Politique de confidentialité&lt;/a&gt; pour plus d\'informations.</string>
+    <string name="channel_discover_opt_in_message">La découverte de salons utilise un service tier appelé &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt; Utiliser cette fonctionnalité va transmettre votre adresse IP et votre recherche à ce service. Voir leur &lt;a href=https://search.jabber.network/privacy&gt;Politique de confidentialité&lt;/a&gt; pour plus d\'informations.</string>
     <string name="action_archive_chat">Archiver la conversation</string>
-    <string name="archive_this_chat">Archiver cette conversation</string>
+    <string name="archive_this_chat">Supprimer cette conversation ensuite</string>
     <string name="send_encrypted_message">Envoyer un message chiffré</string>
     <string name="title_undo_swipe_out_chat">Conversation archivée</string>
     <string name="switch_to_chat">Se rendre à la conversation</string>
     <string name="contact_uses_unverified_keys">Votre contact utilise un appareil qui n\'est pas encore vérifié. Scannez son QR code pour effectuer une vérification et éviter une attaque de l\'homme du milieu.</string>
-    <string name="start_chat">Discuter</string>
+    <string name="start_chat">Lancer une discussion</string>
     <string name="no_certificate_selected">Aucun certificat client sélectionné !</string>
     <string name="pref_summary_appearance">Thème, couleurs, captures d\'écran, saisie</string>
     <string name="pref_title_security">Sécurité</string>
-    <string name="unified_push_summary">Relai de notifications pour les applications de tierce partie compatibles avec UnifiedPush</string>
+    <string name="unified_push_summary">Relai de notifications pour les applications tierces compatibles avec UnifiedPush</string>
     <string name="notifications">Notifications</string>
     <string name="pref_attachments_summary">Taille de fichier, compression des images, qualité des vidéos</string>
     <string name="pref_notifications_summary">Période sans notifications, sonnerie, vibration, inconnus</string>
@@ -1084,12 +1082,32 @@
     <string name="pref_category_server_connection">Connection au serveur</string>
     <string name="pref_privacy_summary">Notifications d\'écriture, dernière connexion, disponibilité</string>
     <string name="pref_connection_summary">Nom d\'hôte et port, Tor</string>
-    <string name="pref_connection_summary_w_cd">Nom d\'hôte et port, Tor et découverte des canaux</string>
+    <string name="pref_connection_summary_w_cd">Nom d\'hôte et port, Tor et découverte des salons</string>
     <string name="pref_category_application">Application</string>
     <string name="pref_category_interaction">Interaction</string>
     <string name="pref_category_on_this_device">Sur l\'appareil</string>
-    <string name="pref_accept_invites_from_strangers">Invitations d\'inconnus</string>
-    <string name="pref_accept_invites_from_strangers_summary">Accepter les invitations aux conversations de groupes provenant d\'inconnus</string>
+    <string name="pref_accept_invites_from_strangers">Invitations d\'inconnu·es</string>
+    <string name="pref_accept_invites_from_strangers_summary">Accepter les invitations aux conversations de groupes provenant d\'inconnu·es</string>
     <string name="pref_large_font">Grande police</string>
     <string name="pref_large_font_summary">Augmenter la taille de la police dans les bulles de message</string>
+    <string name="allow_private_messages">Autoriser les messages privés</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Votre image de profil. Tapotez pour sélectionner une nouvelle image de profil depuis la galerie.</string>
+    <string name="could_not_disable_video">Impossible de désactiver la vidéo.</string>
+    <string name="server_info_sasl2">XEP-0388 : Extensible SASL Profile</string>
+    <string name="edit_name_and_topic">Éditer nom et sujet</string>
+    <string name="edit_configuration">Modifier la configuration</string>
+    <string name="change_notification_settings">Modifier les paramètres de notification</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur.</string>
+    <string name="call_is_using_earpiece">L\'appel passe par les écouteurs.</string>
+    <string name="server_info_bind2">XEP-0386 : Bind 2</string>
+    <string name="edit_nick">Éditer le pseudo</string>
+    <string name="delete_pgp_key">Supprimer la clé OpenPGP</string>
+    <string name="call_is_using_bluetooth">L\'appel passe par le bluetooth.</string>
+    <string name="flip_camera">Changer de caméra</string>
+    <string name="video_is_enabled_tap_to_disable">La vidéo est activée. Tapotez pour la désactiver.</string>
+    <string name="video_is_disabled_tap_to_enable">La vidéo est désactivée. Tapotez pour l\'activer.</string>
+    <string name="call_is_using_wired_headset">L\'appel passe par le casque filaire</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">L\'appel passe par le haut-parleur. Tapotez pour passer sur les écouteurs.</string>
+    <string name="call_is_using_speaker">L\'appel passe par le haut-parleur.</string>
+    <string name="server_info_login_mechanism">Mécanisme de connexion</string>
 </resources>

src/main/res/values-gl/strings.xml 🔗

@@ -676,8 +676,6 @@
     <string name="message_copied_to_clipboard">Mensaxe copiada ao portapapeis</string>
     <string name="message">Mensaxe</string>
     <string name="private_messages_are_disabled">As mensaxes privadas están desactivadas</string>
-    <string name="huawei_protected_apps">Apps protexidos</string>
-    <string name="huawei_protected_apps_summary">Para seguir recibindo notificacións, incluso cando a pantalla está apagada, tes que engadir Conversations á lista de apps protexidas.</string>
     <string name="mtm_accept_cert">¿Aceptar certificado descoñecido?</string>
     <string name="mtm_trust_anchor">O certificado do servidor non está asinado por unha autoridade de certificación coñecida.</string>
     <string name="mtm_accept_servername">¿Aceptar un nome de servidor que non coincida?</string>
@@ -938,7 +936,7 @@
     <string name="return_to_ongoing_call">Volver á chamada activa</string>
     <string name="could_not_switch_camera">Non se puido activar a cámara</string>
     <string name="add_to_favorites">Fixar enriba</string>
-    <string name="remove_from_favorites">Desafixar de enriba</string>
+    <string name="remove_from_favorites">Non fixar enriba</string>
     <string name="gpx_track">Ruta GPX</string>
     <string name="could_not_correct_message">No se pode correxir a mensaxe</string>
     <string name="search_all_conversations">Todos os chats</string>
@@ -1071,4 +1069,24 @@
     <string name="pref_fullscreen_notification_summary">Permitir que a app mostre a notificación de chamada entrante a pantalla completa cando o dispositivo está bloqueado.</string>
     <string name="pref_backup_summary">Crear única, Programar recurrentes</string>
     <string name="pref_create_backup_one_off_summary">Crear unha copia de apoio</string>
+    <string name="allow_private_messages">Permitir mensaxes privadas</string>
+    <string name="your_avatar_tap_to_select_new_avatar">O teu avatar. Toca para escoller un novo avatar desde a galería.</string>
+    <string name="could_not_disable_video">Non se puido desactivar o vídeo.</string>
+    <string name="edit_nick">Editar nome</string>
+    <string name="delete_pgp_key">Eliminar chave OpenPGP</string>
+    <string name="edit_name_and_topic">Editar nome e tema</string>
+    <string name="edit_configuration">Cambiar a configuración</string>
+    <string name="change_notification_settings">Cambiar axustes das notificacións</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">A chamada está usando o auricular, toca para usar o altofalante.</string>
+    <string name="call_is_using_earpiece">A chamada está usando auriculares.</string>
+    <string name="call_is_using_wired_headset">A chamada usa auriculares con cable</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">A chamada usa os altofalandes. Toca para cambiar a auriculares.</string>
+    <string name="call_is_using_speaker">A chamada está usando o altofalante.</string>
+    <string name="call_is_using_bluetooth">A chamada está usando bluetooth.</string>
+    <string name="flip_camera">Cambiar de cámara</string>
+    <string name="video_is_enabled_tap_to_disable">Video activado. Toca para desactivar.</string>
+    <string name="video_is_disabled_tap_to_enable">Video desactivado. Toca para activar.</string>
+    <string name="server_info_sasl2">XEP-0388: Extensible SASL Profile</string>
+    <string name="server_info_login_mechanism">Método de acceso</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
 </resources>

src/main/res/values-hu/strings.xml 🔗

@@ -631,8 +631,6 @@
     <string name="message_copied_to_clipboard">Üzenet a vágólapra másolva</string>
     <string name="message">Üzenet</string>
     <string name="private_messages_are_disabled">A személyes üzenetek le vannak tiltva</string>
-    <string name="huawei_protected_apps">Védett alkalmazások</string>
-    <string name="huawei_protected_apps_summary">Ha akkor is szeretne értesítéseket kapni, amikor a kijelző ki van kapcsolva, hozzá kell adnia a Conversations alkalmazást a védett alkalmazások listájához.</string>
     <string name="mtm_accept_cert">Elfogadja az ismeretlen tanúsítványt?</string>
     <string name="mtm_trust_anchor">A kiszolgáló tanúsítványa nincs aláírva egy ismert hitelesítésszolgáltató által.</string>
     <string name="mtm_accept_servername">Elfogadja a nem egyező kiszolgálónevet?</string>

src/main/res/values-it/strings.xml 🔗

@@ -683,8 +683,6 @@
     <string name="message_copied_to_clipboard">Messaggio copiato negli appunti</string>
     <string name="message">Messaggio</string>
     <string name="private_messages_are_disabled">I messaggi privati sono disattivati</string>
-    <string name="huawei_protected_apps">App protette</string>
-    <string name="huawei_protected_apps_summary">Per ricevere notifiche anche quando lo schermo è spento, devi aggiungere Conversations all\'elenco delle app protette.</string>
     <string name="mtm_accept_cert">Accetti il certificato sconosciuto?</string>
     <string name="mtm_trust_anchor">Il certificato del server non è firmato da una Certificate Authority nota.</string>
     <string name="mtm_accept_servername">Accetti il nome del server non corrispondente?</string>
@@ -1024,7 +1022,7 @@
     <string name="title_activity_share_with">Condividi con …</string>
     <string name="action_archive_chat">Archivia la chat</string>
     <string name="title_activity_new_chat">Nuova chat</string>
-    <string name="archive_this_chat">Archivia questa chat</string>
+    <string name="archive_this_chat">Elimina chat in seguito</string>
     <string name="title_undo_swipe_out_chat">Chat archiviata</string>
     <string name="welcome_header">Unisciti a Conversation</string>
     <string name="pref_use_colorful_bubbles">Messaggi di chat colorati</string>
@@ -1077,4 +1075,29 @@
     <string name="pref_title_trust_system_ca_store">Autorità di certificazione</string>
     <string name="pref_large_font">Caratteri grandi</string>
     <string name="pref_large_font_summary">Aumenta la dimensione dei caratteri nei messaggi</string>
+    <string name="pref_accept_invites_from_strangers">Inviti da estranei</string>
+    <string name="pref_accept_invites_from_strangers_summary">Accetta inviti a chat di gruppo da estranei</string>
+    <string name="pref_backup_summary">Crea una volta sola, Programma ricorrenze</string>
+    <string name="pref_backup_recurring">Backup ricorrente</string>
+    <string name="pref_fullscreen_notification">Notifiche a schermo intero</string>
+    <string name="unsupported_operation">Operazione non supportata</string>
+    <string name="allow_private_messages">Consenti messaggi privati</string>
+    <string name="pref_fullscreen_notification_summary">Consenti a questa app di mostrare notifiche di chiamate in arrivo che occupano l\'intero schermo quando il dispositivo è bloccato.</string>
+    <string name="pref_create_backup_one_off_summary">Crea backup una volta</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Il tuo avatar. Tocca per selezionare un nuovo avatar dalla galleria.</string>
+    <string name="could_not_disable_video">Impossibile disattivare il video.</string>
+    <string name="edit_nick">Modifica nick</string>
+    <string name="delete_pgp_key">Elimina chiave OpenPGP</string>
+    <string name="edit_name_and_topic">Modifica nome e argomento</string>
+    <string name="edit_configuration">Cambia configurazione</string>
+    <string name="change_notification_settings">Cambia impostazioni di notifica</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">La chiamata sta usando gli auricolari. Tocca per passare agli altoparlanti.</string>
+    <string name="call_is_using_earpiece">La chiamata sta usando gli auricolari.</string>
+    <string name="call_is_using_wired_headset">La chiamata sta usando cuffie cablate</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">La chiamata sta usando gli altoparlanti. Tocca per passare agli auricolari.</string>
+    <string name="call_is_using_speaker">La chiamata sta usando gli altoparlanti.</string>
+    <string name="call_is_using_bluetooth">La chiamata sta usando il bluetooth.</string>
+    <string name="video_is_enabled_tap_to_disable">Il video è attivo. Tocca per disattivarlo.</string>
+    <string name="video_is_disabled_tap_to_enable">Il video è disattivato. Tocca per attivarlo.</string>
+    <string name="flip_camera">Cambia fotocamera</string>
 </resources>

src/main/res/values-ja/strings.xml 🔗

@@ -9,28 +9,28 @@
     <string name="action_add_account">アカウントを追加</string>
     <string name="action_edit_contact">名前を編集</string>
     <string name="action_add_phone_book">アドレス帳に追加</string>
-    <string name="action_delete_contact">名簿から削除</string>
+    <string name="action_delete_contact">連絡先リストから削除</string>
     <string name="action_block_contact">連絡先をブロック</string>
-    <string name="action_unblock_contact">連絡先のブロックを解除</string>
+    <string name="action_unblock_contact">連絡先をブロック解除</string>
     <string name="action_block_domain">ドメインをブロック</string>
-    <string name="action_unblock_domain">ドメインのブロックを解除</string>
+    <string name="action_unblock_domain">ドメインをブロック解除</string>
     <string name="action_block_participant">参加者をブロック</string>
-    <string name="action_unblock_participant">参加者のブロックを解除</string>
+    <string name="action_unblock_participant">参加者をブロック解除</string>
     <string name="title_activity_manage_accounts">アカウントを管理</string>
     <string name="title_activity_settings">設定</string>
     <string name="title_activity_choose_contact">連絡先を選択</string>
     <string name="title_activity_choose_contacts">連絡先を選択</string>
     <string name="title_activity_share_via_account">アカウントで共有</string>
-    <string name="title_activity_block_list">ブロック一覧</string>
+    <string name="title_activity_block_list">ブロックリスト</string>
     <string name="just_now">ちょうど今</string>
-    <string name="minute_ago">1分前</string>
-    <string name="minutes_ago">%d分前</string>
+    <string name="minute_ago">1 分前</string>
+    <string name="minutes_ago">%d 分前</string>
     <plurals name="x_unread_conversations">
-        <item quantity="other">%d件の未読の会話</item>
+        <item quantity="other">未読の会話 %d 件</item>
     </plurals>
     <string name="sending">送信中…</string>
     <string name="message_decrypting">メッセージを復号しています。しばらくお待ちください…</string>
-    <string name="pgp_message">OpenPGPで暗号化されたメッセージ</string>
+    <string name="pgp_message">OpenPGP で暗号化されたメッセージ</string>
     <string name="nick_in_use">ニックネームはすでに使用されています</string>
     <string name="invalid_muc_nick">ニックネームが正しくありません</string>
     <string name="admin">管理者</string>
@@ -663,8 +663,6 @@
     <string name="message_copied_to_clipboard">メッセージをクリップボードにコピーしました</string>
     <string name="message">メッセージ</string>
     <string name="private_messages_are_disabled">非公開メッセージを無効化しました</string>
-    <string name="huawei_protected_apps">保護されたアプリ</string>
-    <string name="huawei_protected_apps_summary">画面がオフになっているときでも通知を受信し続けるには、保護されたアプリの一覧に Conversations を追加する必要があります。</string>
     <string name="mtm_accept_cert">未知の証明書を受け入れますか?</string>
     <string name="mtm_trust_anchor">サーバー証明書が既知の認証局によって署名されていません。</string>
     <string name="mtm_accept_servername">不一致のサーバー名を受け入れますか?</string>
@@ -992,24 +990,24 @@
     <string name="contact_uses_unverified_keys">連絡先は未検証のデバイスを使用しています。 QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。</string>
     <string name="unverified_devices">未検証のデバイスを使用しています。他のデバイスで QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。</string>
     <string name="no_permission_to_place_call">電話をかける権限がありません</string>
-    <string name="remove_bookmark_and_close">%s のブックマークを削除して会話を保管しますか ?</string>
+    <string name="remove_bookmark_and_close">%s のブックマークを削除して会話をアーカイブしますか ?</string>
     <string name="remove_bookmark">%s のブックマークを削除しますか ?</string>
     <string name="call_integration_not_available">通話の統合は利用できません。</string>
     <string name="rtp_state_contact_offline">連絡先は利用できません</string>
-    <string name="delete_and_close">削除して会話を保管</string>
+    <string name="delete_and_close">削除して会話をアーカイブ</string>
     <string name="pref_send_crash_reports">クラッシュ報告を送信</string>
-    <string name="corresponding_chats_closed">対応する会話は保管されました。</string>
+    <string name="corresponding_chats_closed">対応する会話はアーカイブされました。</string>
     <string name="no_certificate_selected">クライアント証明書が選択されていません。</string>
     <string name="pref_connection_summary">ホスト名とポート番号、Tor</string>
     <string name="pref_category_on_this_device">このデバイスで</string>
     <string name="pref_notifications_summary">猶予期間、着信音、バイブレーション、見知らぬ人</string>
     <string name="pref_summary_security">端末間暗号化、検証前の無条件の信頼、MITM検出</string>
-    <string name="action_archive_chat">会話を保管</string>
+    <string name="action_archive_chat">会話をアーカイブ</string>
     <string name="title_activity_new_chat">新しい会話</string>
-    <string name="archive_this_chat">この会話を保管</string>
+    <string name="archive_this_chat">その後の会話を削除</string>
     <string name="pref_dynamic_colors_summary">システムの配色 (Material You)</string>
     <string name="send_encrypted_message">暗号化メッセージを送信</string>
-    <string name="title_undo_swipe_out_chat">会話は保管されました</string>
+    <string name="title_undo_swipe_out_chat">会話はアーカイブされました</string>
     <string name="pref_use_colorful_bubbles">多彩な会話の吹き出し</string>
     <string name="switch_to_chat">会話に切り替え</string>
     <string name="pref_title_interface">インターフェース</string>
@@ -1044,4 +1042,10 @@
     <string name="pref_fullscreen_notification">全画面通知</string>
     <string name="pref_fullscreen_notification_summary">端末がロックされているとき、このアプリが全画面を占める着信通知を表示することを許可する。</string>
     <string name="unified_push_summary">UnifiedPush互換サードパーティアプリの通知中継</string>
+    <string name="unsupported_operation">対応されていない動作</string>
+    <string name="pref_privacy_summary">通知の種類、最終確認日時、在席状況</string>
+    <string name="allow_private_messages">非公開メッセージを許可</string>
+    <string name="pref_backup_recurring">繰り返し作成</string>
+    <string name="pref_backup_summary">一回限りもしくは繰り返し計画を作成</string>
+    <string name="pref_create_backup_one_off_summary">一回限りのバックアップを作成</string>
 </resources>

src/main/res/values-nb-rNO/strings.xml 🔗

@@ -448,8 +448,6 @@
     <string name="message_copied_to_clipboard">Melding kopiert til utklippstavle</string>
     <string name="message">Melding</string>
     <string name="private_messages_are_disabled">Private meldinger er skrudd av</string>
-    <string name="huawei_protected_apps">Beskyttede programmer</string>
-    <string name="huawei_protected_apps_summary">For å motta merknader, selv når skjermen er skrudd av, må du legge til Conversations i listen over beskyttede programmer.</string>
     <string name="title_activity_show_location">Vis plasseringsdata</string>
     <string name="create_dialog_group_chat_name">Gruppesludringsnavn</string>
     <string name="create_group_chat">Opprett gruppesludring</string>

src/main/res/values-nl/strings.xml 🔗

@@ -39,7 +39,7 @@
     <string name="moderator">Moderator</string>
     <string name="participant">Deelnemer</string>
     <string name="visitor">Bezoeker</string>
-    <string name="remove_contact_text">Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon zullen niet worden verwijderd.</string>
+    <string name="remove_contact_text">Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon worden niet verwijderd.</string>
     <string name="block_contact_text">Wil je alle berichten van %s blokkeren?</string>
     <string name="unblock_contact_text">Wil je %s deblokkeren en er weer berichten van kunnen ontvangen?</string>
     <string name="block_domain_text">Alle contacten van %s blokkeren?</string>
@@ -66,8 +66,8 @@
     <string name="crash_report_message">Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van %1$s.</string>
     <string name="send_now">Nu versturen</string>
     <string name="send_never">Niet opnieuw vragen</string>
-    <string name="problem_connecting_to_account">Verbinding maken met account mislukt</string>
-    <string name="problem_connecting_to_accounts">Verbinden met meerdere accounts mislukt</string>
+    <string name="problem_connecting_to_account">Verbinding met account mislukt</string>
+    <string name="problem_connecting_to_accounts">Verbinding met meerdere accounts mislukt</string>
     <string name="touch_to_fix">Tik hier op om accounts te beheren</string>
     <string name="attach_file">Bestand bijvoegen</string>
     <string name="not_in_roster">Wil je dit ontbrekende contact toevoegen aan je contactenlijst?</string>
@@ -79,7 +79,9 @@
     <string name="action_clear_history">Geschiedenis wissen</string>
     <string name="clear_conversation_history">Gespreksgeschiedenis wissen</string>
     <string name="delete_file_dialog">Bestand verwijderen</string>
-    <string name="delete_file_dialog_msg">Weet je zeker dat je dit bestand wil verwijderen?\n\n<b>Waarschuwing:</b> Dit zal kopieën van dit bericht die opgeslagen zijn op andere apparaten of servers niet verwijderen.</string>
+    <string name="delete_file_dialog_msg">Weet je zeker dat je dit bestand wil verwijderen?
+\n
+\n<b>Waarschuwing:</b> Dit zal kopieën van dit bericht die opgeslagen zijn op andere apparaten of servers niet verwijderen. </string>
     <string name="choose_presence">Apparaat kiezen</string>
     <string name="send_unencrypted_message">Verstuur onversleuteld bericht</string>
     <string name="send_message">Verstuur bericht</string>
@@ -92,7 +94,7 @@
     <string name="restart">Herstarten</string>
     <string name="install">Installeren</string>
     <string name="openkeychain_not_installed">Gelieve OpenKeychain te installeren</string>
-    <string name="offering">bezig met aanbieden…</string>
+    <string name="offering">Aanbieden…</string>
     <string name="waiting">wachten…</string>
     <string name="no_pgp_key">Geen OpenPGP-sleutel gevonden</string>
     <string name="no_pgp_keys">Geen OpenPGP-sleutels gevonden</string>
@@ -125,7 +127,7 @@
     <string name="attach_take_picture">Foto nemen</string>
     <string name="preemptively_grant">Op voorhand toestemming verlenen voor abonneren</string>
     <string name="error_not_an_image_file">Het bestand dat je gekozen hebt is geen afbeelding</string>
-    <string name="error_compressing_image">Kon de afbeelding niet converteren</string>
+    <string name="error_compressing_image">Kan afbeeldingsbestand niet converteren</string>
     <string name="error_file_not_found">Bestand niet gevonden</string>
     <string name="error_io_exception">Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar?</string>
     <string name="account_status_unknown">Onbekend</string>
@@ -155,8 +157,8 @@
     <string name="mgmt_account_publish_avatar">Avatar publiceren</string>
     <string name="mgmt_account_publish_pgp">OpenPGP-publieke sleutel publiceren</string>
     <string name="unpublish_pgp">OpenPGP-publieke sleutel verwijderen</string>
-    <string name="unpublish_pgp_message">Weet je zeker dat je je publieke OpenPGP-sleutel uit je aankondiging van aanwezigheid wilt verwijderen\?
-\nJe contacten zullen je geen versleutelde OpenPGP-berichten meer kunnen sturen.</string>
+    <string name="unpublish_pgp_message">Weet je zeker dat je jouw publieke OpenPGP-sleutel uit je aankondiging van aanwezigheid wilt verwijderen?
+\nJouw contacten zullen je geen versleutelde OpenPGP-berichten meer kunnen sturen.</string>
     <string name="openpgp_has_been_published">OpenPGP-publieke sleutel gepubliceerd.</string>
     <string name="mgmt_account_enable">Account inschakelen</string>
     <string name="attach_record_voice">Stem opnemen</string>
@@ -232,8 +234,8 @@
     <string name="touch_to_choose_picture">Tik op avatar om een foto uit de galerij te kiezen</string>
     <string name="publishing">Publiceren…</string>
     <string name="error_publish_avatar_server_reject">De server weigerde de publicatie van je afbeelding</string>
-    <string name="error_publish_avatar_converting">Kon de afbeelding niet converteren</string>
-    <string name="error_saving_avatar">Fout bij opslaan van avatar</string>
+    <string name="error_publish_avatar_converting">Kan de afbeelding niet converteren</string>
+    <string name="error_saving_avatar">Kan avatar niet op schijf opslaan</string>
     <string name="or_long_press_for_default">(Of hou lang ingedrukt om de oorspronkelijke terug te zetten)</string>
     <string name="error_publish_avatar_no_server_support">Je server ondersteunt de publicatie van avatars niet</string>
     <string name="private_message">gefluisterd</string>
@@ -251,7 +253,7 @@
     <string name="request_now">Nu aanvragen</string>
     <string name="ignore">Negeren</string>
     <string name="pref_security_settings">Beveiliging</string>
-    <string name="pref_allow_message_correction">Berichtcorrectie toestaan</string>
+    <string name="pref_allow_message_correction">Berichtcorrectie</string>
     <string name="pref_allow_message_correction_summary">Sta je contacten toe hun berichten na het versturen te verbeteren</string>
     <string name="pref_expert_options">Instellingen voor experts</string>
     <string name="pref_expert_options_summary">Wees voorzichtig met deze instellingen</string>
@@ -284,8 +286,8 @@
     <string name="jabber_id_copied_to_clipboard">XMPP-adres gekopieerd naar klembord</string>
     <string name="error_message_copied_to_clipboard">Foutmelding gekopieerd naar klembord</string>
     <string name="web_address">webadres</string>
-    <string name="scan_qr_code">2D-streepjescode scannen</string>
-    <string name="show_qr_code">2D-streepjescode tonen</string>
+    <string name="scan_qr_code">QR-code scannen</string>
+    <string name="show_qr_code">QR-code weergeven</string>
     <string name="show_block_list">Geblokkeerde contacten weergeven</string>
     <string name="account_details">Accountgegevens</string>
     <string name="confirm">Bevestigen</string>
@@ -294,10 +296,10 @@
     <string name="pref_keep_foreground_service_summary">Belet het besturingssysteem je verbinding te onderbreken</string>
     <string name="pref_create_backup">Back-up creëren</string>
     <string name="pref_create_backup_summary">Back-upbestanden worden opgeslagen in %s</string>
-    <string name="notification_create_backup_title">Bezig met creëren van back-upbestanden...</string>
+    <string name="notification_create_backup_title">Back-upbestanden aanmaken</string>
     <string name="notification_backup_created_title">Je back-up is opgeslagen</string>
     <string name="notification_backup_created_subtitle">De back-upbestanden zijn opgeslagen in %s</string>
-    <string name="restoring_backup">Bezig met herstellen van back-up...</string>
+    <string name="restoring_backup">Back-up terugzetten</string>
     <string name="notification_restored_backup_title">Je back-up is hersteld</string>
     <string name="notification_restored_backup_subtitle">Vergeet niet om de account in te schakelen.</string>
     <string name="choose_file">Bestand kiezen</string>
@@ -324,7 +326,7 @@
     <string name="no_more_history_on_server">Geen verdere geschiedenis op server</string>
     <string name="updating">Bijwerken…</string>
     <string name="password_changed">Wachtwoord gewijzigd!</string>
-    <string name="could_not_change_password">Kon wachtwoord niet wijzigen</string>
+    <string name="could_not_change_password">Kan wachtwoord niet wijzigen</string>
     <string name="change_password">Wachtwoord wijzigen</string>
     <string name="current_password">Huidig wachtwoord</string>
     <string name="new_password">Nieuw wachtwoord</string>
@@ -345,11 +347,11 @@
     <string name="remove_owner_privileges">Eigenaarsprivileges intrekken</string>
     <string name="remove_from_room">Verwijderen uit groepsgesprek</string>
     <string name="remove_from_channel">Verwijderen uit kanaal</string>
-    <string name="could_not_change_affiliation">Kon aansluiting niet wijzigen</string>
+    <string name="could_not_change_affiliation">Kan de aansluiting van %s niet wijzigen</string>
     <string name="ban_from_conference">Verbannen uit groepsgesprek</string>
     <string name="ban_from_channel">Verbannen uit kanaal</string>
     <string name="ban_now">Nu verbannen</string>
-    <string name="could_not_change_role">Kon rol van %s niet wijzigen</string>
+    <string name="could_not_change_role">Kan rol van %s niet wijzigen</string>
     <string name="conference_options">Instellingen voor privégroep</string>
     <string name="channel_options">Instellingen voor openbaar kanaal</string>
     <string name="members_only">Privé, enkel leden</string>
@@ -357,7 +359,7 @@
     <string name="moderated">Kanaal modereren</string>
     <string name="you_are_not_participating">Je neemt geen deel</string>
     <string name="modified_conference_options">Gespreksopties aangepast!</string>
-    <string name="could_not_modify_conference_options">Kon gespreksopties niet aanpassen</string>
+    <string name="could_not_modify_conference_options">Kan groepschatopties niet wijzigen</string>
     <string name="never">Nooit</string>
     <string name="until_further_notice">Voor onbepaalde duur</string>
     <string name="snooze">Sluimeren</string>
@@ -374,8 +376,8 @@
     <string name="apk">Android-applicatie</string>
     <string name="vcard">Contact</string>
     <string name="avatar_has_been_published">Avatar is gepubliceerd!</string>
-    <string name="sending_x_file">Bezig met versturen van %s</string>
-    <string name="offering_x_file">Bezig met aanbieden van %s</string>
+    <string name="sending_x_file">Versturen %s</string>
+    <string name="offering_x_file">Aanbieden %s</string>
     <string name="hide_offline">Offline contacten verbergen</string>
     <string name="contact_is_typing">%s is aan het typen…</string>
     <string name="contact_has_stopped_typing">%s is gestopt met typen</string>
@@ -411,15 +413,15 @@
     <string name="invalid_username">Dit is geen geldige gebruikersnaam</string>
     <string name="download_failed_server_not_found">Downloaden mislukt: server niet gevonden</string>
     <string name="download_failed_file_not_found">Downloaden mislukt: bestand niet gevonden</string>
-    <string name="download_failed_could_not_connect">Downloaden mislukt: kon geen verbinding maken met host</string>
-    <string name="download_failed_could_not_write_file">Download mislukt: kon bestand niet schrijven</string>
+    <string name="download_failed_could_not_connect">Downloaden mislukt: kan geen verbinding maken met host</string>
+    <string name="download_failed_could_not_write_file">Download mislukt: kan bestand niet schrijven</string>
     <string name="account_status_tor_unavailable">Tor-netwerk niet beschikbaar</string>
     <string name="account_status_bind_failure">Bindingsfout</string>
     <string name="server_info_broken">Gebroken</string>
     <string name="pref_presence_settings">Aanwezigheid</string>
     <string name="pref_treat_vibrate_as_silent">Trillen behandelen als stille modus</string>
-    <string name="pref_show_connection_options">Uitgebreide verbindingsinstellingen</string>
-    <string name="pref_show_connection_options_summary">Toon hostnaam- en poortinstellingen bij instellen van een account</string>
+    <string name="pref_show_connection_options">Hostname &amp; poort</string>
+    <string name="pref_show_connection_options_summary">Uitgebreide verbindingsinstellingen weergeven bij het instellen van een account</string>
     <string name="hostname_example">xmpp.voorbeeld.be</string>
     <string name="mam_prefs">Archiefvoorkeuren</string>
     <string name="server_side_mam_prefs">Voorkeuren voor archief aan serverzijde</string>
@@ -443,7 +445,7 @@
         <item quantity="other">%d berichten</item>
     </plurals>
     <string name="load_more_messages">Laad meer berichten</string>
-    <string name="sync_with_contacts">Synchroniseer met contacten</string>
+    <string name="sync_with_contacts">Integratie met contactenlijst</string>
     <string name="notify_on_all_messages">Melding bij alle berichten</string>
     <string name="notify_only_when_highlighted">Melding enkel wanneer aangesproken</string>
     <string name="notify_never">Meldingen uitgeschakeld</string>
@@ -510,13 +512,13 @@
     <string name="copy_fingerprint">Vingerafdruk kopiëren</string>
     <string name="verified_fingerprints">Geverifieerde vingerafdrukken</string>
     <string name="use_camera_icon_to_scan_barcode">Gebruik de camera om de streepjescode van een contact te scannen</string>
-    <string name="please_wait_for_keys_to_be_fetched">De sleutels worden opgehaald. Even geduld.</string>
+    <string name="please_wait_for_keys_to_be_fetched">Wacht tot de sleutels zijn opgehaald</string>
     <string name="share_as_barcode">Delen als streepjescode</string>
     <string name="share_as_uri">Delen als XMPP-URI</string>
     <string name="share_as_http">Delen als HTTP-link</string>
     <string name="pref_blind_trust_before_verification">Blindelings vertrouwen vóór verificatie</string>
     <string name="not_trusted">Onvertrouwd</string>
-    <string name="invalid_barcode">Ongeldige 2D-streepjescode</string>
+    <string name="invalid_barcode">Ongeldige QR-code</string>
     <string name="pref_clean_cache">Cache wissen</string>
     <string name="pref_clean_private_storage">Privéopslag wissen</string>
     <string name="pref_clean_private_storage_summary">Privéopslag waar bestanden worden bijgehouden wissen (de bestanden kunnen opnieuw gedownload worden van de server)</string>
@@ -548,7 +550,7 @@
     </plurals>
     <plurals name="months">
         <item quantity="one">%d maand</item>
-        <item quantity="other">%d maand</item>
+        <item quantity="other">%d maanden</item>
     </plurals>
     <string name="pref_automatically_delete_messages">Automatisch berichten verwijderen</string>
     <string name="pref_automatically_delete_messages_description">Verwijder automatisch berichten van dit apparaat ouder dan de ingestelde tijdsperiode.</string>
@@ -578,12 +580,10 @@
     <string name="message_copied_to_clipboard">Bericht gekopieerd naar klembord</string>
     <string name="message">Bericht</string>
     <string name="private_messages_are_disabled">Privéberichten zijn uitgeschakeld</string>
-    <string name="huawei_protected_apps">Beschermde apps</string>
-    <string name="huawei_protected_apps_summary">Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Conversations toevoegen aan de lijst met beschermde apps.</string>
     <string name="mtm_accept_cert">Onbekend certificaat aanvaarden?</string>
     <string name="mtm_trust_anchor">Het servercertificaat is niet ondertekend door een gekende certificaatautoriteit.</string>
     <string name="mtm_accept_servername">Verkeerde servernaam aanvaarden?</string>
-    <string name="mtm_hostname_mismatch">De server kon niet authenticeren als ‘%s’. Het certificaat is enkel geldig voor:</string>
+    <string name="mtm_hostname_mismatch">Kan de server niet verifiëren als ‘%s’. Het certificaat is alleen geldig voor:</string>
     <string name="mtm_connect_anyway">Wil je toch verbinding maken?</string>
     <string name="mtm_cert_details">Certificaatgegevens:</string>
     <string name="once">Eenmalig</string>
@@ -600,7 +600,7 @@
     <string name="draft">Ontwerp:</string>
     <string name="pref_omemo_setting">OMEMO-versleuteling</string>
     <string name="pref_omemo_setting_summary_always">OMEMO zal altijd gebruikt worden voor één-op-één- privégroepsgesprekken.</string>
-    <string name="pref_omemo_setting_summary_default_on">OMEMO zal standaard gebruikt worden voor nieuwe gesprekken.</string>
+    <string name="pref_omemo_setting_summary_default_on">OMEMO wordt standaard gebruikt voor nieuwe gesprekken.</string>
     <string name="pref_omemo_setting_summary_default_off">OMEMO zal uitdrukkelijk ingeschakeld moeten worden voor nieuwe gesprekken.</string>
     <string name="create_shortcut">Snelkoppeling aanmaken</string>
     <string name="default_on">Standaard aan</string>
@@ -627,7 +627,7 @@
     <string name="copy_jabber_id">XMPP-adres kopiëren</string>
     <string name="p1_s3_filetransfer">Bestanden delen via HTTP voor S3</string>
     <string name="pref_start_search">Rechtstreeks zoeken</string>
-    <string name="pref_start_search_summary">Open het toetsenbord op het scherm ‘Gesprek starten’ en plaats de cursor in het zoekveld</string>
+    <string name="pref_start_search_summary">Open op het scherm \'Nieuwe chat\' het toetsenbord en plaats de cursor in het zoekveld</string>
     <string name="group_chat_avatar">Gespreksafbeelding</string>
     <string name="host_does_not_support_group_chat_avatars">Host ondersteunt geen gespreksafbeeldingen</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Enkel de eigenaar kan de gespreksafbeelding wijzigen</string>
@@ -670,18 +670,18 @@
     <string name="verify_x">%s verifiëren</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[We hebben een sms gestuurd naar <b>%s</b>.]]></string>
     <string name="we_have_sent_you_another_sms">We hebben je nóg een sms gestuurd met 6-cijferige code.</string>
-    <string name="please_enter_pin_below">Voer de 6-cijferige code hieronder in.</string>
+    <string name="please_enter_pin_below">Voer hieronder de 6-cijferige code in.</string>
     <string name="resend_sms">Sms opnieuw versturen</string>
     <string name="resend_sms_in">Sms opnieuw versturen (%s)</string>
     <string name="wait_x">Even geduld (%s)</string>
-    <string name="back">terug</string>
+    <string name="back">Terug</string>
     <string name="possible_pin">Mogelijke pincode is automatisch van het klembord geplakt.</string>
     <string name="please_enter_pin">Voer je 6-cijferige code in.</string>
     <string name="abort_registration_procedure">Weet je zeker dat je de registratieprocedure wilt stopzetten?</string>
     <string name="yes">Ja</string>
     <string name="no">Nee</string>
-    <string name="verifying">Bezig met verifiëren…</string>
-    <string name="requesting_sms">Bezig met aanvragen van sms…</string>
+    <string name="verifying">Verifiëren…</string>
+    <string name="requesting_sms">SMS aanvragen…</string>
     <string name="incorrect_pin">De ingevoerde code is onjuist.</string>
     <string name="pin_expired">De toegestuurde code is verlopen.</string>
     <string name="unknown_api_error_network">Onbekende netwerkfout.</string>
@@ -708,7 +708,7 @@
     <string name="ebook">e-boek</string>
     <string name="video_original">Origineel (zonder compressie)</string>
     <string name="open_with">Openen met…</string>
-    <string name="set_profile_picture">Conversations-profielafbeelding</string>
+    <string name="set_profile_picture">Conversations profielafbeelding</string>
     <string name="choose_account">Kies een account</string>
     <string name="restore_backup">Back-up herstellen</string>
     <string name="restore">Herstellen</string>
@@ -725,7 +725,7 @@
     <string name="please_enter_name">Voer een naam in voor het kanaal</string>
     <string name="please_enter_xmpp_address">Voer een XMPP-adres in</string>
     <string name="this_is_an_xmpp_address">Dit is een XMPP-adres. Voer een naam in.</string>
-    <string name="creating_channel">Bezig met creëren van openbaar kanaal...</string>
+    <string name="creating_channel">Openbaar kanaal aanmaken…</string>
     <string name="channel_already_exists">Dit kanaal bestaat al</string>
     <string name="joined_an_existing_channel">Je hebt deelgenomen aan een bestaand kanaal</string>
     <string name="allow_participants_to_edit_subject">Iedereen mag het onderwerp aanpassen</string>
@@ -767,7 +767,7 @@
     <string name="microphone_unavailable">Je microfoon is niet beschikbaar</string>
     <string name="only_one_call_at_a_time">Je kunt slechts één gesprek tegelijk voeren.</string>
     <string name="return_to_ongoing_call">Terug naar lopend gesprek</string>
-    <string name="could_not_switch_camera">Kon camera niet wisselen</string>
+    <string name="could_not_switch_camera">Kan camera niet wisselen</string>
     <string name="add_to_favorites">Bovenaan vastzetten</string>
     <string name="remove_from_favorites">Bovenaan losmaken</string>
     <string name="could_not_correct_message">Kon bericht niet corrigeren</string>
@@ -784,8 +784,8 @@
         <item quantity="other">Bekijk %1$d deelnemers</item>
     </plurals>
     <plurals name="some_messages_could_not_be_delivered">
-        <item quantity="one">Een bericht kon niet worden afgeleverd</item>
-        <item quantity="other">Sommige berichten konden niet worden afgeleverd</item>
+        <item quantity="one">Een bericht kan niet worden afgeleverd</item>
+        <item quantity="other">Sommige berichten kunnen niet worden afgeleverd</item>
     </plurals>
     <string name="failed_deliveries">Mislukte afleveringen</string>
     <string name="more_options">Meer opties</string>
@@ -797,9 +797,312 @@
     <string name="unable_to_enable_video">Kan video niet schakelen.</string>
     <string name="plain_text_document">Onversleuteld document</string>
     <string name="account_registrations_are_not_supported">Accountregistraties zijn niet ondersteund</string>
-    <string name="pref_never_send_crash_summary">Door crashrapportages te versturen help je de ontwikkeling</string>
+    <string name="pref_never_send_crash_summary">Door foutrapportages te versturen help je de ontwikkeling</string>
     <string name="action_add_account_with_certificate">Inloggen met certificaat</string>
     <string name="action_archive_chat">Chat archiveren</string>
     <string name="unable_to_connect_to_keychain">Kan niet verbinden met OpenKeychain</string>
     <string name="start_chat">Chat starten</string>
+    <string name="unable_to_restore_backup">Kan back-up niet herstellen.</string>
+    <string name="unable_to_decrypt_backup">Kan back-up niet decoderen. Is het wachtwoord correct?</string>
+    <string name="account_status_incompatible_client">Incompatibele client</string>
+    <string name="account_state_logged_out">Uitgelogd</string>
+    <string name="pref_send_crash_reports">Stuur crashrapporten</string>
+    <string name="unable_to_parse_certificate">Kan certificaat niet verwerken</string>
+    <string name="unable_to_fetch_mam_prefs">Kan archiveringsvoorkeuren niet ophalen</string>
+    <string name="rtp_state_connectivity_error">Kan oproep niet verbinden</string>
+    <string name="remove_bookmark_and_close">Wil je de bladwijzer voor %s verwijderen en de gesprekken archiveren?</string>
+    <string name="without_mutual_presence_updates"><b>Waarschuwing:</b> Dit verzenden zonder updates van wederzijdse aanwezigheid kan onverwachte problemen veroorzaken.
+\n
+\n<small>Ga naar “Contactgegevens ” om je aanwezigheidsabonnementen te verifiëren.</small></string>
+    <string name="pref_autojoin">Bladwijzers synchroniseren</string>
+    <string name="pref_autojoin_summary">Stel de vlag “autojoin ” in bij het invoeren of verlaten van een MUC en reageer op wijzigingen die door andere clients zijn aangebracht.</string>
+    <string name="preparing_file">Klaar om bestand te delen</string>
+    <string name="file_deleted">Bestand verwijderd</string>
+    <string name="error_unable_to_create_temporary_file">Kan geen tijdelijk bestand aanmaken</string>
+    <string name="title_activity_share_with">Delen met…</string>
+    <string name="archive_this_chat">Chat daarna verwijderen</string>
+    <string name="remove_bookmark">Wil je de bladwijzer voor %s verwijderen?</string>
+    <string name="openkeychain_required_long">%1$s gebruikt &lt;b&gt;OpenKeychain&lt;/b&gt; om berichten te versleutelen en te decoderen en het beheer van je openbare sleutels. &lt;br&gt;&lt; br&gt;Het is gelicentieerd onder GPLv3+ en beschikbaar op F-Droid en Google Play&lt;br&gt;&lt;br&gt;&lt;small&gt;(Start %1$s hierna opnieuw.)&lt;/small&gt;</string>
+    <string name="contact_has_no_pgp_key">Kan het bericht niet versleutelen omdat de contactpersoon hun openbare sleutel niet aankondigt.
+\n
+\n<small>Vraag de contactpersoon om OpenPGP in te stellen. </small></string>
+    <string name="title_activity_new_chat">Nieuwe chat</string>
+    <string name="conference_creation_failed">Kan geen groepschat aanmaken</string>
+    <string name="error_trustkey_bundle">Kan coderingssleutels niet ophalen</string>
+    <string name="mgmt_account_delete_confirm_text">Weet je zeker dat je je account wilt verwijderen? Als je je account verwijdert, wordt je hele chatgeschiedenis gewist</string>
+    <string name="unable_to_perform_this_action">Kan deze actie niet uitvoeren</string>
+    <string name="clear_histor_msg">Wil je alle berichten in deze chat verwijderen?
+\n
+\n<b>Waarschuwing:</b> Dit heeft geen invloed op berichten die zijn opgeslagen op andere apparaten of servers.</string>
+    <string name="unable_to_start_recording">Kan opname niet starten</string>
+    <string name="unable_to_save_recording">Kan opname niet opslaan</string>
+    <string name="unable_to_set_channel_configuration">Kan kanaalconfiguratie niet opslaan</string>
+    <string name="contacts_have_no_pgp_keys">Kan het bericht niet versleutelen omdat de contactpersonen hun openbare sleutel niet aankondigen.
+\n
+\n<small>Vraag de hen om OpenPGP in te stellen. </small></string>
+    <string name="unable_to_update_account">Kan account niet bijwerken</string>
+    <string name="error_trustkey_device_list">Kan apparaatlijst niet ophalen</string>
+    <string name="unable_to_connect_to_server">Kan geen verbinding maken met de server.</string>
+    <string name="could_not_delete_account_from_server">Kan account niet van server verwijderen</string>
+    <string name="file_transmission_failed">kan het bestand niet delen</string>
+    <string name="error_no_keys_to_trust_server_error">Er zijn geen bruikbare sleutels beschikbaar voor deze contactpersoon.
+\nKan geen nieuwe sleutels van de server halen. Misschien is er iets mis met de server van de contactpersoon?</string>
+    <string name="error_security_exception_during_image_copy">De app die je gebruikte om deze afbeelding te selecteren, bood niet genoeg rechten om het bestand te lezen.
+\n
+\n<small>Gebruik een andere bestandsbeheerder om een afbeelding te kiezen</small>.</string>
+    <string name="request_presence_updates">Vraag eerst aanwezigheidsupdates aan bij je contactpersoon.
+\n
+\n<small>Dit wordt gebruikt om te bepalen welke chat-app jouw contactpersoon gebruikt</small>.</string>
+    <string name="send_encrypted_message">Verstuur versleuteld bericht</string>
+    <string name="pref_notification_sound">Kennisgevingsgeluid</string>
+    <string name="pref_notification_sound_summary">Kennisgevingsgeluid voor nieuwe berichten</string>
+    <string name="pref_call_ringtone_summary">Ringtone voor inkomende oproepen</string>
+    <string name="pref_notification_grace_period_summary">De tijdsduurmeldingen worden gedempt na het detecteren van activiteit op een van je andere apparaten.</string>
+    <string name="pref_prevent_screenshots_summary">App-inhoud verbergen in de app-schakelaar en schermopnames blokkeren</string>
+    <string name="error_security_exception">De app die je gebruikte om dit bestand te delen, bood onvoldoende rechten.</string>
+    <string name="account_status_regis_not_sup">Aanmelding wordt niet ondersteund door server</string>
+    <string name="server_info_external_service_discovery">XEP-0215: External Service Discovery</string>
+    <string name="install_openkeychain">Versleuteld bericht. Installeer OpenKeychain om het te decoderen.</string>
+    <string name="group_chats">Groepsberichten</string>
+    <string name="conference_technical_problems">Je hebt deze groepschat verlaten om technische redenen</string>
+    <string name="hosted_on">gehost op %s</string>
+    <string name="unable_to_find_server">Kan server niet vinden.</string>
+    <string name="unable_to_establish_secure_connection">Kan geen veilige verbinding tot stand brengen.</string>
+    <string name="pref_quick_action_summary">Knop “Verzenden” vervangen door snelle actie</string>
+    <string name="user_has_left_conference">%1$s heeft de groepschat verlaten</string>
+    <string name="search_group_chats">Groepschats doorzoeken</string>
+    <string name="captcha_required">CAPTCHA vereist</string>
+    <string name="certificate_chain_is_not_trusted">Onvertrouwde certificaatketen</string>
+    <string name="no_application_found_to_open_link">Geen app om koppeling te openen</string>
+    <string name="no_application_found_to_view_contact">Geen app om contact te bekijken</string>
+    <string name="vector_graphic">vector-afbeelding</string>
+    <string name="multimedia_file">multimediabestand</string>
+    <string name="audiobook">Audioboek</string>
+    <string name="download_failed_invalid_file">Download mislukt: Ongeldig bestand</string>
+    <string name="pref_away_when_screen_off">Afwezig wanneer het apparaat is vergrendeld</string>
+    <string name="pref_away_when_screen_off_summary">Afwezig melden als het apparaat vergrendeld is</string>
+    <string name="pref_dnd_on_silent_mode_summary">Bezet melden als het apparaat in stille modus is</string>
+    <string name="shared_text_with_x">Tekst gedeeld met %s</string>
+    <string name="error_no_keys_to_trust_presence">Er zijn geen bruikbare sleutels beschikbaar voor dit contact.
+\nZorg ervoor dat jullie allebei een aanwezigheidsabonnement hebben.</string>
+    <string name="removing_from_public_conference">Je probeert %s te verwijderen van een publiek kanaal. De enige manier om dat te doen, is door die gebruiker voor altijd te verbannen.</string>
+    <string name="shared_file_with_x">Bestand gedeeld met %s</string>
+    <string name="shared_image_with_x">Afbeelding gedeeld met %s</string>
+    <string name="shared_images_with_x">Afbeeldingen gedeeld met %s</string>
+    <string name="hostname_or_onion">Server- of .onion-adres</string>
+    <string name="pref_enter_is_send_summary">Gebruik de Enter-toets om een bericht te verzenden. Je kunt altijd Ctrl + Enter gebruiken om een bericht te verzenden, zelfs als deze optie is uitgeschakeld.</string>
+    <string name="pref_treat_vibrate_as_dnd_summary">Bezet melden als het apparaat op trillen staat</string>
+    <string name="clear_other_devices_desc">Weet je zeker dat je alle andere apparaten uit de OMEMO-aankondiging wilt verwijderen? De volgende keer dat je apparaten verbinding maken, zullen ze zichzelf opnieuw aankondigen, maar ontvangen ze mogelijk geen berichten die ondertussen zijn verzonden.</string>
+    <string name="account_status_host_unknown">De server is niet verantwoordelijk voor dit domein</string>
+    <string name="title_undo_swipe_out_chat">Chat gearchiveerd</string>
+    <string name="no_permission_to_place_call">Geen toestemming om te bellen</string>
+    <string name="no_keys_just_confirm">Je hebt de vingerafdruk van deze persoon al vertrouwd. Door \"Gereed\" te selecteren, bevestig je alleen dat %s deel uitmaakt van deze groepschat.</string>
+    <string name="welcome_header">Doe mee aan het gesprek</string>
+    <string name="welcome_header_quicksy">Welkom bij Quicksy!</string>
+    <string name="agree_and_continue">Akkoord en doorgaan</string>
+    <string name="quicksy_wants_your_consent">Quicksy vraagt toestemming om je gegevens te gebruiken</string>
+    <string name="pref_dnd_on_silent_mode">Bezet in stille modus</string>
+    <string name="no_storage_permission">Verleen %1$s toegang tot externe opslag</string>
+    <string name="no_camera_permission">Verleen %1$s toegang tot de camera</string>
+    <string name="pref_picture_compression_summary">Tip: Gebruik \'Bestand kiezen\' in plaats van \'Afbeelding kiezen\' om afzonderlijke afbeeldingen ongecomprimeerd te verzenden, ongeacht deze instelling.</string>
+    <string name="battery_optimizations_enabled_explained">Jouw apparaat maakt gebruik van zware batterij-optimalisaties voor %1$s, wat kan leiden tot vertraagde meldingen of zelfs berichtverlies.
+\nHet wordt aanbevolen om deze uit te schakelen.</string>
+    <string name="battery_optimizations_enabled_dialog">Jouw apparaat maakt gebruik van zware batterij-optimalisaties voor %1$s, wat kan leiden tot vertraagde meldingen of zelfs berichtverlies.
+\n
+\nJe wordt nu gevraagd om deze uit te schakelen.</string>
+    <string name="this_account_is_logged_out">Je bent afgemeld bij dit account</string>
+    <string name="security_error_invalid_file_access">Beveiligingsfout: Ongeldige bestandstoegang!</string>
+    <string name="no_application_to_share_uri">Er is geen app om URI te delen</string>
+    <string name="pref_broadcast_last_activity_summary">Laat je contacten zien wanneer je de app voor het laatst hebt gebruikt</string>
+    <string name="pref_theme_light">Licht</string>
+    <string name="pref_theme_dark">Donker</string>
+    <string name="missing_internet_permission">Toestemming verlenen om internet te gebruiken</string>
+    <string name="report_jid_as_spammer">Meld dit XMPP-adres voor spammen.</string>
+    <string name="pref_delete_omemo_identities_summary">Je OMEMO-sleutels opnieuw genereren. Al je contacten zullen je opnieuw moeten verifiëren. Gebruik dit alleen als laatste redmiddel.</string>
+    <string name="data_saver_enabled_explained">Jouw besturingssysteem verhindert %1$s om toegang te krijgen tot internet op de achtergrond. Om meldingen van nieuwe berichten te ontvangen, moet je %1$s onbeperkte toegang toestaan wanneer \"Databesparing\" is ingeschakeld.
+\n%1$s zal zich nog steeds inspannen om gegevens op te slaan waar mogelijk.</string>
+    <string name="device_does_not_support_data_saver">Jouw apparaat biedt geen ondersteuning voor het uitschakelen van gegevensbesparing voor %1$s.</string>
+    <string name="all_omemo_keys_have_been_verified">Je hebt alle OMEMO-sleutels die je bezit geverifieerd</string>
+    <string name="pref_blind_trust_before_verification_summary">Vertrouw nieuwe apparaten van niet-geverifieerde contacten, maar bevestig nieuwe apparaten onmiddellijk handmatig voor geverifieerde contacten.</string>
+    <string name="pref_clean_cache_summary">Cachemap opschonen (gebruikt door camera-app)</string>
+    <string name="corresponding_chats_closed">Bijbehorende chats gearchiveerd.</string>
+    <string name="pref_notifications_from_strangers_summary">Meldingen van berichten en oproepen van vreemden.</string>
+    <string name="pref_use_colorful_bubbles">Kleurrijke chatbubbels</string>
+    <string name="pref_use_colorful_bubbles_summary">Duidelijke achtergrondkleuren voor verzonden en ontvangen berichten</string>
+    <string name="pref_dynamic_colors">Dynamische kleuren</string>
+    <string name="pref_dynamic_colors_summary">Systeemkleuren (Material You)</string>
+    <string name="ongoing_calls_channel_name">Lopende oproepen</string>
+    <string name="missed_calls_channel_name">Gemiste oproepen</string>
+    <string name="continue_btn">Doorgaan</string>
+    <string name="application_found_to_open_website">Geen app om website te openen</string>
+    <string name="pref_headsup_notifications_summary">Let op-meldingen weergeven</string>
+    <string name="magic_create_text">Er is een handleiding opgezet voor het aanmaken van een account op conversations.im.
+\nWanneer je conversations.im als provider kiest, kun je communiceren met gebruikers van andere providers door hen jouw volledige XMPP-adres te geven.</string>
+    <string name="sync_with_contacts_long">%1$s verwerkt jouw contactenlijst lokaal, op jouw apparaat, om u de namen en profielfoto\'s te tonen voor overeenkomende contacten op XMPP.
+\n
+\nGegevens uit de contacten lijst blijven te allen tijde op jouw apparaat!</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">De barcode bevat geen vingerafdrukken voor deze chat.</string>
+    <string name="distrust_omemo_key_text">Weet je zeker dat je de verificatie van dit apparaat wilt verwijderen?
+\nDit apparaat en berichten ervan worden gemarkeerd als \'Niet vertrouwd\'.</string>
+    <string name="error_trustkey_general">%1$s kan geen versleutelde berichten verzenden naar %2$s. Dit kan te wijten zijn aan het feit dat jouw contact een verouderde server of client gebruikt die OMEMO niet aankan.</string>
+    <string name="pref_broadcast_last_activity">Laatst gezien</string>
+    <string name="reconnect_on_other_host">Opnieuw verbinding maken op een andere host</string>
+    <string name="blindly_trusted_omemo_keys">Blindelings vertrouwde OMEMO-sleutels, wat betekent dat ze iemand anders kunnen zijn of dat iemand kan hebben ingetikt.</string>
+    <string name="verifying_omemo_keys_trusted_source_account">Je staat op het punt om de OMEMO-sleutels van je eigen account te verifiëren. Dit is alleen veilig als je deze link hebt gevolgd van een betrouwbare bron waar alleen jij deze link had kunnen publiceren.</string>
+    <string name="incoming_calls_channel_name">Inkomende oproepen</string>
+    <string name="restore_warning_continued">Probeer geen back-ups terug te zetten die je niet zelf hebt aangemaakt!</string>
+    <string name="no_microphone_permission">Verleen %1$s toegang tot de microfoon</string>
+    <string name="foreground_service_channel_description">Deze meldingscategorie wordt gebruikt om een permanente melding weer te geven die aangeeft dat %1$s actief is.</string>
+    <string name="notification_group_calls">Oproepen</string>
+    <string name="delivery_failed_channel_name">Mislukte leveringen</string>
+    <string name="pref_message_notification_settings">Instellingen voor berichtmeldingen</string>
+    <string name="pref_incoming_call_notification_settings">Meldingsinstellingen voor inkomende oproepen</string>
+    <string name="no_account_deactivated">Geen (gedeactiveerd)</string>
+    <string name="delete_from_server">Account van server verwijderen</string>
+    <string name="hide_notification">Melding verbergen</string>
+    <string name="ongoing_video_call">Lopend videogesprek</string>
+    <string name="reconnecting_call">Oproep opnieuw verbinden</string>
+    <string name="incoming_call_duration_timestamp">Inkomende oproep (%s) · %s</string>
+    <string name="outgoing_call">Uitgaande oproep</string>
+    <string name="outgoing_call_timestamp">Uitgaande oproep · %s</string>
+    <string name="backup">Back-up</string>
+    <string name="rtp_state_finding_device">Apparaten ontdekken</string>
+    <string name="rtp_state_ringing">Gaat over</string>
+    <string name="rtp_state_application_failure">Storing met app</string>
+    <string name="rtp_state_security_error">Probleem met verificatie</string>
+    <string name="hang_up">Ophangen</string>
+    <string name="ongoing_call">Lopend gesprek</string>
+    <string name="rtp_state_contact_offline">Persoon is niet beschikbaar</string>
+    <string name="rtp_state_connectivity_lost_error">Verbinding verbroken</string>
+    <string name="rtp_state_retracted">Ingetrokken oproep</string>
+    <string name="outgoing_call_duration_timestamp">Uitgaande oproep (%s) · %s</string>
+    <string name="missed_call_timestamp">Gemiste oproep · %s</string>
+    <string name="missed_call">Gemiste oproep</string>
+    <string name="audio_call">Audiogesprek</string>
+    <string name="help">Hulp</string>
+    <string name="account_status_temporary_auth_failure">Tijdelijke authenticatiefout</string>
+    <string name="no_xmpp_adddress_found">Geen XMPP-adres gevonden</string>
+    <string name="delete_avatar">Avatar verwijderen</string>
+    <string name="audio_video_disabled_tor">Oproepen zijn uitgeschakeld bij gebruik van Tor</string>
+    <string name="switch_to_video">Overschakelen naar video</string>
+    <string name="reject_switch_to_video">Verzoek om overschakeling naar video afwijzen</string>
+    <string name="unified_push_distributor">UnifiedPush Distributor</string>
+    <string name="pref_up_push_account_title">XMPP-account</string>
+    <string name="pref_up_push_account_summary">Het account waarmee pushberichten worden ontvangen.</string>
+    <string name="pref_up_push_server_title">Push-server</string>
+    <string name="decline">Weigeren</string>
+    <plurals name="n_missed_calls_from_x">
+        <item quantity="one">%1$d gemiste oproep van %2$s</item>
+        <item quantity="other">%1$d gemiste oproepen van %2$s</item>
+    </plurals>
+    <string name="make_call">Oproepen</string>
+    <string name="please_enable_an_account">Schakel een account in</string>
+    <string name="rtp_state_incoming_video_call">Inkomende video-oproep</string>
+    <string name="rtp_state_content_add_video">Overschakelen naar videogesprek?</string>
+    <string name="rtp_state_content_add">Extra sporen toevoegen?</string>
+    <string name="rtp_state_connecting">Verbinden</string>
+    <string name="rtp_state_connected">Verbonden</string>
+    <string name="rtp_state_reconnecting">Opnieuw verbinden</string>
+    <string name="switch_to_chat">Overschakelen naar chat</string>
+    <string name="exit">Afsluiten</string>
+    <string name="rtp_state_accepting_call">Oproep aannemen</string>
+    <string name="rtp_state_ending_call">Oproep beëindigen</string>
+    <string name="answer_call">Antwoorden</string>
+    <string name="dismiss_call">Afwijzen</string>
+    <string name="gpx_track">GPX-route</string>
+    <string name="log_in">Inloggen</string>
+    <string name="log_out">Uitloggen</string>
+    <plurals name="n_missed_calls_from_m_contacts">
+        <item quantity="one">%1$d gemiste oproep van %2$d contactpersoon</item>
+        <item quantity="other">%1$d gemiste oproepen van %2$d contactpersonen</item>
+    </plurals>
+    <string name="pref_channel_discovery_summary">De meeste gebruikers moeten ‘jabber.network’ kiezen voor betere suggesties uit het hele openbare XMPP-ecosysteem.</string>
+    <string name="disable_tor_to_make_call">Tor uitschakelen voor oproepen</string>
+    <string name="reconnecting_video_call">Video-oproep opnieuw verbinden</string>
+    <string name="server_does_not_support_easy_onboarding_invites">Server biedt geen ondersteuning voor het aanmaken van uitnodigingen</string>
+    <string name="outdated_backup_file_format">Je probeert een verouderd back-upbestandsformaat te importeren</string>
+    <string name="sharing_application_not_grant_permission">De app voor delen heeft geen toestemming gegeven voor toegang tot dit bestand.</string>
+    <string name="group_chats_and_channels">Groepschats en kanalen</string>
+    <string name="jabber_network">jabber.network</string>
+    <string name="pref_channel_discovery">Channel discovery-methode</string>
+    <string name="rtp_state_incoming_call">Inkomende oproep</string>
+    <string name="incoming_call">Inkomende oproep</string>
+    <plurals name="n_missed_calls">
+        <item quantity="one">%d gemiste oproep</item>
+        <item quantity="other">%d gemiste oproepen</item>
+    </plurals>
+    <string name="encrypted_with_openpgp">Versleuteld met OpenPGP</string>
+    <string name="record_voice_mail">Antwoordbericht opnemen</string>
+    <string name="add_contact_or_create_or_join_group_chat">Contactpersoon toevoegen, groepschat aanmaken of erbij aansluiten, of kanalen ontdekken</string>
+    <string name="pref_notifications_summary">Uitstelperiode, Beltoon, Trilling, Vreemden</string>
+    <string name="pref_category_sending">Verzenden</string>
+    <string name="pref_category_receiving">Ontvangen</string>
+    <string name="pref_automatic_download">Automatisch downloaden</string>
+    <string name="appearance">Uiterlijk</string>
+    <string name="pref_light_dark_mode">Licht/donker thema</string>
+    <string name="pref_allow_screenshots">Schermopnames toestaan</string>
+    <string name="pref_allow_screenshots_summary">App-inhoud weergeven in app-schakelaar en schermopnames toestaan</string>
+    <string name="pref_category_e2ee">End-to-end-versleuteling</string>
+    <string name="pref_title_trust_system_ca_store">Certificaatautoriteiten</string>
+    <string name="pref_title_trust_system_ca_store_summary">CA-certificaten van het systeem vertrouwen</string>
+    <string name="pref_summary_security">E2E-versleuteling, Blind vertrouwen voor verificatie, MITM-detectie</string>
+    <string name="pref_create_backup_one_off_summary">Eenmalige back-up aanmaken</string>
+    <string name="pref_backup_recurring">Herhaalde back-up</string>
+    <string name="pref_backup_summary">Eenmalig aanmaken, Herhaald inplannen</string>
+    <string name="pref_fullscreen_notification">Meldingen op volledig scherm</string>
+    <string name="contact_list_integration_not_available">Contactlijstintegratie is niet beschikbaar</string>
+    <string name="unified_push_summary">Notification relay for UnifiedPush compatible third party apps</string>
+    <string name="report_spam">Spam rapporteren</string>
+    <string name="allow_private_messages">Privéberichten toestaan</string>
+    <string name="pref_accept_invites_from_strangers_summary">Accepteer uitnodigingen om chats van vreemden te groeperen</string>
+    <string name="pref_accept_invites_from_strangers">Uitnodigingen van vreemden</string>
+    <string name="pref_large_font">Groot lettertype</string>
+    <string name="pref_large_font_summary">Vergroot de lettergrootte in berichtbubbels</string>
+    <string name="unverified_devices">Je gebruikt niet-geverifieerde apparaten. Scan de QR-code op jouw andere apparaten om verificatie uit te voeren en actieve MITM-aanvallen te belemmeren.</string>
+    <string name="pref_fullscreen_notification_summary">Toestaan dat deze app inkomende oproepmeldingen weergeeft op het volledige scherm wanneer het apparaat is vergrendeld.</string>
+    <string name="contact_uses_unverified_keys">Jouw contactpersoon maakt gebruik van niet-geverifieerde apparaten. Scan hun QR-code om verificatie uit te voeren en actieve MITM-aanvallen te belemmeren.</string>
+    <string name="call_integration_not_available">Oproepintegratie niet beschikbaar!</string>
+    <string name="pref_up_long_summary">In de rol van een UnifiedPush Distributor wordt de permanente, betrouwbare en batterijvriendelijke XMPP-verbinding gebruikt om andere UnifiedPush-compatibele apps als Tusky, Ltt.rs, FluffyChat etc. uit de slaapstand te halen.</string>
+    <string name="pref_attachments_summary">Bestandsgrootte, Beeldcompressie, Videokwaliteit</string>
+    <string name="pref_connection_summary_w_cd">Hostnaam &amp; poort, Tor, Channel Discovery</string>
+    <string name="channel_discover_opt_in_message">Channel Discovery gebruikt een externe dienst &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Bij het gebruik van deze functie, worden jouw IP-adres en zoektermen naar deze dienst verzonden. Lees de &lt;a href=https://search.jabber.network/privacy&gt;Privacy Policy&lt;/a&gt; voor meer informatie.</string>
+    <string name="report_spam_and_block">Spam rapporteren en spammer blokkeren</string>
+    <string name="privacy_policy">Privacybeleid</string>
+    <string name="no_certificate_selected">Geen clientcertificaat geselecteerd!</string>
+    <string name="pref_title_interface">Interface</string>
+    <string name="pref_title_security">Beveiliging</string>
+    <string name="notifications">Meldingen</string>
+    <string name="pref_summary_appearance">Thema, Kleuren, Schermopnames, Invoer</string>
+    <string name="delete_and_close">Chat archiveren &amp; verwijderen</string>
+    <string name="detect_mim">Kanaalbinding vereisen</string>
+    <string name="detect_mim_summary">Kanaalbinding kan sommige machine-in-the-middle-aanvallen detecteren</string>
+    <string name="pref_category_server_connection">Server-verbinding</string>
+    <string name="pref_category_operating_system">Besturingssysteem</string>
+    <string name="pref_privacy_summary">Typemeldingen, Laatst gezien, Beschikbaarheid</string>
+    <string name="pref_connection_summary">Hostnaam &amp; poort, Tor</string>
+    <string name="pref_keyboard_options">Toetsenbord</string>
+    <string name="pref_category_engagement_notifications">Meldingen van betrokkenheid</string>
+    <string name="pref_category_application">Toepassing</string>
+    <string name="pref_category_interaction">Interactie</string>
+    <string name="pref_category_on_this_device">Op apparaat</string>
+    <string name="unsupported_operation">Niet-ondersteunde actie</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Jouw avatar. Tik om een nieuwe avatar uit de galerij te selecteren.</string>
+    <string name="could_not_disable_video">Video kan niet worden uitgeschakeld.</string>
+    <string name="edit_nick">Bijnaam bewerken</string>
+    <string name="delete_pgp_key">OpenPGP-sleutel verwijderen</string>
+    <string name="edit_name_and_topic">Naam en onderwerp bewerken</string>
+    <string name="edit_configuration">Configuratie wijzigen</string>
+    <string name="change_notification_settings">Meldingsinstellingen wijzigen</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Oproep maakt gebruik van luidspreker. Tik om over te schakelen naar de oortelefoon.</string>
+    <string name="call_is_using_earpiece">Oproep maakt gebruik van een oortelefoon.</string>
+    <string name="call_is_using_wired_headset">Oproep maakt gebruik van bedrade headset</string>
+    <string name="call_is_using_speaker">Oproep maakt gebruik van luidspreker.</string>
+    <string name="call_is_using_bluetooth">Oproep maakt gebruik van bluetooth.</string>
+    <string name="flip_camera">Camera omdraaien</string>
+    <string name="video_is_enabled_tap_to_disable">Video is ingeschakeld. Tik om uit te schakelen.</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Oproep maakt gebruik van een oortelefoon. Tik om over te schakelen naar de luidspreker.</string>
+    <string name="video_is_disabled_tap_to_enable">Video is uitgeschakeld. Tik om in te schakelen.</string>
+    <string name="server_info_sasl2">XEP-0388: Extensible SASL Profile</string>
+    <string name="server_info_login_mechanism">Inlogmethode</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
 </resources>

src/main/res/values-pl/strings.xml 🔗

@@ -9,7 +9,7 @@
     <string name="action_add_account">Dodaj konto</string>
     <string name="action_edit_contact">Edytuj nazwę</string>
     <string name="action_add_phone_book">Dodaj do kontaktów</string>
-    <string name="action_delete_contact">Usuń z rostera</string>
+    <string name="action_delete_contact">Usuń z rostera</string>
     <string name="action_block_contact">Zablokuj kontakt</string>
     <string name="action_unblock_contact">Odblokuj kontakt</string>
     <string name="action_block_domain">Zablokuj domenę</string>
@@ -190,16 +190,16 @@
     <string name="error_out_of_memory">Brak pamięci. Obraz jest za duży</string>
     <string name="add_phone_book_text">Czy chcesz dodać %s do listy kontaktów?</string>
     <string name="server_info_show_more">Informacje o serwerze</string>
-    <string name="server_info_mam">XEP-0313: MAM</string>
-    <string name="server_info_carbon_messages">XEP-0280: Kopie wiadomości</string>
-    <string name="server_info_csi">XEP-0352: Wskaźnik stanu klienta</string>
-    <string name="server_info_blocking">XEP-0191: Polecenia Blokujące</string>
-    <string name="server_info_roster_version">XEP-0237: Roster Versioning</string>
-    <string name="server_info_stream_management">XEP-0198: Zarządzanie Strumieniem</string>
-    <string name="server_info_external_service_discovery">XEP-0215: Wykrywanie Zewnętrznych Usług</string>
-    <string name="server_info_pep">XEP-0163: PEP (Awatary / OMEMO)</string>
-    <string name="server_info_http_upload">XEP-0363: Przesyłanie plików przez HTTP</string>
-    <string name="server_info_push">XEP-0357: Push</string>
+    <string name="server_info_mam">XEP-0313: zarządzanie archiwami wiadomości</string>
+    <string name="server_info_carbon_messages">XEP-0280: kopie wiadomości</string>
+    <string name="server_info_csi">XEP-0352: wskazywanie stanu klienta</string>
+    <string name="server_info_blocking">XEP-0191: polecenia blokujące</string>
+    <string name="server_info_roster_version">XEP-0237: wersjonowanie rostera</string>
+    <string name="server_info_stream_management">XEP-0198: zarządzanie strumieniem</string>
+    <string name="server_info_external_service_discovery">XEP-0215: wykrywanie zewnętrznych usług</string>
+    <string name="server_info_pep">XEP-0163: protokół osobistych zdarzeń (awatary/OMEMO)</string>
+    <string name="server_info_http_upload">XEP-0363: wysyłanie plików przez HTTP</string>
+    <string name="server_info_push">XEP-0357: powiadomienia Push</string>
     <string name="server_info_available">dostępny</string>
     <string name="server_info_unavailable">niedostępny</string>
     <string name="missing_public_keys">Brak informacji o kluczu publicznym</string>
@@ -696,8 +696,6 @@
     <string name="message_copied_to_clipboard">Wiadomość skopiowana do schowka</string>
     <string name="message">Wiadomość</string>
     <string name="private_messages_are_disabled">Prywatne wiadomości są wyłączone</string>
-    <string name="huawei_protected_apps">Aplikacje chronione</string>
-    <string name="huawei_protected_apps_summary">Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Conversations do listy chronionych aplikacji.</string>
     <string name="mtm_accept_cert">Zaakceptować nieznany certyfikat?</string>
     <string name="mtm_trust_anchor">Certyfikat serwera nie jest podpisany przez znany Urząd Certyfikacji.</string>
     <string name="mtm_accept_servername">Czy zaakceptować niepasującą nazwę serwera?</string>
@@ -1046,7 +1044,7 @@
     <string name="remove_bookmark_and_close">Czy chcesz usunąć zakładkę dla %s i zarchiwizować rozmowę?</string>
     <string name="action_archive_chat">Archiwizuj rozmowę</string>
     <string name="title_activity_new_chat">Nowa rozmowa</string>
-    <string name="archive_this_chat">Archiwizuj tę rozmowę</string>
+    <string name="archive_this_chat">Usuń rozmowę później</string>
     <string name="pref_use_colorful_bubbles">Kolorowe dymki rozmowy</string>
     <string name="barcode_does_not_contain_fingerprints_for_this_chat">Kod kreskowy nie zawiera odcisków palca dla tej rozmowy.</string>
     <string name="corresponding_chats_closed">Powiązane rozmowy zarchiwizowane.</string>
@@ -1101,4 +1099,23 @@
     <string name="pref_fullscreen_notification">Pełnoekranowe powiadomienia</string>
     <string name="unsupported_operation">Nieobsługiwana operacja</string>
     <string name="pref_fullscreen_notification_summary">Pozwól tej aplikacji na pokazywanie powiadomień o przychodzącym połączeniu, które zajmują cały ekran gdy urządzenie jest zablokowane.</string>
+    <string name="allow_private_messages">Pozwól na prywatne wiadomości</string>
+    <string name="edit_nick">Edytuj nazwę</string>
+    <string name="delete_pgp_key">Usuń klucz OpenPGP</string>
+    <string name="edit_name_and_topic">Edytuj nazwę i temat</string>
+    <string name="edit_configuration">Zmień konfigurację</string>
+    <string name="change_notification_settings">Zmień ustawienia powiadomień</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Rozmowa używa słuchawek. Dotknij aby przełączyć na głośnik.</string>
+    <string name="call_is_using_earpiece">Rozmowa używa słuchawek.</string>
+    <string name="call_is_using_wired_headset">Rozmowa używa przewodowego zestawu słuchawkowego</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Rozmowa używa głośnika. Dotknij aby przełączyć na słuchawki.</string>
+    <string name="call_is_using_speaker">Rozmowa używa głośnika.</string>
+    <string name="call_is_using_bluetooth">Rozmowa używa Bluetooth.</string>
+    <string name="flip_camera">Odwróć kamerę</string>
+    <string name="video_is_enabled_tap_to_disable">Wideo jest włączone. Dotknij aby wyłączyć.</string>
+    <string name="video_is_disabled_tap_to_enable">Wideo jest wyłączone. Dotknij aby włączyć.</string>
+    <string name="could_not_disable_video">Nie udało się wyłączyć wideo.</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Twój awatar. Dotknij aby wybrać nowy awatar z galerii.</string>
+    <string name="server_info_sasl2">XEP-0388: rozszerzalny profil SASL</string>
+    <string name="server_info_bind2">XEP-0386: uproszczone nawiązywanie połączenia</string>
 </resources>

src/main/res/values-pt-rBR/strings.xml 🔗

@@ -40,7 +40,7 @@
     <string name="moderator">Moderador</string>
     <string name="participant">Participante</string>
     <string name="visitor">Visitante</string>
-    <string name="remove_contact_text">Deseja remover %s da sua lista de contatos? As conversas associadas a esse contato não serão removidas.</string>
+    <string name="remove_contact_text">Gostaria de remover %s da sua lista de contatos? O chat com este contato não será removido.</string>
     <string name="block_contact_text">Deseja bloquear o recebimento de mensagens de %s?</string>
     <string name="unblock_contact_text">Deseja desbloquear o recebimento de mensagens de %s?</string>
     <string name="block_domain_text">Bloquear todos os contatos de %s?</string>
@@ -78,19 +78,19 @@
     <string name="preparing_images">Preparando para enviar as imagens</string>
     <string name="sharing_files_please_wait">Compartilhando arquivos. Por favor, aguarde...</string>
     <string name="action_clear_history">Limpar o histórico</string>
-    <string name="clear_conversation_history">Limpa o histórico de conversas</string>
+    <string name="clear_conversation_history">Limpar histórico do chat</string>
     <string name="clear_histor_msg">Deseja excluir todas as mensagens dessa conversa?
 \n
 \n<b>Atenção:</b> Isso não afetará mensagens armazenadas em outros dispositivos ou servidores.</string>
     <string name="delete_file_dialog">Excluir arquivo</string>
     <string name="delete_file_dialog_msg">Deseja realmente excluir este arquivo?\n\n<b>Atenção:</b> Isso não excluirá cópias deste arquivo armazenadas em outros dispositivos ou servidores.</string>
     <string name="choose_presence">Selecione o dispositivo</string>
-    <string name="send_unencrypted_message">Enviar mensagem não criptografada</string>
+    <string name="send_unencrypted_message">Enviar mensagem de texto não criptografada</string>
     <string name="send_message">Enviar mensagem</string>
     <string name="send_message_to_x">Enviar mensagem para %s</string>
     <string name="send_omemo_x509_message">Enviar mensagem criptografada via v\\OMEMO</string>
     <string name="your_nick_has_been_changed">Novo apelido em uso</string>
-    <string name="send_unencrypted">Enviar descriptografada</string>
+    <string name="send_unencrypted">Enviar texto não criptografada</string>
     <string name="decryption_failed">Não foi possível descriptografar. Talvez você não tenha a chave privada apropriada.</string>
     <string name="openkeychain_required">OpenKeychain</string>
     <string name="openkeychain_required_long"><![CDATA[%1$s utiliza o <b>OpenKeychain</b> para criptografar e descriptografar as mensagens e gerenciar suas chaves públicas. <br><br> Ele está licenciado sob a GPLv3+ e está disponível no F-Droid e na Google Play.<br><br><small>(Por favor reinicie o %1$s em seguida).</small>]]></string>
@@ -175,7 +175,7 @@
     <string name="unpublish_pgp_message">Tem certeza que deseja remover sua chave pública OpenPGP do seu anúncio de presença?\nSeus contatos não poderão mais enviar mensagens criptografadas com o OpenPGP para você.</string>
     <string name="openpgp_has_been_published">A chave pública do OpenPGP foi publicada.</string>
     <string name="mgmt_account_enable">Habilitar a conta</string>
-    <string name="mgmt_account_delete_confirm_text">Se você excluir a sua conta todo o seu histórico de conversas será apagado</string>
+    <string name="mgmt_account_delete_confirm_text">Tem certeza de que deseja excluir sua conta? Excluir sua conta apaga todo o seu histórico do chat</string>
     <string name="attach_record_voice">Gravar voz</string>
     <string name="account_settings_jabber_id">Endereço XMPP</string>
     <string name="block_jabber_id">Bloquear endereço XMPP</string>
@@ -678,8 +678,6 @@
     <string name="message_copied_to_clipboard">A mensagem foi copiada para a área de transferência</string>
     <string name="message">Mensagem</string>
     <string name="private_messages_are_disabled">As mensagens privadas estão desabilitadas</string>
-    <string name="huawei_protected_apps">Apps protegidos</string>
-    <string name="huawei_protected_apps_summary">Para continuar recebendo notificações, mesmo com a tela apagada, você precisa adicionar o Conversations à lista de apps protegidos.</string>
     <string name="mtm_accept_cert">Aceitar certificado desconhecido?</string>
     <string name="mtm_trust_anchor">O servidor do certificado não está assinado por uma autoridade certificadora reconhecida.</string>
     <string name="mtm_accept_servername">Aceitar nome de servidor não correspondente?</string>
@@ -939,7 +937,7 @@
     <string name="video_call">Chamada de vídeo</string>
     <string name="help">Ajuda</string>
     <string name="microphone_unavailable">Seu microfone não está disponível</string>
-    <string name="only_one_call_at_a_time">Você só pode ter uma chamada de cada vez</string>
+    <string name="only_one_call_at_a_time">Você só pode fazer uma chamada por vez</string>
     <string name="return_to_ongoing_call">Retornar para a chamada em andamento</string>
     <string name="could_not_switch_camera">Não foi possível trocar a câmera</string>
     <string name="add_to_favorites">Fixar no topo</string>
@@ -985,4 +983,43 @@
     <string name="audio_video_disabled_tor">As chamadas estão desabilitadas ao usar Tor</string>
     <string name="switch_to_video">Mudar para vídeo</string>
     <string name="reject_switch_to_video">Recusar requisição de mudança para vídeo</string>
-</resources>
+    <string name="pref_send_crash_reports">Enviar relatórios de erro</string>
+    <string name="remove_bookmark_and_close">Gostaria de remover o favorito de %s e arquivar chat?</string>
+    <string name="archive_this_chat">Exclua o chat depois</string>
+    <string name="title_activity_share_with">Compartilhar com…</string>
+    <string name="action_archive_chat">Arquivar chat</string>
+    <string name="title_activity_new_chat">Novo chat</string>
+    <string name="send_encrypted_message">Enviar mensagem criptografada</string>
+    <string name="account_state_logged_out">Desconectado</string>
+    <string name="remove_bookmark">Gostaria de remover o favorito de %s?</string>
+    <string name="unsupported_operation">Operação não suportada</string>
+    <string name="delete_and_close">Deletar e arquivar chat</string>
+    <string name="contact_uses_unverified_keys">Seu contato tem dispositivos não verificados. Escaneie o Código QR dele para fazer uma verificação e impedir ataques MITM.</string>
+    <string name="log_out">Sair</string>
+    <string name="unverified_devices">Você está usando dispositivos não verificados. Escaneie o Código QR em outro dispositivo para fazer a verificação, e impedir ataques MITM.</string>
+    <string name="switch_to_chat">Trocar para o chat</string>
+    <string name="pref_title_interface">Interface</string>
+    <string name="pref_summary_appearance">Tema, Cores, Capturas de tela, Entrada</string>
+    <string name="pref_title_security">Segurança</string>
+    <string name="pref_summary_security">Encriptação E2E, Confiar cegamente antes da verificação, Detecção de MITM</string>
+    <string name="notifications">Notificações</string>
+    <string name="pref_attachments_summary">Tamanho do arquivo, Compressão de imagem, Qualidade de vídeo</string>
+    <string name="pref_title_trust_system_ca_store_summary">Confiar nos certificados CA do sistema</string>
+    <string name="privacy_policy">Política de privacidade</string>
+    <string name="contact_list_integration_not_available">A integração de lista de contatos não está disponível</string>
+    <string name="log_in">Entrar</string>
+    <string name="pref_light_dark_mode">Modo claro/escuro</string>
+    <string name="pref_allow_screenshots">Permitir capturas de tela</string>
+    <string name="allow_private_messages">Permitir mensagens privadas</string>
+    <string name="report_spam">Reportar spam</string>
+    <string name="report_spam_and_block">Reportar e bloquear o spammer</string>
+    <string name="call_integration_not_available">A integração de chamada não está disponível!</string>
+    <string name="start_chat">Iniciar chat</string>
+    <string name="no_certificate_selected">Nenhum certificado de cliente selecionado!</string>
+    <string name="pref_category_sending">Enviando</string>
+    <string name="pref_category_receiving">Recebendo</string>
+    <string name="pref_automatic_download">Download automático</string>
+    <string name="appearance">Aparência</string>
+    <string name="pref_category_server_connection">Conexão com o servidor</string>
+    <string name="pref_category_operating_system">Sistema Operacional</string>
+</resources>

src/main/res/values-ro-rRO/strings.xml 🔗

@@ -688,8 +688,6 @@
     <string name="message_copied_to_clipboard">Mesaj copiat în clipboard</string>
     <string name="message">Mesaj</string>
     <string name="private_messages_are_disabled">Mesajele private sunt dezactivate</string>
-    <string name="huawei_protected_apps">Aplicații protejate</string>
-    <string name="huawei_protected_apps_summary">Pentru a continua să primiți notificări, chiar și când ecranul este oprit, trebuie să adăugați Conversations în lista de aplicații protejate.</string>
     <string name="mtm_accept_cert">Acceptați certificatul necunoscut?</string>
     <string name="mtm_trust_anchor">Certificatul serverului nu este semnat de o autoritate de certificare (CA) cunoscută.</string>
     <string name="mtm_accept_servername">Acceptați numele serverului ce nu corespunde?</string>
@@ -1088,4 +1086,21 @@
     <string name="pref_fullscreen_notification">Notificări pe tot ecranul</string>
     <string name="unsupported_operation">Operațiune neacceptată</string>
     <string name="pref_fullscreen_notification_summary">Atunci când dispozitivul este blocat permite aplicației să arate notificările apelurilor pe tot ecranul.</string>
+    <string name="allow_private_messages">Permite mesaje private</string>
+    <string name="could_not_disable_video">Nu s-a putut dezactiva videoul.</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Avatarul dumneavoastră. Atingeți pentru a selecta un nou avatar din galerie.</string>
+    <string name="change_notification_settings">Schimbă setările notificărilor</string>
+    <string name="edit_configuration">Schimbă configurația</string>
+    <string name="edit_name_and_topic">Editare nume și subiect</string>
+    <string name="delete_pgp_key">Ștergere cheie OpenPGP</string>
+    <string name="edit_nick">Editare nume</string>
+    <string name="video_is_disabled_tap_to_enable">Video dezactivat. Atingeți pentru activare.</string>
+    <string name="video_is_enabled_tap_to_disable">Video activat. Atingeți pentru dezactivare.</string>
+    <string name="flip_camera">Întoarce camera</string>
+    <string name="call_is_using_bluetooth">Apelul folosește Bluetooth.</string>
+    <string name="call_is_using_speaker">Apelul folosește difuzorul.</string>
+    <string name="call_is_using_wired_headset">Apelul folosește un set de căști cu fir</string>
+    <string name="call_is_using_earpiece">Apelul folosește receptorul.</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Apelul folosește difuzorul. Atingeți pentru a trece la receptor.</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Apelul folosește receptorul. Atingeți pentru a trece la difuzor.</string>
 </resources>

src/main/res/values-ru/strings.xml 🔗

@@ -41,7 +41,7 @@
     <string name="moderator">Модератор</string>
     <string name="participant">Участник</string>
     <string name="visitor">Посетитель</string>
-    <string name="remove_contact_text">Вы хотите удалить %s из своего списка контактов? Беседы, связанные с этим контактом, будут сохранены.</string>
+    <string name="remove_contact_text">Вы хотите удалить %s из своего списка контактов? Чат с этим контактом удалён не будет.</string>
     <string name="block_contact_text">Вы хотите заблокировать дальнейшие сообщения от %s?</string>
     <string name="unblock_contact_text">Вы хотите разблокировать пользователя %s?</string>
     <string name="block_domain_text">Заблокировать всех пользователей домена %s?</string>
@@ -79,7 +79,7 @@
     <string name="preparing_images">Подготовка к передаче изображений</string>
     <string name="sharing_files_please_wait">Обмен файлами. Пожалуйста, подождите…</string>
     <string name="action_clear_history">Очистить историю</string>
-    <string name="clear_conversation_history">Очистить историю</string>
+    <string name="clear_conversation_history">Очистить историю чата</string>
     <string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?
 \n
 \n<b>Внимание:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах.</string>
@@ -88,12 +88,12 @@
 \n
 \n<b>Предупреждение:</b> Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. </string>
     <string name="choose_presence">Выберите устройство</string>
-    <string name="send_unencrypted_message">Нешифрованное сообщение</string>
+    <string name="send_unencrypted_message">Отправить сообщение чистым текстом</string>
     <string name="send_message">Сообщение</string>
     <string name="send_message_to_x">Сообщение для %s</string>
     <string name="send_omemo_x509_message">v\\OMEMO-зашифр. сообщение</string>
     <string name="your_nick_has_been_changed">Используется новое имя</string>
-    <string name="send_unencrypted">Отправить в незашифрованном виде</string>
+    <string name="send_unencrypted">Отправить чистый текст</string>
     <string name="decryption_failed">Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа.</string>
     <string name="openkeychain_required">Установите OpenKeychain</string>
     <string name="openkeychain_required_long"><![CDATA[%1$s использует <b>OpenKeychain</b> для шифрования и дешифрования сообщений и управления открытыми ключами.<br><br>OpenKeychain распространяется под лицензией GPLv3+ и доступна для загрузки через F-Droid или Google Play.<br><br><small>(Потребуется перезапуск %1$s после установки.)</small>]]></string>
@@ -177,7 +177,7 @@
     <string name="unpublish_pgp_message">Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения.</string>
     <string name="openpgp_has_been_published">Публичный ключ OpenPGP опубликован.</string>
     <string name="mgmt_account_enable">Включить аккаунт</string>
-    <string name="mgmt_account_delete_confirm_text">Вы точно хотите удалить свою учётную запись? Это удалит все истории диалогов</string>
+    <string name="mgmt_account_delete_confirm_text">Вы точно хотите удалить свою учётную запись? Удаление учётной записи сотрёт все истории диалогов</string>
     <string name="attach_record_voice">Запись голоса</string>
     <string name="account_settings_jabber_id">XMPP-адрес</string>
     <string name="block_jabber_id">Заблокировать XMPP-адрес</string>
@@ -274,7 +274,7 @@
     <string name="ignore">Игнорировать</string>
     <string name="without_mutual_presence_updates"><b>Внимание:</b> Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблем.\n\n<small>Просмотрите сведения о контакте для проверки настроек обновлений присутствия.</small></string>
     <string name="pref_security_settings">Безопасность</string>
-    <string name="pref_allow_message_correction">Разрешить исправление сообщений</string>
+    <string name="pref_allow_message_correction">Исправление сообщений</string>
     <string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string>
     <string name="pref_expert_options">Расширенные настройки</string>
     <string name="pref_expert_options_summary">Пожалуйста, будьте осторожны с данными настройками</string>
@@ -464,8 +464,8 @@
     <string name="pref_dnd_on_silent_mode_summary">Устанавливает статус \"Не беспокоить\", когда устройство в беззвучном режиме</string>
     <string name="pref_treat_vibrate_as_silent">Не доступен в режиме вибрации</string>
     <string name="pref_treat_vibrate_as_dnd_summary">Устанавливает статус \"Не беспокоить\", когда устройство в режиме вибрации</string>
-    <string name="pref_show_connection_options">Расширенные настройки подключения</string>
-    <string name="pref_show_connection_options_summary">Показывать имя сервера и порт в настройках аккаунтов</string>
+    <string name="pref_show_connection_options">Имя хоста и Порт</string>
+    <string name="pref_show_connection_options_summary">Показывать расширенные настройки соединения при настройке аккаунта</string>
     <string name="hostname_example">xmpp.example.com</string>
     <string name="action_add_account_with_certificate">Авторизироваться с помощью сертификата</string>
     <string name="unable_to_parse_certificate">Не удалось прочитать сертификат</string>
@@ -558,7 +558,7 @@
     <string name="gp_medium">Средний</string>
     <string name="gp_long">Длинный</string>
     <string name="pref_broadcast_last_activity">Оповещать других об использовании</string>
-    <string name="pref_broadcast_last_activity_summary">Позволяет вашим контактам видеть, когда вы используете Conversations</string>
+    <string name="pref_broadcast_last_activity_summary">Позволяет вашим контактам видеть, когда вы в последний раз использовали приложение</string>
     <string name="pref_privacy">Приватность</string>
     <string name="pref_theme_options">Тема</string>
     <string name="pref_theme_options_summary">Выбрать цветовую палитру</string>
@@ -685,8 +685,6 @@
     <string name="message_copied_to_clipboard">Сообщение скопировано в буфер обмена</string>
     <string name="message">Сообщение</string>
     <string name="private_messages_are_disabled">Личные сообщения выключены</string>
-    <string name="huawei_protected_apps">Защищенные приложения</string>
-    <string name="huawei_protected_apps_summary">Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Conversations в список защищенных приложений.</string>
     <string name="mtm_accept_cert">Принять Неизвестный Сертификат?</string>
     <string name="mtm_trust_anchor">Этот сертификат сервера не подписан ни одним из известных центров сертификации.</string>
     <string name="mtm_accept_servername">Принять несовпадающее имя сервера?</string>
@@ -704,14 +702,14 @@
     <string name="error_trustkey_device_list">Не удалось получить список устройств</string>
     <string name="error_trustkey_bundle">Не удалось получить ключи шифрования</string>
     <string name="error_trustkey_hint_mutual">Подсказка: в некоторых случаях это может исправлено добавлением друг друга в список контактов.</string>
-    <string name="disable_encryption_message">Вы уверены, что хотите выключить OMEMO-шифрование для этой беседы?
+    <string name="disable_encryption_message">Вы уверены, что хотите выключить OMEMO-шифрование для этого чата?
 \nЭто позволит администратору сервера читать ваши сообщения, но также это может быть единственным способом связи с людьми, использующими устаревшие клиенты.</string>
     <string name="disable_now">Отключить сейчас</string>
     <string name="draft">Черновик:</string>
     <string name="pref_omemo_setting">OMEMO-шифрование</string>
     <string name="pref_omemo_setting_summary_always">OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций.</string>
-    <string name="pref_omemo_setting_summary_default_on">OMEMO будет использоваться по умолчанию для новых бесед.</string>
-    <string name="pref_omemo_setting_summary_default_off">OMEMO нужно будет явно включать для новых бесед.</string>
+    <string name="pref_omemo_setting_summary_default_on">OMEMO будет использоваться по умолчанию для новых чатов.</string>
+    <string name="pref_omemo_setting_summary_default_off">OMEMO нужно будет явно включать для новых чатов.</string>
     <string name="create_shortcut">Создать ярлык</string>
     <string name="default_on">Включено по умолчанию</string>
     <string name="default_off">Выключено по умолчанию</string>
@@ -732,14 +730,14 @@
     <string name="no_microphone_permission">Предоставить %1$s разрешение на использование микрофона</string>
     <string name="search_messages">Поиск сообщений</string>
     <string name="gif">GIF</string>
-    <string name="view_conversation">Посмотреть беседу</string>
+    <string name="view_conversation">Посмотреть чат</string>
     <string name="pref_use_share_location_plugin">Расширение для обмена информацией о местонахождении</string>
     <string name="pref_use_share_location_plugin_summary">Используйте расширение для обмена информацией о местонахождении вместо встроенной карты</string>
     <string name="copy_link">Копировать веб-адрес</string>
     <string name="copy_jabber_id">Копировать XMPP-адрес</string>
     <string name="p1_s3_filetransfer">Файлообмен по HTTP для S3</string>
     <string name="pref_start_search">Быстрый поиск</string>
-    <string name="pref_start_search_summary">На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска</string>
+    <string name="pref_start_search_summary">На экране \"Новая беседа\" открывать клавиатуру и ставить курсор в поле поиска</string>
     <string name="group_chat_avatar">Аватар конференции</string>
     <string name="host_does_not_support_group_chat_avatars">Сервер не поддерживает наличие аватар у конференций</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Только владелец может менять аватар конференции</string>
@@ -790,20 +788,20 @@
     <string name="verify_x">Подтвердите %s</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[Мы отправили вам SMS на <b>%s</b>.]]></string>
     <string name="we_have_sent_you_another_sms">Мы отправили вам еще одну SMS с кодом из 6 цифр.</string>
-    <string name="please_enter_pin_below">Пожалуйста, введите код из 6 цифр ниже.</string>
+    <string name="please_enter_pin_below">Пожалуйста, введите ПИН-код из 6 цифр ниже.</string>
     <string name="resend_sms">Отправьте заново SMS</string>
     <string name="resend_sms_in">Отправьте заново SMS (%s)</string>
     <string name="wait_x">Пожалуйста, подождите (%s)</string>
-    <string name="back">назад</string>
-    <string name="possible_pin">Автоматически вставлен возможный код из буфера обмена.</string>
-    <string name="please_enter_pin">Пожалуйста, введите ваш код из 6 цифр.</string>
+    <string name="back">Назад</string>
+    <string name="possible_pin">Автоматически вставлен возможный ПИН-код из буфера обмена.</string>
+    <string name="please_enter_pin">Пожалуйста, введите ваш ПИН-код из 6 цифр.</string>
     <string name="abort_registration_procedure">Вы уверены, что хотите прервать процедуру регистрации?</string>
     <string name="yes">Да</string>
     <string name="no">Нет</string>
     <string name="verifying">Подтверждение…</string>
     <string name="requesting_sms">Запрос SMS…</string>
-    <string name="incorrect_pin">Введенный вами код некорректен.</string>
-    <string name="pin_expired">Отправленный вам код просрочен.</string>
+    <string name="incorrect_pin">Введенный вами ПИН-код некорректен.</string>
+    <string name="pin_expired">Отправленный вам ПИН-код просрочен.</string>
     <string name="unknown_api_error_network">Неизвестная ошибка сети.</string>
     <string name="unknown_api_error_response">Неизвестный ответ от сервера.</string>
     <string name="unable_to_connect_to_server">Не удалось подключиться к серверу.</string>
@@ -931,8 +929,8 @@
     <string name="remove_from_favorites">Открепить</string>
     <string name="gpx_track">GPX-трек</string>
     <string name="could_not_correct_message">Не удалось исправить сообщение</string>
-    <string name="search_all_conversations">Все беседы</string>
-    <string name="search_this_conversation">Эта беседа</string>
+    <string name="search_all_conversations">Все чаты</string>
+    <string name="search_this_conversation">Этот чат</string>
     <string name="your_avatar">Ваш аватар</string>
     <string name="avatar_for_x">Аватар для %s</string>
     <string name="encrypted_with_omemo">Зашифровано с помощью OMEMO</string>
@@ -1039,8 +1037,8 @@
     <string name="call_integration_not_available">Интеграция вызовов недоступна!</string>
     <string name="no_permission_to_place_call">Нет разрешения на телефонный звонок</string>
     <string name="remove_bookmark">Вы хотите удалить закладку для «%s»?</string>
-    <string name="delete_and_close">Закрыть и удалить</string>
-    <string name="remove_bookmark_and_close">Вы хотите удалить закладку для %s и закрыть беседу?</string>
+    <string name="delete_and_close">Архивировать и удалить чат</string>
+    <string name="remove_bookmark_and_close">Вы хотите удалить закладку для %s и архивировать чат?</string>
     <string name="pref_send_crash_reports">Отправить отчёты о вылетах</string>
     <string name="switch_to_chat">Перейти к беседе</string>
     <string name="pref_summary_appearance">Тема, цвета, снимки, ввод</string>
@@ -1057,7 +1055,7 @@
     <string name="channel_discover_opt_in_message">Обзор каналов использует сторонний сервис &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Эта функция передаст Ваш IP-адрес и ваш поисковый запрос этому сервису. Ознакомьтесь с его &lt;a href=https://search.jabber.network/privacy&gt;Политикой конфиденциальности&lt;/a&gt; для получения подробностей.</string>
     <string name="action_archive_chat">Архивировать беседу</string>
     <string name="title_activity_new_chat">Новая беседа</string>
-    <string name="archive_this_chat">Архивировать эту беседу</string>
+    <string name="archive_this_chat">Затем удалить беседу</string>
     <string name="title_undo_swipe_out_chat">Беседа архивирована</string>
     <string name="welcome_header">Присоединиться к общению</string>
     <string name="barcode_does_not_contain_fingerprints_for_this_chat">Штрих-код не содержит отпечатков для этой беседы.</string>
@@ -1086,4 +1084,16 @@
     <string name="corresponding_chats_closed">Соответствующие беседы архивированы.</string>
     <string name="pref_title_trust_system_ca_store_summary">Доверять сертификатам системных УЦ</string>
     <string name="pref_connection_summary_w_cd">Имя хоста и порт, Тор, обзор каналов</string>
-</resources>
+    <string name="allow_private_messages">Разрешить личные сообщения</string>
+    <string name="pref_accept_invites_from_strangers_summary">Принимать приглашения в групповые беседы от незнакомцев</string>
+    <string name="unsupported_operation">Операция не поддерживается</string>
+    <string name="pref_fullscreen_notification">Полноэкранные уведомления</string>
+    <string name="pref_backup_recurring">Повторяющиеся рез. копии</string>
+    <string name="pref_fullscreen_notification_summary">Разрешить приложению показывать экраны входящих звонков при заблокированном экране.</string>
+    <string name="pref_up_long_summary">Conversations, будучи распределителем UnifiedPush, будет использовать стойкое, надёжное и малопотребляющее подключение XMPP для пробуждения других совместимых с UnifiedPush приложений, таких как Tusky, Ltt.rs, FluffyChat и др.</string>
+    <string name="detect_mim">Требовать привязку канала</string>
+    <string name="detect_mim_summary">Привязка канала может обнаружить некоторые атаки посредником</string>
+    <string name="pref_category_server_connection">Соединение с сервером</string>
+    <string name="pref_accept_invites_from_strangers">Приглашения от незнакомцев</string>
+    <string name="pref_category_engagement_notifications">Уведомления о взаимодействии</string>
+</resources>

src/main/res/values-sq-rAL/strings.xml 🔗

@@ -513,7 +513,6 @@
     <string name="message_copied_to_clipboard">Mesazhi u kopjua në të papastër</string>
     <string name="message">Mesazh</string>
     <string name="private_messages_are_disabled">Mesazhet private janë të çaktivizuara</string>
-    <string name="huawei_protected_apps">Aplikacione të Mbrojtur</string>
     <string name="mtm_accept_cert">Të pranohet Dëshmi e Panjohur\?</string>
     <string name="mtm_trust_anchor">Dëshmia e shërbyesit s’është nënshkruar prej një Autoriteti të njohur Dëshmish.</string>
     <string name="mtm_accept_servername">Të Pranohet Emër Shërbyesi i Ngatërruar\?</string>
@@ -785,7 +784,6 @@
     <string name="error_message">Mesazh Gabimi</string>
     <string name="error_unable_to_create_temporary_file">S’u krijua dot kartelë e përkohshme</string>
     <string name="this_device_has_been_verified">Kjo pajisje u verifikua</string>
-    <string name="huawei_protected_apps_summary">Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Conversations te lista e aplikacioneve të mbrojtur.</string>
     <string name="this_looks_like_channel">Kjo duket si adresë kanali</string>
     <string name="audio_call">Thirrje audio</string>
     <string name="video_call">Thirrje video</string>
@@ -1031,7 +1029,7 @@
     <string name="start_chat">Nisni fjalosje</string>
     <string name="action_archive_chat">Arkivoje fjalosjen</string>
     <string name="title_activity_new_chat">Fjalosje e re</string>
-    <string name="archive_this_chat">Arkivoje këtë fjalosje</string>
+    <string name="archive_this_chat">Fshije fjalosjen më pas</string>
     <string name="barcode_does_not_contain_fingerprints_for_this_chat">Kodi me vija s’përmban shenja gishtash për këtë fjalosje.</string>
     <string name="switch_to_chat">Kaloni te fjalosja</string>
     <string name="title_undo_swipe_out_chat">Fjalosje e arkivuar</string>
@@ -1081,4 +1079,25 @@
     <string name="pref_fullscreen_notification_summary">Lejojeni këtë aplikacion të shfaqë njoftime për thirrje ardhëse që zënë krejt ekranin, kur pajisja është e kyçur.</string>
     <string name="unsupported_operation">Veprim i pambuluar</string>
     <string name="pref_backup_recurring">Kopjeruajtje ripërsëritëse</string>
+    <string name="pref_create_backup_one_off_summary">Krijoni një kopjeruajtje një here të vetme</string>
+    <string name="allow_private_messages">Lejoni mesazhe private</string>
+    <string name="edit_nick">Përpunoni nofkë</string>
+    <string name="edit_name_and_topic">Përpunoni emër dhe temë</string>
+    <string name="edit_configuration">Ndryshoni formësim</string>
+    <string name="change_notification_settings">Ndryshoni rregullime njoftimesh</string>
+    <string name="call_is_using_earpiece">Thirrja po përdor kufje.</string>
+    <string name="call_is_using_wired_headset">Thirrja po përdor kufje me fill</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Thirrja po përdor altoparlant. Prekeni, që të kalohet në kufje.</string>
+    <string name="call_is_using_speaker">Thirrja po përdor altoparlant.</string>
+    <string name="call_is_using_bluetooth">Thirrja po përdor Bluetooth.</string>
+    <string name="delete_pgp_key">Fshini kyç OpenPGP</string>
+    <string name="could_not_disable_video">S’u çaktivizua dot videoja.</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Avatari juaj. Prekeni, që të përzgjidhni prej galerisë avatar të ri.</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Thirrja po përdor kufje. Prekeni, që të kalohet në altoparlant.</string>
+    <string name="video_is_enabled_tap_to_disable">Videoja është e aktivizuar. Prekeni, që të çaktivizohet.</string>
+    <string name="flip_camera">Ktheni kamerën më anë tjetër</string>
+    <string name="video_is_disabled_tap_to_enable">Videoja është e çaktivizuar. Prekeni, që të aktivizohet.</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
+    <string name="server_info_sasl2">XEP-0388: Profil SASL i Zgjerueshëm</string>
+    <string name="server_info_login_mechanism">Mekanizëm hyrjesh</string>
 </resources>

src/main/res/values-sv/strings.xml 🔗

@@ -687,8 +687,6 @@
     <string name="message_copied_to_clipboard">Meddelandet har kopierats till urklipp</string>
     <string name="message">Meddelande</string>
     <string name="private_messages_are_disabled">Privata meddelanden är inaktiverade</string>
-    <string name="huawei_protected_apps">Skyddade appar</string>
-    <string name="huawei_protected_apps_summary">För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade appar.</string>
     <string name="mtm_accept_cert">Acceptera okänt certifikat\?</string>
     <string name="mtm_trust_anchor">Servercertifikatet är inte signerat av en känd certifikatutfärdare.</string>
     <string name="mtm_accept_servername">Acceptera servernamn som inte matchar?</string>

src/main/res/values-szl/strings.xml 🔗

@@ -699,8 +699,6 @@
     <string name="message_copied_to_clipboard">Wiadōmość skopiyrowano do skrytki</string>
     <string name="message">Wiadōmość</string>
     <string name="private_messages_are_disabled">Prywatne wiadōmości sōm zastawiōne</string>
-    <string name="huawei_protected_apps">Aplikacyje chrōniōne</string>
-    <string name="huawei_protected_apps_summary">Coby dostować wiadōmości, kedy ekran je zastawiōny, musisz przidać Conversations do listy chrōniōnych aplikacyji.</string>
     <string name="mtm_accept_cert">Zaakceptować niyznōmy certyfikat\?</string>
     <string name="mtm_trust_anchor">Certyfikat ôd serwera niy ma podpisany ôd znōmego Amtu Certyfikacyje.</string>
     <string name="mtm_accept_servername">Zaakceptować niypasujōnce miano ôd serwera\?</string>

src/main/res/values-tr-rTR/strings.xml 🔗

@@ -677,8 +677,6 @@
     <string name="message_copied_to_clipboard">İleti panoya kopyalandı</string>
     <string name="message">İleti</string>
     <string name="private_messages_are_disabled">Özel iletiler devre dışı bırakıldı</string>
-    <string name="huawei_protected_apps">Korunan uygulamalar</string>
-    <string name="huawei_protected_apps_summary">Ekranınız kapalıyken bile bildirim almak için Conversations\'ı korunan uygulamalara eklemelisiniz.</string>
     <string name="mtm_accept_cert">Bilinmeyen sertifikayı kabul et?</string>
     <string name="mtm_trust_anchor">Sunucu sertifikası bilinen bir Sertifika Yetkilisi tarafından imzalanmamış.</string>
     <string name="mtm_accept_servername">Uyuşmayan Sunucu isimlerini kabul et?</string>

src/main/res/values-uk/strings.xml 🔗

@@ -163,7 +163,7 @@
     <string name="encryption_choice_omemo">OMEMO</string>
     <string name="mgmt_account_delete">Вилучити обліковий запис</string>
     <string name="mgmt_account_disable">Тимчасово вимкнути</string>
-    <string name="mgmt_account_publish_avatar">Опублікувати піктограму користувача</string>
+    <string name="mgmt_account_publish_avatar">Опублікувати аватар</string>
     <string name="mgmt_account_publish_pgp">Опублікувати публічний ключ OpenPGP</string>
     <string name="unpublish_pgp">Вилучити публічний ключ OpenPGP</string>
     <string name="unpublish_pgp_message">Ви впевнені, що хочете вилучити свій публічний ключ OpenPGP з оголошення про присутність\?
@@ -187,7 +187,7 @@
     <string name="server_info_roster_version">XEP-0237: Зміни у списку контактів</string>
     <string name="server_info_stream_management">XEP-0198: Керування потоком</string>
     <string name="server_info_external_service_discovery">XEP-0215: Виявлення зовнішньої служби</string>
-    <string name="server_info_pep">XEP-0163: PEP (піктограми користувачів, OMEMO)</string>
+    <string name="server_info_pep">XEP-0163: PEP (аватари користувачів, OMEMO)</string>
     <string name="server_info_http_upload">XEP-0363: Обмін файлами через HTTP</string>
     <string name="server_info_push">XEP-0357: Push-повідомлення</string>
     <string name="server_info_available">так</string>
@@ -245,13 +245,13 @@
     <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s +%2$d дочитали до цього місця</string>
     <string name="everyone_has_read_up_to_this_point">Усі прочитали до цього місця</string>
     <string name="publish">Опублікувати</string>
-    <string name="touch_to_choose_picture">Торкніться піктограми користувача, щоб вибрати зображення з галереї</string>
+    <string name="touch_to_choose_picture">Торкніться аватара, щоб вибрати зображення з галереї</string>
     <string name="publishing">Публікація…</string>
     <string name="error_publish_avatar_server_reject">Сервер відхилив Вашу публікацію</string>
     <string name="error_publish_avatar_converting">Не вдалося конвертувати Ваше зображення</string>
-    <string name="error_saving_avatar">Неможливо зберегти піктограму користувача на пристрій</string>
+    <string name="error_saving_avatar">Неможливо зберегти аватар на пристрій</string>
     <string name="or_long_press_for_default">(Або натисніть і тримайте, щоб скинути до значення за замовчуванням)</string>
-    <string name="error_publish_avatar_no_server_support">Ваш сервер не підтримує публікацію піктограм користувачів</string>
+    <string name="error_publish_avatar_no_server_support">Ваш сервер не підтримує публікацію аватарів</string>
     <string name="private_message">шепоче</string>
     <string name="private_message_to">%s</string>
     <string name="send_private_message_to">Надіслати приватне повідомлення %s</string>
@@ -313,7 +313,7 @@
     <string name="account_details">Деталі облікового запису</string>
     <string name="confirm">Підтвердити</string>
     <string name="try_again">Спробуйте ще</string>
-    <string name="pref_keep_foreground_service">Фонова служба</string>
+    <string name="pref_keep_foreground_service">Процес на передньому плані</string>
     <string name="pref_keep_foreground_service_summary">Не дає операційній системі припиняти Ваш зв\'язок</string>
     <string name="pref_create_backup">Створити резервну копію</string>
     <string name="pref_create_backup_summary">Резервні копії зберігатимуться до %s</string>
@@ -344,7 +344,7 @@
     <string name="enable_notifications">Увімкнути сповіщення</string>
     <string name="no_conference_server_found">Не знайдено сервер групи</string>
     <string name="conference_creation_failed">Не вдалося створити групу</string>
-    <string name="account_image_description">Піктограма облікового запису</string>
+    <string name="account_image_description">Зображення облікового запису</string>
     <string name="copy_omemo_clipboard_description">Копіювати цифровий підпис OMEMO</string>
     <string name="regenerate_omemo_key">Повторно створити ключ OMEMO</string>
     <string name="clear_other_devices">Стерти пристрої</string>
@@ -366,7 +366,7 @@
     <string name="enable_all_accounts">Увімкнути всі облікові записи</string>
     <string name="disable_all_accounts">Вимкнути всі облікові записи</string>
     <string name="perform_action_with">Здійснити дію з</string>
-    <string name="no_affiliation">Учасник</string>
+    <string name="no_affiliation">Без ролі</string>
     <string name="no_role">Поза мережею</string>
     <string name="outcast">Вигнанець</string>
     <string name="member">Учасник</string>
@@ -379,7 +379,7 @@
     <string name="remove_owner_privileges">Відкликати права власника</string>
     <string name="remove_from_room">Вилучити з групи</string>
     <string name="remove_from_channel">Прибрати з каналу</string>
-    <string name="could_not_change_affiliation">Неможливо змінити статус участі %s</string>
+    <string name="could_not_change_affiliation">Неможливо змінити роль %s</string>
     <string name="ban_from_conference">Заборонити доступ до групи</string>
     <string name="ban_from_channel">Вилучити з каналу</string>
     <string name="removing_from_public_conference">Ви намагаєтеся вилучити %s з публічного каналу. Єдиним способом для цього є заблокувати користувача назавжди.</string>
@@ -409,7 +409,7 @@
     <string name="pdf_document">документ PDF</string>
     <string name="apk">Програма Android</string>
     <string name="vcard">Контакт</string>
-    <string name="avatar_has_been_published">Піктограму користувача опубліковано!</string>
+    <string name="avatar_has_been_published">Аватар опубліковано!</string>
     <string name="sending_x_file">Надсилання %s</string>
     <string name="offering_x_file">Пропозиція %s</string>
     <string name="hide_offline">Сховати поза мережею</string>
@@ -464,10 +464,10 @@
     <string name="hostname_example">xmpp.example.com</string>
     <string name="action_add_account_with_certificate">Вхід із сертифікатом</string>
     <string name="unable_to_parse_certificate">Не вдалося розпізнати сертифікат</string>
-    <string name="mam_prefs">Налаштування збереження</string>
-    <string name="server_side_mam_prefs">Налаштування збереження на стороні сервера</string>
-    <string name="fetching_mam_prefs">Отримання налаштувань збереження. Будь ласка, зачекайте…</string>
-    <string name="unable_to_fetch_mam_prefs">Не вдалося отримати налаштування збереження</string>
+    <string name="mam_prefs">Налаштування архівації</string>
+    <string name="server_side_mam_prefs">Налаштування архівації на стороні сервера</string>
+    <string name="fetching_mam_prefs">Отримання налаштувань архівації. Будь ласка, зачекайте…</string>
+    <string name="unable_to_fetch_mam_prefs">Не вдалося отримати налаштування архівації</string>
     <string name="captcha_required">Потрібно розв\'язати головоломку</string>
     <string name="captcha_hint">Уведіть текст із зображення вище</string>
     <string name="certificate_chain_is_not_trusted">Ланцюжок сертифікатів не довірений</string>
@@ -571,7 +571,7 @@
     <string name="pref_delete_omemo_identities">Вилучити ідентифікаційні дані OMEMO</string>
     <string name="pref_delete_omemo_identities_summary">Створити наново Ваші ключі OMEMO. Всі Ваші контакти будуть змушені підтвердити Вас знову. Використовуйте це, лише якщо немає іншого вибору.</string>
     <string name="delete_selected_keys">Вилучити вибрані ключі</string>
-    <string name="error_publish_avatar_offline">Потрібно підключення, щоб можна було опублікувати піктограму користувача.</string>
+    <string name="error_publish_avatar_offline">Потрібно підключення, щоб можна було опублікувати аватар.</string>
     <string name="show_error_message">Показати повідомлення про помилку</string>
     <string name="error_message">Повідомлення про помилку</string>
     <string name="data_saver_enabled">Увімкнено заощадження трафіку</string>
@@ -668,8 +668,6 @@
     <string name="message_copied_to_clipboard">Повідомлення скопійовано</string>
     <string name="message">Повідомлення</string>
     <string name="private_messages_are_disabled">Приватні повідомлення вимкнено</string>
-    <string name="huawei_protected_apps">Захищені програми</string>
-    <string name="huawei_protected_apps_summary">Щоб отримувати сповіщення навіть коли екран погас, необхідно додати Conversations до списку захищених програм.</string>
     <string name="mtm_accept_cert">Прийняти незнайомий сертифікат?</string>
     <string name="mtm_trust_anchor">Сертифікат сервера не підтверджено відомим центром сертифікації.</string>
     <string name="mtm_accept_servername">Прийняти сервер з невідповідним ім\'ям?</string>
@@ -721,13 +719,13 @@
     <string name="p1_s3_filetransfer">Доступ до файлів через HTTP для S3</string>
     <string name="pref_start_search">Безпосередній пошук</string>
     <string name="pref_start_search_summary">На екрані «Нова розмова» показувати клавіатуру та розміщувати курсор у полі пошуку</string>
-    <string name="group_chat_avatar">Піктограма групи</string>
-    <string name="host_does_not_support_group_chat_avatars">Сервер не підтримує піктограми груп</string>
-    <string name="only_the_owner_can_change_group_chat_avatar">Лише власник може змінити піктограму групи</string>
+    <string name="group_chat_avatar">Зображення групи</string>
+    <string name="host_does_not_support_group_chat_avatars">Сервер не підтримує зображень груп</string>
+    <string name="only_the_owner_can_change_group_chat_avatar">Лише власник може змінити зображення групи</string>
     <string name="contact_name">Ім\'я контакту</string>
     <string name="nickname">Прізвисько</string>
     <string name="group_chat_name">Ім\'я</string>
-    <string name="providing_a_name_is_optional">Назва не є обов\'язковою</string>
+    <string name="providing_a_name_is_optional">Ім\'я вказувати необов\'язково</string>
     <string name="create_dialog_group_chat_name">Назва групи</string>
     <string name="conference_destroyed">Цю групу закрито</string>
     <string name="unable_to_save_recording">Неможливо зберегти запис</string>
@@ -880,8 +878,8 @@
     <string name="rtp_state_incoming_video_call">Вхідний відеовиклик</string>
     <string name="rtp_state_connecting">З\'єднання</string>
     <string name="rtp_state_connected">З\'єднано</string>
-    <string name="rtp_state_accepting_call">Приймаю виклик</string>
-    <string name="rtp_state_ending_call">Завершую виклик</string>
+    <string name="rtp_state_accepting_call">Приймання виклику</string>
+    <string name="rtp_state_ending_call">Завершення виклику</string>
     <string name="answer_call">Відповісти</string>
     <string name="dismiss_call">Відхилити</string>
     <string name="rtp_state_finding_device">Пошук пристроїв</string>
@@ -1020,9 +1018,9 @@
     <string name="no_xmpp_adddress_found">Не знайдено адреси XMPP</string>
     <string name="no_account_deactivated">Немає (вимкнено)</string>
     <string name="decline">Відхилити</string>
-    <string name="your_avatar">Ваша піктограма</string>
-    <string name="avatar_for_x">Піктограма для %s</string>
-    <string name="delete_avatar">Вилучити піктограму</string>
+    <string name="your_avatar">Ваш аватар</string>
+    <string name="avatar_for_x">Аватар для %s</string>
+    <string name="delete_avatar">Вилучити аватар</string>
     <string name="unable_to_parse_invite">Не вдалося обробити запрошення</string>
     <string name="server_does_not_support_easy_onboarding_invites">Створення запрошень не підтримується сервером</string>
     <string name="account_registrations_are_not_supported">Реєстрація облікових записів не підтримується</string>
@@ -1117,4 +1115,24 @@
     <string name="pref_fullscreen_notification_summary">Показувати сповіщення про вхідні виклики на весь екран, коли пристрій заблоковано.</string>
     <string name="pref_backup_summary">Створити резервну копію, запланувати повторюване резервування</string>
     <string name="pref_create_backup_one_off_summary">Створити резервну копію</string>
+    <string name="allow_private_messages">Дозволити приватні повідомлення</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Ваш аватар. Торкніться, щоб вибрати новий аватар із галереї.</string>
+    <string name="could_not_disable_video">Не вдалося вимкнути відео.</string>
+    <string name="delete_pgp_key">Видалити ключ OpenPGP</string>
+    <string name="change_notification_settings">Змінити налаштування сповіщень</string>
+    <string name="edit_configuration">Змінити налаштування</string>
+    <string name="edit_nick">Змінити прізвисько</string>
+    <string name="edit_name_and_topic">Редагувати назву і тему</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Виклик здійснюється через навушник. Торкніться, щоб переключитися на динамік.</string>
+    <string name="call_is_using_earpiece">Виклик здійснюється через навушник.</string>
+    <string name="call_is_using_wired_headset">Виклик здійснюється за допомогою дротової гарнітури</string>
+    <string name="video_is_enabled_tap_to_disable">Відео ввімкнено. Торкніться, щоб вимкнути.</string>
+    <string name="video_is_disabled_tap_to_enable">Відео вимкнено. Торкніться, щоб увімкнути.</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Виклик здійснюється через динамік. Торкніться, щоб переключитися на навушник.</string>
+    <string name="call_is_using_speaker">Виклик здійснюється через динамік.</string>
+    <string name="call_is_using_bluetooth">Виклик здійснюється через Bluetooth.</string>
+    <string name="flip_camera">Розвернути камеру</string>
+    <string name="server_info_login_mechanism">Механізм авторизації</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
+    <string name="server_info_sasl2">XEP-0388: Розширюваний профіль SASL</string>
 </resources>

src/main/res/values-vi/strings.xml 🔗

@@ -660,8 +660,6 @@
     <string name="message_copied_to_clipboard">Đã chép tin nhắn vào clipboard</string>
     <string name="message">Tin nhắn</string>
     <string name="private_messages_are_disabled">Tin nhắn riêng tư bị tắt</string>
-    <string name="huawei_protected_apps">Ứng dụng được bảo vệ</string>
-    <string name="huawei_protected_apps_summary">Để tiếp tục nhận các thông báo, kể cả khi màn hình đã tắt, bạn cần thêm Conversations vào danh sách các ứng dụng được bảo vệ.</string>
     <string name="mtm_accept_cert">Chấp nhận chứng chỉ không xác định?</string>
     <string name="mtm_trust_anchor">Chứng chỉ máy chủ này không được một người có quyền chứng chỉ đã biết ký.</string>
     <string name="mtm_accept_servername">Chấp nhận tên máy chủ không khớp?</string>

src/main/res/values-zh-rCN/strings.xml 🔗

@@ -26,7 +26,7 @@
     <string name="minute_ago">1 分钟前</string>
     <string name="minutes_ago">%d 分钟前</string>
     <plurals name="x_unread_conversations">
-        <item quantity="other">%d 个未读聊天</item>
+        <item quantity="other">%d 个未读对话</item>
     </plurals>
     <string name="sending">正在发送…</string>
     <string name="message_decrypting">解密消息中。请稍候…</string>
@@ -38,7 +38,7 @@
     <string name="moderator">主持人</string>
     <string name="participant">参与者</string>
     <string name="visitor">访客</string>
-    <string name="remove_contact_text">是否从联系人列表中移除 %s?将不会移除与对方的聊天。</string>
+    <string name="remove_contact_text">是否从联系人列表中移除 %s?将不会移除与对方的对话。</string>
     <string name="block_contact_text">是否屏蔽 %s 向您发送消息?</string>
     <string name="unblock_contact_text">是否解除屏蔽 %s 并允许对方向您发送消息?</string>
     <string name="block_domain_text">屏蔽来自 %s 的所有联系人?</string>
@@ -62,12 +62,12 @@
     <string name="save">保存</string>
     <string name="ok">完成</string>
     <string name="crash_report_title">%1$s 已崩溃</string>
-    <string name="crash_report_message">使用 XMPP 账号发送堆栈跟踪有助于 %1$s 的持续开发。</string>
+    <string name="crash_report_message">使用您的 XMPP 账号发送崩溃报告有助于 %1$s 的持续开发。</string>
     <string name="send_now">立即发送</string>
     <string name="send_never">不再询问</string>
     <string name="problem_connecting_to_account">无法连接到账号</string>
     <string name="problem_connecting_to_accounts">无法连接到多个账号</string>
-    <string name="touch_to_fix">轻击即可管理账号</string>
+    <string name="touch_to_fix">点击以管理您的账号</string>
     <string name="attach_file">附加文件</string>
     <string name="not_in_roster">对方不在您的联系人列表中,是否添加?</string>
     <string name="add_contact">添加联系人</string>
@@ -75,9 +75,9 @@
     <string name="preparing_image">正在准备发送图片</string>
     <string name="preparing_images">正在准备发送图片</string>
     <string name="sharing_files_please_wait">分享文件中。请稍候…</string>
-    <string name="action_clear_history">清空历史记录</string>
+    <string name="action_clear_history">清空聊天记录</string>
     <string name="clear_conversation_history">清空聊天记录</string>
-    <string name="clear_histor_msg">是否要删除此聊天中的所有消息?
+    <string name="clear_histor_msg">是否要删除此对话中的所有消息?
 \n
 \n<b>警告:</b>存储在其他设备或服务器上的消息将不受影响。</string>
     <string name="delete_file_dialog">删除文件</string>
@@ -85,12 +85,12 @@
 \n
 \n<b>警告:</b>存储在其他设备或服务器上此文件的副本将不会删除。 </string>
     <string name="choose_presence">选择设备</string>
-    <string name="send_unencrypted_message">发送明文消息</string>
+    <string name="send_unencrypted_message">发送未加密消息</string>
     <string name="send_message">发送消息</string>
     <string name="send_message_to_x">发送消息至 %s</string>
     <string name="send_omemo_x509_message">发送 v\\OMEMO 加密消息</string>
     <string name="your_nick_has_been_changed">正在使用新昵称</string>
-    <string name="send_unencrypted">发送明文</string>
+    <string name="send_unencrypted">发送未加密</string>
     <string name="decryption_failed">解密失败。也许您没有正确的私钥。</string>
     <string name="openkeychain_required">OpenKeychain</string>
     <string name="openkeychain_required_long">%1$s 使用 &lt;b&gt;OpenKeychain&lt;/b&gt; 来加密和解密消息并管理公钥。&lt;br&gt;&lt;br&gt;它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。&lt;br&gt;&lt;br&gt;&lt;small&gt;(请之后重启 %1$s。)&lt;/small&gt;</string>
@@ -123,7 +123,7 @@
     <string name="pref_notification_grace_period">静默期</string>
     <string name="pref_notification_grace_period_summary">在其他设备上检测到活动之后,通知在此期间静音。</string>
     <string name="pref_advanced_options">高级</string>
-    <string name="pref_never_send_crash_summary">通过发送堆栈跟踪,您正在帮助此应用的开发</string>
+    <string name="pref_never_send_crash_summary">通过发送崩溃报告,您可以帮助此应用的进一步开发</string>
     <string name="pref_confirm_messages">确认消息</string>
     <string name="pref_confirm_messages_summary">让您的联系人知道您已收到并阅读了其消息</string>
     <string name="pref_prevent_screenshots">防止截屏</string>
@@ -186,7 +186,7 @@
     <string name="attach_record_voice">录制语音</string>
     <string name="account_settings_jabber_id">XMPP 地址</string>
     <string name="block_jabber_id">屏蔽 XMPP 地址</string>
-    <string name="account_settings_example_jabber_id">username@example.com</string>
+    <string name="account_settings_example_jabber_id">用户名@example.com</string>
     <string name="password">密码</string>
     <string name="invalid_jid">此 XMPP 地址无效</string>
     <string name="error_out_of_memory">空间不足。图片太大</string>
@@ -234,7 +234,7 @@
     <string name="select">选择</string>
     <string name="contact_already_exists">此联系人已存在</string>
     <string name="join">加入</string>
-    <string name="channel_full_jid_example">channel@conference.example.com/nick</string>
+    <string name="channel_full_jid_example">channel@conference.example.com/昵称</string>
     <string name="channel_bare_jid_example">channel@conference.example.com</string>
     <string name="save_as_bookmark">保存为书签</string>
     <string name="delete_bookmark">删除书签</string>
@@ -259,7 +259,7 @@
     <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s 和其他 %2$d 人已阅读至此</string>
     <string name="everyone_has_read_up_to_this_point">每个人都已阅读至此</string>
     <string name="publish">发布</string>
-    <string name="touch_to_choose_picture">轻击头像即可从图库中选择图片</string>
+    <string name="touch_to_choose_picture">点击头像从图库中选择图片</string>
     <string name="publishing">正在发布…</string>
     <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布</string>
     <string name="error_publish_avatar_converting">无法转换图片</string>
@@ -276,7 +276,7 @@
     <string name="skip">跳过</string>
     <string name="disable_notifications">禁用通知</string>
     <string name="enable">启用</string>
-    <string name="conference_requires_password">需要密码才能进入此群聊</string>
+    <string name="conference_requires_password">进入群聊需要输入密码</string>
     <string name="enter_password">输入密码</string>
     <string name="request_presence_updates">请先向您的联系人请求在线状态更新。
 \n
@@ -299,12 +299,12 @@
     <string name="pref_quiet_hours_summary">通知将在免打扰时间内静音</string>
     <string name="pref_expert_options_other">其他</string>
     <string name="pref_autojoin">同步书签</string>
-    <string name="pref_autojoin_summary">在进入或离开多用户聊天时设置“自动加入”标志,并回应其他客户端所做的改变。</string>
+    <string name="pref_autojoin_summary">在进入或离开群聊时设置“自动加入”标志,并回应其他客户端所做的改变。</string>
     <string name="toast_message_omemo_fingerprint">OMEMO 指纹已复制到剪贴板</string>
-    <string name="conference_banned">此群聊已将您封禁了</string>
+    <string name="conference_banned">禁止您进入此群聊</string>
     <string name="conference_members_only">此群聊仅成员进入</string>
     <string name="conference_resource_constraint">资源限制</string>
-    <string name="conference_kicked">此群聊已将您踢出了</string>
+    <string name="conference_kicked">已从群聊中踢出了您</string>
     <string name="conference_shutdown">此群聊已关闭</string>
     <string name="conference_unknown_error">您已不在群聊中</string>
     <string name="conference_technical_problems">由于技术原因,您离开了群聊</string>
@@ -357,7 +357,7 @@
     <string name="no_application_found_to_open_link">未找到可以打开链接的应用</string>
     <string name="no_application_found_to_view_contact">未找到可以查看联系人的应用</string>
     <string name="pref_show_dynamic_tags">动态标签</string>
-    <string name="pref_show_dynamic_tags_summary">在联系人下方显示只读标签</string>
+    <string name="pref_show_dynamic_tags_summary">在联系人下方显示动态标签汇总</string>
     <string name="enable_notifications">启用通知</string>
     <string name="no_conference_server_found">群聊服务器未找到</string>
     <string name="conference_creation_failed">无法创建群聊</string>
@@ -371,8 +371,8 @@
     <string name="error_no_keys_to_trust_presence">此联系人没有可用的密钥。
 \n请确保双方都有在线状态订阅。</string>
     <string name="error_trustkeys_title">出了点问题</string>
-    <string name="fetching_history_from_server">正在从服务器获取历史记录</string>
-    <string name="no_more_history_on_server">服务器上没有更多历史记录</string>
+    <string name="fetching_history_from_server">正在从服务器获取聊天记录</string>
+    <string name="no_more_history_on_server">服务器上没有更多聊天记录</string>
     <string name="updating">正在更新…</string>
     <string name="password_changed">密码已修改!</string>
     <string name="could_not_change_password">无法修改密码</string>
@@ -399,17 +399,17 @@
     <string name="could_not_change_affiliation">无法更改 %s 的从属关系</string>
     <string name="ban_from_conference">从群聊中封禁</string>
     <string name="ban_from_channel">从频道中封禁</string>
-    <string name="removing_from_public_conference">您正试图从公开频道中移除 %s。唯一的办法就是永远封禁此用户。</string>
+    <string name="removing_from_public_conference">您正试图从公开频道中移除 %s。唯一的办法就是永远封禁该用户。</string>
     <string name="ban_now">立即封禁</string>
     <string name="could_not_change_role">无法更改 %s 的角色</string>
     <string name="conference_options">私人群聊配置</string>
     <string name="channel_options">公开频道配置</string>
     <string name="members_only">私人,仅成员进入</string>
-    <string name="non_anonymous">使 XMPP 地址对任何人可见</string>
-    <string name="moderated">对频道进行审核</string>
+    <string name="non_anonymous">公开用户的 XMPP 地址</string>
+    <string name="moderated">开启频道发言审核</string>
     <string name="you_are_not_participating">您未参与</string>
-    <string name="modified_conference_options">群聊选项修改成功!</string>
-    <string name="could_not_modify_conference_options">无法修改群聊选项</string>
+    <string name="modified_conference_options">群聊配置修改成功!</string>
+    <string name="could_not_modify_conference_options">无法修改群聊配置</string>
     <string name="never">从不</string>
     <string name="until_further_notice">直至另行通知</string>
     <string name="snooze">稍后提醒</string>
@@ -685,8 +685,6 @@
     <string name="message_copied_to_clipboard">消息已复制到剪贴板</string>
     <string name="message">消息</string>
     <string name="private_messages_are_disabled">已禁用私信</string>
-    <string name="huawei_protected_apps">受保护的应用</string>
-    <string name="huawei_protected_apps_summary">为了在屏幕关闭时也能收到消息提醒,您需要将 Conversations 加入受保护的应用列表。</string>
     <string name="mtm_accept_cert">接受未知证书?</string>
     <string name="mtm_trust_anchor">此服务器证书不是由已知的证书颁发机构签发的。</string>
     <string name="mtm_accept_servername">接受不匹配的服务器名称?</string>
@@ -704,14 +702,14 @@
     <string name="error_trustkey_device_list">无法获取设备列表</string>
     <string name="error_trustkey_bundle">无法获取密钥</string>
     <string name="error_trustkey_hint_mutual">提示:某些情况下,双方可以添加到联系人列表解决此问题。</string>
-    <string name="disable_encryption_message">是否确定要禁用此聊天的 OMEMO 加密?
+    <string name="disable_encryption_message">是否确定要禁用此对话的 OMEMO 加密?
 \n将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方法。</string>
     <string name="disable_now">立即禁用</string>
     <string name="draft">草稿:</string>
     <string name="pref_omemo_setting">OMEMO 加密</string>
     <string name="pref_omemo_setting_summary_always">OMEMO 将始终用于一对一和私人群聊。</string>
-    <string name="pref_omemo_setting_summary_default_on">新聊天将默认使用 OMEMO。</string>
-    <string name="pref_omemo_setting_summary_default_off">新聊天必须明确开启 OMEMO。</string>
+    <string name="pref_omemo_setting_summary_default_on">新对话将默认使用 OMEMO。</string>
+    <string name="pref_omemo_setting_summary_default_off">新对话必须明确开启 OMEMO。</string>
     <string name="create_shortcut">创建快捷方式</string>
     <string name="default_on">默认开启</string>
     <string name="default_off">默认关闭</string>
@@ -732,14 +730,14 @@
     <string name="no_microphone_permission">授予 %1$s 访问麦克风的权限</string>
     <string name="search_messages">搜索消息</string>
     <string name="gif">GIF</string>
-    <string name="view_conversation">查看聊天</string>
+    <string name="view_conversation">查看对话</string>
     <string name="pref_use_share_location_plugin">位置共享插件</string>
     <string name="pref_use_share_location_plugin_summary">使用位置共享插件代替内置地图</string>
     <string name="copy_link">复制 web 地址</string>
     <string name="copy_jabber_id">复制 XMPP 地址</string>
     <string name="p1_s3_filetransfer">用于 S3 的 HTTP 文件共享</string>
     <string name="pref_start_search">直接搜索</string>
-    <string name="pref_start_search_summary">在“新聊天”屏幕上打开键盘并将光标放在搜索栏中</string>
+    <string name="pref_start_search_summary">在“新对话”屏幕上打开键盘并将光标放在搜索栏中</string>
     <string name="group_chat_avatar">群聊头像</string>
     <string name="host_does_not_support_group_chat_avatars">主机不支持群聊头像</string>
     <string name="only_the_owner_can_change_group_chat_avatar">只有所有者才能更改群聊头像</string>
@@ -748,7 +746,7 @@
     <string name="group_chat_name">名称</string>
     <string name="providing_a_name_is_optional">提供名称是可选的</string>
     <string name="create_dialog_group_chat_name">群聊名</string>
-    <string name="conference_destroyed">已解散此群聊</string>
+    <string name="conference_destroyed">此群聊已经解散了</string>
     <string name="unable_to_save_recording">无法保存录制</string>
     <string name="foreground_service_channel_name">前台服务</string>
     <string name="foreground_service_channel_description">此通知类别用于显示永久通知,表明 %1$s 正在运行。</string>
@@ -784,7 +782,7 @@
     <string name="phone_number">电话号码</string>
     <string name="verify_your_phone_number">验证您的电话号码</string>
     <string name="enter_country_code_and_phone_number">Quicksy 将发送短信(运营商可能收费)来验证电话号码。请输入您的国家/地区代码和电话号码:</string>
-    <string name="we_will_be_verifying">我们将验证这个电话号码<br/><br/><b>%s</b><br/><br/>可以吗?是否编辑号码?</string>
+    <string name="we_will_be_verifying">我们将验证这个电话号码 <br/><br/><b>%s</b><br/><br/> 可以吗?是否编辑号码?</string>
     <string name="not_a_valid_phone_number">%s 不是有效的电话号码。</string>
     <string name="please_enter_your_phone_number">请输入您的电话号码。</string>
     <string name="search_countries">搜索国家/地区</string>
@@ -855,16 +853,16 @@
     <string name="channel_already_exists">此频道已存在</string>
     <string name="joined_an_existing_channel">您已加入现有的频道</string>
     <string name="unable_to_set_channel_configuration">无法保存频道配置</string>
-    <string name="allow_participants_to_edit_subject">允许任何人编辑话题</string>
-    <string name="allow_participants_to_invite_others">允许任何人邀请别人</string>
-    <string name="anyone_can_edit_subject">任何人可以编辑话题。</string>
+    <string name="allow_participants_to_edit_subject">允许参与者(非访客)编辑话题</string>
+    <string name="allow_participants_to_invite_others">允许任何参与者邀请其他用户</string>
+    <string name="anyone_can_edit_subject">参与者(非访客)可以编辑话题。</string>
     <string name="owners_can_edit_subject">所有者可以编辑话题。</string>
     <string name="admins_can_edit_subject">管理员可以编辑话题。</string>
-    <string name="owners_can_invite_others">所有者可以邀请别人。</string>
-    <string name="anyone_can_invite_others">任何人可以邀请别人。</string>
-    <string name="jabber_ids_are_visible_to_admins">XMPP 地址对管理员可见。</string>
-    <string name="jabber_ids_are_visible_to_anyone">XMPP 地址对任何人可见。</string>
-    <string name="no_users_hint_channel">此公开频道无参与者。邀请联系人或使用分享按钮分发其 XMPP 地址。</string>
+    <string name="owners_can_invite_others">所有者可以邀请其他用户。</string>
+    <string name="anyone_can_invite_others">任何参与者可以邀请其他用户。</string>
+    <string name="jabber_ids_are_visible_to_admins">用户的 XMPP 地址仅管理员可见。</string>
+    <string name="jabber_ids_are_visible_to_anyone">用户的 XMPP 地址对任何人可见。</string>
+    <string name="no_users_hint_channel">此公开频道无参与者。邀请联系人或使用分享按钮分发频道的 XMPP 地址。</string>
     <string name="no_users_hint_group_chat">此私人群聊无参与者。</string>
     <string name="manage_permission">管理权限</string>
     <string name="search_participants">搜索参与者</string>
@@ -904,11 +902,11 @@
     <string name="rtp_state_content_add">添加额外轨道?</string>
     <string name="rtp_state_connecting">正在连接</string>
     <string name="rtp_state_connected">已连接</string>
-    <string name="rtp_state_reconnecting">正在重连</string>
+    <string name="rtp_state_reconnecting">正在重新连接</string>
     <string name="rtp_state_accepting_call">正在接受通话</string>
     <string name="rtp_state_ending_call">正在结束通话</string>
-    <string name="answer_call">应答</string>
-    <string name="dismiss_call">忽略</string>
+    <string name="answer_call">接听</string>
+    <string name="dismiss_call">拒接</string>
     <string name="rtp_state_finding_device">正在发现设备</string>
     <string name="rtp_state_ringing">正在响铃</string>
     <string name="rtp_state_declined_or_busy">占线</string>
@@ -920,8 +918,8 @@
     <string name="hang_up">挂断</string>
     <string name="ongoing_call">正在进行的通话</string>
     <string name="ongoing_video_call">正在进行的视频通话</string>
-    <string name="reconnecting_call">正在重连通话</string>
-    <string name="reconnecting_video_call">正在重连视频通话</string>
+    <string name="reconnecting_call">正在重新连接通话</string>
+    <string name="reconnecting_video_call">正在重新连接视频通话</string>
     <string name="disable_tor_to_make_call">禁用 Tor 以进行通话</string>
     <string name="incoming_call">来电</string>
     <string name="missed_call_timestamp">未接来电 · %s</string>
@@ -947,8 +945,8 @@
     <string name="remove_from_favorites">取消置顶</string>
     <string name="gpx_track">GPX 轨迹</string>
     <string name="could_not_correct_message">无法更正消息</string>
-    <string name="search_all_conversations">所有聊天</string>
-    <string name="search_this_conversation">此聊天</string>
+    <string name="search_all_conversations">所有对话</string>
+    <string name="search_this_conversation">此对话</string>
     <string name="your_avatar">您的头像</string>
     <string name="avatar_for_x">%s 的头像</string>
     <string name="encrypted_with_omemo">用 OMEMO 加密</string>
@@ -1015,25 +1013,25 @@
     <string name="rtp_state_contact_offline">联系人不可用</string>
     <string name="no_permission_to_place_call">没有拨打电话的权限</string>
     <string name="call_integration_not_available">呼叫集成不可用!</string>
-    <string name="remove_bookmark_and_close">是否移除 %s 的书签并存档聊天?</string>
+    <string name="remove_bookmark_and_close">是否移除 %s 的书签并存档对话?</string>
     <string name="remove_bookmark">是否移除 %s 的书签?</string>
-    <string name="delete_and_close">删除并存档聊天</string>
+    <string name="delete_and_close">删除并存档对话</string>
     <string name="channel_discover_opt_in_message">频道发现使用称为 &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt; 的第三方服务。&lt;br&gt;&lt;br&gt;使用此功能会将您的 IP 地址和搜索词传输到此服务。请参阅其 &lt;a href=https://search.jabber.network/privacy&gt;隐私政策&lt;/a&gt; 以获取更多信息。</string>
-    <string name="start_chat">开始聊天</string>
+    <string name="start_chat">开始对话</string>
     <string name="title_activity_share_with">分享至…</string>
     <string name="pref_use_colorful_bubbles_summary">为已发送和已接收消息使用不同的背景颜色</string>
     <string name="no_certificate_selected">未选择客户端证书!</string>
     <string name="pref_use_colorful_bubbles">彩色聊天气泡</string>
     <string name="pref_dynamic_colors">动态色彩</string>
     <string name="pref_dynamic_colors_summary">系统色彩 (Material You)</string>
-    <string name="title_activity_new_chat">新聊天</string>
-    <string name="archive_this_chat">之后删除聊天</string>
-    <string name="title_undo_swipe_out_chat">聊天已存档</string>
-    <string name="switch_to_chat">切换到聊天</string>
-    <string name="action_archive_chat">存档聊天</string>
-    <string name="barcode_does_not_contain_fingerprints_for_this_chat">二维码不包含此聊天的指纹。</string>
+    <string name="title_activity_new_chat">新对话</string>
+    <string name="archive_this_chat">之后删除对话</string>
+    <string name="title_undo_swipe_out_chat">对话已存档</string>
+    <string name="switch_to_chat">切换到对话</string>
+    <string name="action_archive_chat">存档对话</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">二维码不包含此对话的指纹。</string>
     <string name="welcome_header">加入对话</string>
-    <string name="corresponding_chats_closed">相应的聊天已存档。</string>
+    <string name="corresponding_chats_closed">相应的对话已存档。</string>
     <string name="pref_send_crash_reports">发送崩溃报告</string>
     <string name="pref_title_security">安全</string>
     <string name="notifications">通知</string>
@@ -1074,5 +1072,25 @@
     <string name="pref_fullscreen_notification">全屏通知</string>
     <string name="pref_fullscreen_notification_summary">当设备锁定时,允许此应用显示占据全屏的来电通知。</string>
     <string name="unsupported_operation">不支持的操作</string>
-    <string name="pref_backup_summary">创建一次性、计划定期备份</string>
+    <string name="pref_backup_summary">创建一次性备份、计划定期备份</string>
+    <string name="allow_private_messages">允许私信</string>
+    <string name="edit_nick">编辑昵称</string>
+    <string name="your_avatar_tap_to_select_new_avatar">您的头像。点击从图库中选择新头像。</string>
+    <string name="could_not_disable_video">无法禁用视频。</string>
+    <string name="delete_pgp_key">删除 OpenPGP 密钥</string>
+    <string name="edit_name_and_topic">编辑名称和话题</string>
+    <string name="edit_configuration">更改配置</string>
+    <string name="change_notification_settings">更改通知设置</string>
+    <string name="call_is_using_earpiece">正在使用听筒进行通话。</string>
+    <string name="call_is_using_wired_headset">正在使用有线耳机进行通话</string>
+    <string name="call_is_using_speaker">正在使用扬声器进行通话。</string>
+    <string name="call_is_using_bluetooth">正在使用蓝牙进行通话。</string>
+    <string name="flip_camera">翻转摄像头</string>
+    <string name="video_is_disabled_tap_to_enable">视频已禁用,点击以启用。</string>
+    <string name="video_is_enabled_tap_to_disable">视频已启用,点击以禁用。</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">正在使用扬声器进行通话,点击切换到听筒。</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">正在使用听筒进行通话,点击切换到扬声器。</string>
+    <string name="server_info_login_mechanism">登录机制</string>
+    <string name="server_info_bind2">XEP-0386:绑定 2</string>
+    <string name="server_info_sasl2">XEP-0388:可扩展 SASL 配置文件</string>
 </resources>

src/main/res/values-zh-rTW/strings.xml 🔗

@@ -620,7 +620,6 @@
     <string name="message_copied_to_clipboard">訊息已複製到剪貼簿</string>
     <string name="message">訊息</string>
     <string name="private_messages_are_disabled">私密訊息已停用</string>
-    <string name="huawei_protected_apps">受保護的應用程式</string>
     <string name="mtm_accept_cert">接受未知憑證?</string>
     <string name="mtm_trust_anchor">伺服器憑證是由不明的憑證簽發單位所簽署。</string>
     <string name="mtm_accept_servername">接受不相符的伺服器名稱?</string>
@@ -918,7 +917,6 @@
     <string name="we_will_be_verifying">我們將驗證 <br/><br/><b>%s</b><br/><br/> 電話號碼是否正確,或者您想編輯這個號碼嗎?</string>
     <string name="distrust_omemo_key_text">您確定要移除此裝置的驗證嗎?
 \n此裝置和來自此裝置的訊息將被標示為「未受信任」。</string>
-    <string name="huawei_protected_apps_summary">若要在螢幕關閉時保持接收通知,您需要將 Conversations 加入受保護的應用程式清單。</string>
     <string name="mtm_hostname_mismatch">由於「%s」,伺服器無法驗證,憑證僅對此有效:</string>
     <string name="verifying_omemo_keys_trusted_source_account">您將要驗證您自己帳戶的 OMEMO 金鑰。僅有從可信來源 (僅有您能夠發布此連結) 跟隨此連結時才是安全的。</string>
     <string name="silent_messages_channel_description">此通知群組用於顯示不應觸發任何音效的通知,例如在另一個裝置上啟用時 (寬限期)。</string>

src/main/res/values/arrays.xml 🔗

@@ -1,24 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
-	<string-array name="filesizes">
-		<item>@string/never</item>
-		<item>256 KiB</item>
-		<item>512 KiB</item>
-		<item>1 MiB</item>
-		<item>3.5 MiB</item>
-		<item>5 MiB</item>
-		<item>10 MiB</item>
-	</string-array>
-	<string-array name="filesizes_values">
+	<integer-array name="file_size_values">
 		<item>0</item>
-		<item>262144</item>
 		<item>524288</item>
 		<item>1048576</item>
 		<item>3490000</item>
 		<item>5242880</item>
 		<item>10485760</item>
-	</string-array>
+		<item>52428800</item>
+	</integer-array>
 
 	<integer-array name="mute_options_durations">
 		<item>1800</item>

src/main/res/values/dimens.xml 🔗

@@ -38,8 +38,6 @@
     <dimen name="scan_laser_width">4dp</dimen>
     <dimen name="scan_dot_size">8dp</dimen>
 
-    <dimen name="swipe_escape_velocity">1200dp</dimen> <!-- android default is 120dp -->
-
     <dimen name="background_image_opacity">0.12</dimen>
 
     <dimen name="sd_label_max_width">256dp</dimen>

src/main/res/values/strings.xml 🔗

@@ -203,6 +203,8 @@
     <string name="server_info_external_service_discovery">XEP-0215: External Service Discovery</string>
     <string name="server_info_pep">XEP-0163: PEP (Avatars / OMEMO)</string>
     <string name="server_info_http_upload">XEP-0363: HTTP File Upload</string>
+    <string name="server_info_bind2">XEP-0386: Bind 2</string>
+    <string name="server_info_sasl2">XEP-0388: Extensible SASL Profile</string>
     <string name="server_info_push">XEP-0357: Push</string>
     <string name="server_info_available">available</string>
     <string name="server_info_unavailable">unavailable</string>
@@ -695,8 +697,6 @@
     <string name="message_copied_to_clipboard">Message copied to clipboard</string>
     <string name="message">Message</string>
     <string name="private_messages_are_disabled">Private messages are disabled</string>
-    <string name="huawei_protected_apps">Protected Apps</string>
-    <string name="huawei_protected_apps_summary">To keep receiving notifications, even when the screen is turned off, you need to add Conversations to the list of protected apps.</string>
     <string name="mtm_accept_cert">Accept Unknown Certificate?</string>
     <string name="mtm_trust_anchor">The server certificate is not signed by a known Certificate Authority.</string>
     <string name="mtm_accept_servername">Accept Mismatching Server Name?</string>
@@ -974,6 +974,7 @@
     <string name="search_all_conversations">All chats</string>
     <string name="search_this_conversation">This chat</string>
     <string name="your_avatar">Your avatar</string>
+    <string name="your_avatar_tap_to_select_new_avatar">Your avatar. Tap to select new avatar from gallery.</string>
     <string name="avatar_for_x">Avatar for %s</string>
     <string name="encrypted_with_omemo">Encrypted with OMEMO</string>
     <string name="encrypted_with_openpgp">Encrypted with OpenPGP</string>
@@ -1000,6 +1001,7 @@
     <string name="no_active_accounts_support_this">No active accounts support this feature</string>
     <string name="backup_started_message">The backup has been started. You’ll get a notification once it has been completed.</string>
     <string name="unable_to_enable_video">Unable to enable video.</string>
+    <string name="could_not_disable_video">Could not disable video.</string>
     <string name="plain_text_document">Plain text document</string>
     <string name="account_registrations_are_not_supported">Account registrations are not supported</string>
     <string name="no_xmpp_adddress_found">No Jabber ID found</string>
@@ -1073,4 +1075,20 @@
     <string name="unsupported_operation">Unsupported operation</string>
     <string name="schedule_message">Send later</string>
     <string name="options">Options</string>
+    <string name="allow_private_messages">Allow private messages</string>
+    <string name="edit_nick">Edit nick</string>
+    <string name="delete_pgp_key">Delete OpenPGP key</string>
+    <string name="edit_name_and_topic">Edit name and topic</string>
+    <string name="edit_configuration">Change configuration</string>
+    <string name="change_notification_settings">Change notification settings</string>
+    <string name="call_is_using_earpiece_tap_to_switch_to_speaker">Call is using earpiece. Tap to switch to speaker.</string>
+    <string name="call_is_using_earpiece">Call is using earpiece.</string>
+    <string name="call_is_using_wired_headset">Call is using wired headset</string>
+    <string name="call_is_using_speaker_tap_to_switch_to_earpiece">Call is using speaker. Tap to switch to earpiece.</string>
+    <string name="call_is_using_speaker">Call is using speaker.</string>
+    <string name="call_is_using_bluetooth">Call is using bluetooth.</string>
+    <string name="flip_camera">Flip camera</string>
+    <string name="video_is_enabled_tap_to_disable">Video is enabled. Tap to disable.</string>
+    <string name="video_is_disabled_tap_to_enable">Video is disabled. Tap to enable.</string>
+    <string name="server_info_login_mechanism">Login mechanism</string>
 </resources>

src/main/res/xml/preferences_attachments.xml 🔗

@@ -22,8 +22,6 @@
     <PreferenceCategory android:title="@string/pref_category_receiving">
         <ListPreference
             android:defaultValue="@integer/auto_accept_filesize"
-            android:entries="@array/filesizes"
-            android:entryValues="@array/filesizes_values"
             android:icon="@drawable/ic_download_24dp"
             android:key="auto_accept_file_size"
             android:title="@string/pref_automatic_download"

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/fastlane/metadata/android/es-ES/full_description.txt 🔗

@@ -1,14 +1,14 @@
-Quicksy es un programa que se ejecuta en el popular cliente Jabber/XMPP, con descubrimiento de contactos automatizado.
+Quicksy es una bifurcación del popular cliente Jabber/XMPP Conversations con busqueda automático de contactos.
 
-Regístrese con su número de teléfono y Quicksy, según los números de teléfono de su agenda, sugerirá automáticamente contactos potenciales.
+Te registras con tu número de teléfono y Quicksy automáticamente, basándose en los números de teléfono de tu agenda, te sugerirá posibles contactos.
 
-En esencia, Quicksy es un cliente Jabber completo que le permite comunicarse con cualquier usuario en cualquier servidor público. De manera similar, se puede contactar a los usuarios de Quicksy desde el extranjero simplemente agregando +phonenumber@quicksy.im a su lista de contactos.
+Quicksy es un cliente Jabber completo que te permite comunicarte con cualquier usuario en cualquier servidor público. Asimismo, puedes contactar a los usuarios de Quicksy desde el exterior simplemente agregando +phonenumber@quicksy.im a tu lista de contactos.
 
-Elimine la sincronización de contactos; la interfaz de usuario se deja intencionalmente lo más cerca posible de Conversations. Esto permite a los usuarios migrar, si lo desean, de Quicksy a Conversations sin tener que volver a aprender cómo funciona la aplicación.
+Aparte de la sincronización de contactos, la interfaz de usuario se parece lo más posible a Conversations. Esto permite que los usuarios puedan migrar de Quicksy a Conversations sin tener que volver a aprender cómo funciona la aplicación.
 
-Los contactos sugeridos consisten en otros usuarios de Quicksy y usuarios habituales de Jabber/XMPP que han proporcionado su ID de Jabber a la Lista de Quicksy (https://quicksy.im/#get-listed).
+Los contactos sugeridos consisten en otros usuarios de Quicksy y usuarios habituales de Jabber/XMPP que han ingresado su ID de Jabber en el Directorio de Quicksy (https://quicksy.im/#get-listed).
 
-NOTA: Para proporcionar (https://quicksy.im/enter/) su ID de Jabber a la Lista
-Quicksy requiere una tarifa de registro aplicable única.
+NOTA: Para ingresar (https://quicksy.im/enter/) tu ID de Jabber en Quicksy
+Directorio se requiere un único pago de registro.
 
-Para más detalles, lea la Política de Privacidad (https://quicksy.im/#privacy).
+Lee la Política de privacidad (https://quicksy.im/#privacy) para obtener más información.

src/quicksy/fastlane/metadata/android/fr-FR/full_description.txt 🔗

@@ -0,0 +1,14 @@
+Quicksy est un spin-off du client populaire Jabber/XMPP Conversations avec découverte automatique des contacts.
+
+Vous vous inscrivez avec votre numéro de téléphone et Quicksy va automatiquement — en se basant sur les numéros de votre carnet de contacts — vous suggérer des contacts.
+
+Sous le capot Quicksy est un client Jabber à part entière qui vous permet de communiquer avec n'importe quel utilisateur·ice sur n'importe quel serveur public fédéré. De même, les utilisateur·ices de Quicksy peuvent être contactés de l'extérieur simplement en ajoutant +numéro-de-téléphone@quicksy.im à votre liste de contacts.
+
+Outre la synchronisation des contacts, l'interface utilisateur·ice est délibérément aussi proche que possible de Conversations. Cela permet aux utilisateur·ices de migrer éventuellement de Quicksy vers Conversations sans avoir à réapprendre le fonctionnement de l'application.
+
+Les contacts suggérés inclus d'autres utilisateur·ices Quicksy et des utilisateur·ices Jabber/XMPP classiques qui ont entré leur identifiant Jabber dans le répertoire Quicksy (https://quicksy.im/#get-listed).
+
+NOTE : Pour ajouter (https://quicksy.im/enter/) votre identifiant Jabber dans le répertoire
+Quicksy des frais d'inscription uniques sont requis.
+
+Lisez la politique de confidentialité (https://quicksy.im/#privacy) pour plus d'information.

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

@@ -5,16 +5,36 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.preference.PreferenceManager;
 import android.util.Log;
 
 import com.google.common.collect.ImmutableMap;
 
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
+import eu.siacs.conversations.crypto.TrustManagers;
+import eu.siacs.conversations.crypto.sasl.Plain;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Entry;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.utils.SmsRetrieverWrapper;
+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 im.conversations.android.xmpp.model.stanza.Iq;
+
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -38,7 +58,6 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.UUID;
 import java.util.WeakHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -52,26 +71,6 @@ import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.android.PhoneNumberContact;
-import eu.siacs.conversations.crypto.TrustManagers;
-import eu.siacs.conversations.crypto.sasl.Plain;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Entry;
-import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.utils.AccountUtils;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
-import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.SmsRetrieverWrapper;
-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 io.michaelrocks.libphonenumber.android.Phonenumber;
-
 public class QuickConversationsService extends AbstractQuickConversationsService {
 
 
@@ -88,8 +87,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
 
     private static final String BASE_URL = "https://" + API_DOMAIN;
 
-    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
-
     private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
     private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 
@@ -308,16 +305,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
     }
 
     private String getInstallationId() {
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
-        String id = preferences.getString(INSTALLATION_ID, null);
-        if (id != null) {
-            return id;
-        } else {
-            id = UUID.randomUUID().toString();
-            preferences.edit().putString(INSTALLATION_ID, id).apply();
-            return id;
-        }
-
+        final var appSettings = service.getAppSettings();
+        final long installationId = appSettings.getInstallationId();
+        return AccountUtils.createUuid4(installationId, installationId).toString();
     }
 
     private int getApiErrorCode(final Exception e) {
@@ -463,15 +453,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 +488,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");

src/quicksy/res/values-ar/strings.xml 🔗

@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pref_notification_grace_period_summary">مدى الوقت الذي يظل فيه Quicksy هادئًا بعد رؤية نشاط على جهاز آخر</string>
-    <string name="pref_never_send_crash_summary">عبر إرسال الأخطاء انت تقوم بالمساعدة في تطوير برمجة Quicksy</string>
+    <string name="pref_notification_grace_period_summary">طول الفترة الزمنية التي يظل فيها Quicksy هادئًا بعد رؤية النشاط على جهاز آخر</string>
+    <string name="pref_never_send_crash_summary">بإرسال تتبعات المكدس، فإنك تساعد في التطوير المستمر لـ Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي</string>
     <string name="huawei_protected_apps_summary">للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف Quicksy إلى قائمة التطبيقات المحميّة.</string>
-    <string name="set_profile_picture">صورة حساب Quicksy</string>
+    <string name="set_profile_picture">صورة الملف الشخصي بسرعة Quicksy</string>
     <string name="not_available_in_your_country">إن كويكسي Quicksy غير متوفر في بلدكم.</string>
     <string name="unable_to_verify_server_identity">لا  يمكن التأكد من خادم الهويّة.</string>
     <string name="unknown_security_error">خطأ أمني مجهول.</string>

src/quicksy/res/values-gl/strings.xml 🔗

@@ -4,7 +4,7 @@
     <string name="pref_never_send_crash_summary">Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Permitir a todos os teus contactos saber cando estás a utilizar Quicksy</string>
     <string name="huawei_protected_apps_summary">Para seguir recibindo notificacións, mesmo coa pantalla apagada, tes que engadir a Quicksy á lista de apps protexidas.</string>
-    <string name="set_profile_picture">Imaxe de perfil Quicksy</string>
+    <string name="set_profile_picture">Imaxe de perfil en Quicksy</string>
     <string name="not_available_in_your_country">Quicksy non está dispoñible no teu país.</string>
     <string name="unable_to_verify_server_identity">Non se puido verificar a identidade do servidor.</string>
     <string name="unknown_security_error">Fallo de seguridade descoñecido.</string>

src/quicksy/res/values-nl/strings.xml 🔗

@@ -4,9 +4,9 @@
     <string name="pref_never_send_crash_summary">Door crashrapportages te versturen help je de ontwikkeling van Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Laat al je contactpersonen weten wanneer je Quicksy gebruikt</string>
     <string name="huawei_protected_apps_summary">Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Quicksy toevoegen aan de lijst met beschermde apps.</string>
-    <string name="set_profile_picture">Quicksy-profielafbeelding</string>
+    <string name="set_profile_picture">Quicksy profielafbeelding</string>
     <string name="not_available_in_your_country">Quicksy is niet beschikbaar in je land.</string>
     <string name="unable_to_verify_server_identity">Kan serveridentiteit niet verifiëren.</string>
     <string name="unknown_security_error">Onbekende beveiligingsfout.</string>
     <string name="timeout_while_connecting_to_server">Time-out bij verbinden met server.</string>
-</resources>
+</resources>

src/quicksy/res/values-ro-rRO/strings.xml 🔗

@@ -4,9 +4,9 @@
     <string name="pref_never_send_crash_summary">Trimițând datele despre erori ajutați la continuarea dezvoltării aplicației Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Contactele vă sunt anunțate atunci când folosiți Quicksy</string>
     <string name="huawei_protected_apps_summary">Pentru a continua să primiți notificări, chiar și când ecranul este oprit, trebuie să adăugați Quicksy în lista de aplicații protejate.</string>
-    <string name="set_profile_picture">Poză profil Quicksy</string>
+    <string name="set_profile_picture">Poză de profil Quicksy</string>
     <string name="not_available_in_your_country">Quicksy nu este disponibilă în țara dumneavoastră.</string>
     <string name="unable_to_verify_server_identity">Nu s-a putut verifica identitatea serverului.</string>
     <string name="unknown_security_error">Eroare de securitate necunoscută.</string>
     <string name="timeout_while_connecting_to_server">A expirat timpul de așteptare conexiune server.</string>
-</resources>
+</resources>

src/quicksy/res/values-zh-rCN/strings.xml 🔗

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pref_notification_grace_period_summary">发现另一台设备上的活动后,Quicksy 在此期间保持安静</string>
-    <string name="pref_never_send_crash_summary">通过发送堆栈跟踪,您正在帮助 Quicksy 的持续开发</string>
+    <string name="pref_notification_grace_period_summary">发现另一台设备上的活动后,Quicksy 在此期间保持静音</string>
+    <string name="pref_never_send_crash_summary">通过发送崩溃报告,您可以帮助 Quicksy 的持续开发</string>
     <string name="pref_broadcast_last_activity_summary">让您的所有联系人知道您最后使用 Quicksy 的时间</string>
-    <string name="huawei_protected_apps_summary">为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。</string>
+    <string name="huawei_protected_apps_summary">为了在屏幕关闭后继续接收通知,您需要将 Quicksy 加入受保护的应用列表。</string>
     <string name="set_profile_picture">Quicksy 个人资料图片</string>
     <string name="not_available_in_your_country">Quicksy 在您所在的国家/地区无法使用。</string>
     <string name="unable_to_verify_server_identity">无法验证服务器身份。</string>