CHANGELOG.md 🔗
@@ -1,5 +1,9 @@
# Changelog
+### Version 2.19.0
+
+* Upload and show high resolution avatars (profile pictures) on servers that have support for them
+
### Version 2.18.2
* Support 'Service Outage Status'
Stephen Paul Weber created
* commit 'a2378805932c94c140deb73d3a21884ecd8ce6fa': (87 commits)
version bump to 2.19.0-beta
default to bubbles start align on tablets
make enter phone number screen work on small screens
use 'safe' aac encoding by default instead of maintainig list
Translated using Weblate (Hungarian)
cache vCard exceptions
move most MUC handling into Manager
move non-muc presence sending into PresenceManager
put offline messages purge into manager
Add Samsung Galaxy S9+ and Google Pixel 2 to the list of AAC sensitive devices
Translated using Weblate (Portuguese)
Translated using Weblate (Portuguese)
Translated using Weblate (Portuguese)
Translated using Weblate (Japanese)
Translated using Weblate (Portuguese)
Translated using Weblate (Japanese)
Translated using Weblate (Spanish)
Translated using Weblate (Italian)
Translated using Weblate (Kabyle)
Translated using Weblate (Portuguese (Brazil))
...
CHANGELOG.md | 4
build.gradle | 8
fastlane/metadata/android/es-ES/changelogs/4213904.txt | 2
fastlane/metadata/android/es-ES/changelogs/4214004.txt | 2
fastlane/metadata/android/es-ES/changelogs/4214204.txt | 2
fastlane/metadata/android/et/changelogs/4209204.txt | 2
fastlane/metadata/android/uk/changelogs/395.txt | 2
src/cheogram/java/com/cheogram/android/BobTransfer.java | 3
src/cheogram/java/com/cheogram/android/FinishOnboarding.java | 2
src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java | 5
src/conversations/fastlane/metadata/android/es-ES/full_description.txt | 34
src/conversations/res/drawable/ic_app_icon_notification.xml | 10
src/conversations/res/drawable/ic_launcher_foreground.xml | 16
src/conversations/res/drawable/ic_launcher_monochrome.xml | 24
src/conversations/res/values-pt/strings.xml | 16
src/main/AndroidManifest.xml | 4
src/main/java/de/gultsch/common/FutureMerger.java | 42
src/main/java/de/gultsch/common/IntMap.java | 100
src/main/java/eu/siacs/conversations/AppSettings.java | 58
src/main/java/eu/siacs/conversations/Config.java | 13
src/main/java/eu/siacs/conversations/android/Device.java | 80
src/main/java/eu/siacs/conversations/entities/Account.java | 80
src/main/java/eu/siacs/conversations/entities/Blockable.java | 16
src/main/java/eu/siacs/conversations/entities/Bookmark.java | 62
src/main/java/eu/siacs/conversations/entities/Contact.java | 38
src/main/java/eu/siacs/conversations/entities/Conversation.java | 3
src/main/java/eu/siacs/conversations/entities/MucOptions.java | 318
src/main/java/eu/siacs/conversations/entities/RawBlockable.java | 4
src/main/java/eu/siacs/conversations/entities/Roster.java | 95
src/main/java/eu/siacs/conversations/entities/Transferable.java | 31
src/main/java/eu/siacs/conversations/generator/IqGenerator.java | 330
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java | 67
src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java | 47
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java | 308
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java | 16
src/main/java/eu/siacs/conversations/http/SlotRequester.java | 126
src/main/java/eu/siacs/conversations/parser/AbstractParser.java | 6
src/main/java/eu/siacs/conversations/parser/IqParser.java | 263
src/main/java/eu/siacs/conversations/parser/MessageParser.java | 328
src/main/java/eu/siacs/conversations/parser/PresenceParser.java | 107
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java | 24
src/main/java/eu/siacs/conversations/persistance/FileBackend.java | 301
src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java | 32
src/main/java/eu/siacs/conversations/services/AvatarService.java | 39
src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java | 9
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java | 15
src/main/java/eu/siacs/conversations/services/NotificationService.java | 12
src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java | 17
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 733
src/main/java/eu/siacs/conversations/ui/Activities.java | 9
src/main/java/eu/siacs/conversations/ui/BaseActivity.java | 4
src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java | 225
src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java | 8
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java | 228
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java | 128
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 70
src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java | 8
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java | 88
src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java | 3
src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java | 76
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java | 45
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java | 4
src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java | 47
src/main/java/eu/siacs/conversations/ui/XmppActivity.java | 180
src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java | 169
src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java | 5
src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java | 18
src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java | 101
src/main/java/eu/siacs/conversations/utils/Compatibility.java | 4
src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java | 51
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java | 28
src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java | 57
src/main/java/eu/siacs/conversations/utils/Resolver.java | 15
src/main/java/eu/siacs/conversations/utils/UIHelper.java | 51
src/main/java/eu/siacs/conversations/xml/Element.java | 11
src/main/java/eu/siacs/conversations/xml/Namespace.java | 14
src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java | 43
src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java | 33
src/main/java/eu/siacs/conversations/xmpp/Jid.java | 2
src/main/java/eu/siacs/conversations/xmpp/Managers.java | 43
src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java | 16
src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java | 19
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java | 588
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java | 2
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java | 9
src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java | 70
src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java | 61
src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java | 941
src/main/java/eu/siacs/conversations/xmpp/manager/AxolotlManager.java | 39
src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java | 229
src/main/java/eu/siacs/conversations/xmpp/manager/BookmarkManager.java | 176
src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java | 64
src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java | 44
src/main/java/eu/siacs/conversations/xmpp/manager/HttpUploadManager.java | 262
src/main/java/eu/siacs/conversations/xmpp/manager/LegacyBookmarkManager.java | 59
src/main/java/eu/siacs/conversations/xmpp/manager/MessageDisplayedSynchronizationManager.java | 93
src/main/java/eu/siacs/conversations/xmpp/manager/MultiUserChatManager.java | 1006
src/main/java/eu/siacs/conversations/xmpp/manager/NativeBookmarkManager.java | 178
src/main/java/eu/siacs/conversations/xmpp/manager/NickManager.java | 64
src/main/java/eu/siacs/conversations/xmpp/manager/OfflineMessagesManager.java | 31
src/main/java/eu/siacs/conversations/xmpp/manager/PepManager.java | 79
src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java | 12
src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java | 140
src/main/java/eu/siacs/conversations/xmpp/manager/PrivateStorageManager.java | 68
src/main/java/eu/siacs/conversations/xmpp/manager/PubSubManager.java | 401
src/main/java/eu/siacs/conversations/xmpp/manager/RegistrationManager.java | 314
src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java | 377
src/main/java/eu/siacs/conversations/xmpp/manager/StreamHostManager.java | 69
src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java | 34
src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java | 155
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java | 89
src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java | 20
src/main/java/im/conversations/android/xmpp/NodeConfiguration.java | 20
src/main/java/im/conversations/android/xmpp/model/Extension.java | 10
src/main/java/im/conversations/android/xmpp/model/avatar/Info.java | 48
src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java | 5
src/main/java/im/conversations/android/xmpp/model/blocking/Block.java | 5
src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java | 5
src/main/java/im/conversations/android/xmpp/model/blocking/Item.java | 4
src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java | 5
src/main/java/im/conversations/android/xmpp/model/bob/Data.java | 27
src/main/java/im/conversations/android/xmpp/model/bob/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java | 4
src/main/java/im/conversations/android/xmpp/model/bookmark2/Password.java | 12
src/main/java/im/conversations/android/xmpp/model/conference/DirectInvite.java | 21
src/main/java/im/conversations/android/xmpp/model/conference/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/data/Data.java | 33
src/main/java/im/conversations/android/xmpp/model/data/Field.java | 6
src/main/java/im/conversations/android/xmpp/model/hints/NoCopy.java | 11
src/main/java/im/conversations/android/xmpp/model/hints/NoStore.java | 12
src/main/java/im/conversations/android/xmpp/model/ibb/Close.java | 11
src/main/java/im/conversations/android/xmpp/model/ibb/Data.java | 12
src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java | 14
src/main/java/im/conversations/android/xmpp/model/ibb/Open.java | 11
src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java | 5
src/main/java/im/conversations/android/xmpp/model/media/Media.java | 25
src/main/java/im/conversations/android/xmpp/model/media/Uri.java | 12
src/main/java/im/conversations/android/xmpp/model/media/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/muc/History.java | 5
src/main/java/im/conversations/android/xmpp/model/muc/Item.java | 54
src/main/java/im/conversations/android/xmpp/model/muc/Password.java | 17
src/main/java/im/conversations/android/xmpp/model/muc/admin/Item.java | 30
src/main/java/im/conversations/android/xmpp/model/muc/admin/MucAdmin.java | 17
src/main/java/im/conversations/android/xmpp/model/muc/admin/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/muc/owner/Destroy.java | 12
src/main/java/im/conversations/android/xmpp/model/muc/owner/MucOwner.java | 16
src/main/java/im/conversations/android/xmpp/model/muc/owner/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/muc/user/Invite.java | 17
src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java | 49
src/main/java/im/conversations/android/xmpp/model/nick/Nick.java | 5
src/main/java/im/conversations/android/xmpp/model/offline/Offline.java | 12
src/main/java/im/conversations/android/xmpp/model/offline/Purge.java | 12
src/main/java/im/conversations/android/xmpp/model/offline/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java | 9
src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Delete.java | 16
src/main/java/im/conversations/android/xmpp/model/register/Instructions.java | 2
src/main/java/im/conversations/android/xmpp/model/register/Password.java | 2
src/main/java/im/conversations/android/xmpp/model/register/Register.java | 5
src/main/java/im/conversations/android/xmpp/model/register/RegisterStreamFeature.java | 13
src/main/java/im/conversations/android/xmpp/model/register/Remove.java | 2
src/main/java/im/conversations/android/xmpp/model/reporting/Report.java | 16
src/main/java/im/conversations/android/xmpp/model/roster/Group.java | 5
src/main/java/im/conversations/android/xmpp/model/roster/Item.java | 25
src/main/java/im/conversations/android/xmpp/model/roster/Query.java | 5
src/main/java/im/conversations/android/xmpp/model/socks5/Activate.java | 18
src/main/java/im/conversations/android/xmpp/model/socks5/Query.java | 20
src/main/java/im/conversations/android/xmpp/model/socks5/StreamHost.java | 25
src/main/java/im/conversations/android/xmpp/model/socks5/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java | 17
src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java | 39
src/main/java/im/conversations/android/xmpp/model/streams/Features.java | 16
src/main/java/im/conversations/android/xmpp/model/time/Time.java | 20
src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java | 12
src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java | 12
src/main/java/im/conversations/android/xmpp/model/time/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/token/Register.java | 12
src/main/java/im/conversations/android/xmpp/model/token/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java | 9
src/main/java/im/conversations/android/xmpp/model/up/Push.java | 13
src/main/java/im/conversations/android/xmpp/model/up/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/upload/Put.java | 22
src/main/java/im/conversations/android/xmpp/model/upload/Request.java | 8
src/main/java/im/conversations/android/xmpp/model/upload/purpose/Ephemeral.java | 11
src/main/java/im/conversations/android/xmpp/model/upload/purpose/Message.java | 11
src/main/java/im/conversations/android/xmpp/model/upload/purpose/Permanent.java | 11
src/main/java/im/conversations/android/xmpp/model/upload/purpose/Profile.java | 11
src/main/java/im/conversations/android/xmpp/model/upload/purpose/Purpose.java | 10
src/main/java/im/conversations/android/xmpp/model/upload/purpose/package-info.java | 5
src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java | 9
src/main/java/im/conversations/android/xmpp/model/vcard/Type.java | 12
src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java | 4
src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java | 7
src/main/java/im/conversations/android/xmpp/processor/AccountStateProcessor.java | 128
src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java | 70
src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgedProcessor.java | 53
src/main/res/drawable/ic_contacts_24dp.xml | 10
src/main/res/layout/activity_contact_details.xml | 116
src/main/res/layout/activity_view_profile_picture.xml | 28
src/main/res/values-ar/strings.xml | 1
src/main/res/values-bg/strings.xml | 1
src/main/res/values-bn-rIN/strings.xml | 1
src/main/res/values-ca/strings.xml | 1
src/main/res/values-cs/strings.xml | 1
src/main/res/values-da-rDK/strings.xml | 1
src/main/res/values-de/strings.xml | 4
src/main/res/values-el/strings.xml | 1
src/main/res/values-es/strings.xml | 9
src/main/res/values-et/strings.xml | 8
src/main/res/values-eu/strings.xml | 1
src/main/res/values-fa-rIR/strings.xml | 1
src/main/res/values-fi/strings.xml | 1
src/main/res/values-fr/strings.xml | 1
src/main/res/values-gl/strings.xml | 4
src/main/res/values-hr/strings.xml | 1
src/main/res/values-hu/strings.xml | 3
src/main/res/values-id/strings.xml | 1
src/main/res/values-it/strings.xml | 4
src/main/res/values-iw/strings.xml | 1
src/main/res/values-ja/strings.xml | 23
src/main/res/values-kab/strings.xml | 47
src/main/res/values-ko/strings.xml | 1
src/main/res/values-nb-rNO/strings.xml | 1
src/main/res/values-night/themes.xml | 31
src/main/res/values-nl/strings.xml | 1
src/main/res/values-pl/strings.xml | 4
src/main/res/values-pt-rBR/strings.xml | 4
src/main/res/values-pt/strings.xml | 143
src/main/res/values-ro-rRO/strings.xml | 4
src/main/res/values-ru/strings.xml | 4
src/main/res/values-sk/strings.xml | 1
src/main/res/values-sq-rAL/strings.xml | 4
src/main/res/values-sr/strings.xml | 4
src/main/res/values-sv/strings.xml | 1
src/main/res/values-sw640dp/defaults.xml | 1
src/main/res/values-sw640dp/device.xml | 0
src/main/res/values-szl/strings.xml | 1
src/main/res/values-tr-rTR/strings.xml | 1
src/main/res/values-uk/strings.xml | 4
src/main/res/values-vi/strings.xml | 1
src/main/res/values-zh-rCN/strings.xml | 4
src/main/res/values-zh-rTW/strings.xml | 1
src/main/res/values/dimens.xml | 2
src/main/res/values/strings.xml | 5
src/main/res/values/themes.xml | 31
src/main/res/xml/preferences_interface_bubbles.xml | 1
src/quicksy/fastlane/metadata/android/es-ES/full_description.txt | 14
src/quicksy/fastlane/metadata/android/et/full_description.txt | 4
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java | 21
src/quicksy/res/drawable/ic_app_icon_notification.xml | 12
src/quicksy/res/drawable/ic_launcher_foreground.xml | 14
src/quicksy/res/drawable/ic_launcher_monochrome.xml | 9
src/quicksy/res/layout/activity_enter_number.xml | 152
src/quicksy/res/values-kab/strings.xml | 5
src/test/java/eu/siacs/conversations/xmpp/JidTest.java | 16
src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java | 100
256 files changed, 9,118 insertions(+), 4,391 deletions(-)
@@ -1,5 +1,9 @@
# Changelog
+### Version 2.19.0
+
+* Upload and show high resolution avatars (profile pictures) on servers that have support for them
+
### Version 2.18.2
* Support 'Service Outage Status'
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.9.1'
+ classpath 'com.android.tools.build:gradle:8.9.3'
classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2"
}
}
@@ -67,12 +67,14 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.emoji2:emoji2:1.5.0'
freeImplementation 'androidx.emoji2:emoji2-bundled:1.5.0'
- implementation 'androidx.exifinterface:exifinterface:1.4.0'
+ implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
+ implementation 'androidx.exifinterface:exifinterface:1.4.1'
+ implementation 'androidx.heifwriter:heifwriter:1.1.0-beta01'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.sharetarget:sharetarget:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.viewpager:viewpager:1.1.0'
- implementation 'androidx.work:work-runtime:2.10.0'
+ implementation 'androidx.work:work-runtime:2.10.1'
implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1'
implementation 'com.google.android.material:material:1.13.0-alpha10'
implementation 'com.google.guava:guava:33.4.6-android'
@@ -0,0 +1,2 @@
+* Posibilidad de elegir ubicación de copia de respaldo
+* Más URIs (tel:, mailto:) cliqueables
@@ -0,0 +1,2 @@
+* Corregir reacciones a archivos recibidos mediante P2P
+* Mejorar detección de URI
@@ -0,0 +1,2 @@
+* Compatibilidad con "Service Outage Status"
+* Correcciones menores de seguridad para resolver múltiples bodies, occupant-ids y stanza-id
@@ -1,2 +1,2 @@
-* Play Store'i versioonis on nüüd lihtsam ligipääs privaatsuspoliitikale (Quicksy ja Conversations)
+* Play Store'i versioonis on nüüd lihtsam ligipääs andmekaitsepõhimõtetele (Quicksy ja Conversations)
* Conversationsi Play Store'i versioonist oleme eemaldanud lõimingu aadressiraamatuga
@@ -1,3 +1,3 @@
* Додано «Повернутися до чату» на екрані голосового виклику
-* Удосконалено комбінації клавіш
+* Удосконалено клавіатурні скорочення
* Виправлення помилок
@@ -8,6 +8,7 @@ import java.util.Map;
import java.util.HashMap;
import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
@@ -119,7 +120,7 @@ public class BobTransfer implements Transferable {
throw new IOException(file.getAbsolutePath());
}
- final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false);
+ final OutputStream outputStream = new FileOutputStream(file, false);
if (outputStream != null && bytes != null) {
outputStream.write(bytes);
@@ -97,7 +97,7 @@ public class FinishOnboarding {
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);
+ xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()));
Conversation withCheogram = xmppConnectionService.findOrCreateConversation(newAccount, iq4.getFrom().asBareJid(), true, true, true);
xmppConnectionService.markRead(withCheogram);
xmppConnectionService.clearConversationHistory(withCheogram);
@@ -26,6 +26,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
public class QuickConversationsService extends AbstractQuickConversationsService {
@@ -107,7 +108,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
if (allContacts == null) allContacts = PhoneNumberContact.load(service);
refresh(account, gateways, allContacts.values());
if (!considerSync(account, gateways, allContacts, forced)) {
- service.syncRoster(account);
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
}
}
}
@@ -151,7 +152,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
mRunningSyncJobs.decrementAndGet();
- service.syncRoster(account);
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
service.updateRosterUi(XmppConnectionService.UpdateRosterReason.INIT);
return true;
}
@@ -2,29 +2,31 @@ Fácil de usar, confiable y amigable con la batería. Con soporte integrado para
Principios de diseño:
-Ser tan bello y fácil de usar como sea posible, sin sacrificar la seguridad o la privacidad.
-Confiar en protocolos existentes y bien establecidos.
-No requerir una cuenta de Google ni Google Cloud Messaging (GCM).
-Requerir la menor cantidad de permisos posible.
+* Ser tan bello y fácil de usar como sea posible, sin sacrificar la seguridad o la privacidad.
+* Confiar en protocolos existentes y bien establecidos.
+* No requerir una cuenta de Google ni Google Cloud Messaging (GCM).
+* Requerir la menor cantidad de permisos posible.
+
Características:
-Cifrado de extremo a extremo con <a href="http://conversations.im/omemo/">OMEMO</a> o <a href="http://openpgp.org/about/">OpenPGP</a>.
-Envío y recepción de imágenes.
-Llamadas de audio y video cifradas (DTLS-SRTP).
-Interfaz intuitiva que sigue las pautas de diseño de Android.
-Imágenes / Avatares para tus contactos.
-Sincronización con el cliente de escritorio.
-Conferencias (con soporte para marcadores).
-Integración con la agenda de contactos.
-Múltiples cuentas / bandeja de entrada unificada.
-Muy bajo impacto en la duración de la batería.
+* Cifrado de extremo a extremo con <a href="http://conversations.im/omemo/">OMEMO</a> u <a href="http://openpgp.org/about/">OpenPGP</a>.
+* Envío y recepción de imágenes.
+* Llamadas de audio y video cifradas (DTLS-SRTP).
+* Interfaz intuitiva que sigue las pautas de diseño de Android.
+* Imágenes / Avatares para tus contactos.
+* Sincronización con el cliente de escritorio.
+* Conferencias (con soporte para marcadores).
+* Integración con la agenda de contactos.
+* Múltiples cuentas / bandeja de entrada unificada.
+* Muy bajo impacto en la duración de la batería.
+
Conversations facilita mucho la creación de una cuenta en el servidor gratuito conversations.im. Sin embargo, Conversations también funcionará con cualquier otro servidor XMPP. Muchos servidores XMPP son gestionados por voluntarios y son gratuitos.
Características de XMPP:
-Conversations funciona con todos los servidores XMPP disponibles. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en lo que se llaman XEP. Conversations admite algunas de estas para mejorar la experiencia general del usuario. Existe la posibilidad de que tu servidor XMPP actual no soporte estas extensiones. Por lo tanto, para aprovechar al máximo Conversations, deberías considerar cambiar a un servidor XMPP que sí lo haga o, aún mejor, configurar tu propio servidor XMPP para ti y tus amigos.
+Conversations funciona con todos los servidores XMPP disponibles. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en lo que se conoce como XEP. Conversations admite algunas de éstas para mejorar la experiencia general del usuario. Existe la posibilidad de que tu servidor XMPP actual no admita estas extensiones. Por lo tanto, para aprovechar al máximo Conversations, deberías considerar cambiar a un servidor XMPP que sí lo haga o, aún mejor, configurar tu propio servidor XMPP para ti y tus amigos.
-Estos XEP son, hasta ahora:
+Estas XEP son, hasta ahora:
XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Se utilizará para transferir archivos si ambas partes están detrás de un firewall o NAT.
XEP-0163: Protocolo de Eventos Personales para avatares.
@@ -1,11 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportWidth="512"
- android:viewportHeight="512">
- <group android:translateX="5.31">
+ android:viewportWidth="472"
+ android:viewportHeight="472">
+ <group
+ android:translateX="20"
+ android:translateY="20">
<path
android:fillColor="@android:color/white"
- android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z" />
+ android:pathData="M216 22C106.95 22.53 18 109.75 18 216.9c0 107.15 88.95 193.6 198 193.1 33.91-0.17 60.08-7.25 89.93-18.23l94.8 37.77c9.45 3.79 19.26-4.77 16.81-14.67l-25.7-103.69c16.51-29.05 22.16-62.1 22.16-96.13C414 107.9 325.05 21.5 216 22.01Zm-93.84 175.75c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.84 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Zm94.15-0.44c11.85-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.45 21.55-11.85 0.06-21.46-9.5-21.46-21.35 0-11.85 9.6-21.5 21.46-21.55Zm94.4-0.44c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.85 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Z" />
</group>
</vector>
@@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="1146.7721"
- android:viewportHeight="1146.7721">
+ android:width="432dp"
+ android:height="432dp"
+ android:viewportWidth="732"
+ android:viewportHeight="732">
<group
- android:translateX="322.69516"
- android:translateY="317.38605">
+ android:translateX="150"
+ android:translateY="150">
<path
android:fillColor="#ffffff"
- android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z" />
+ android:pathData="M216 74.92c-79.3 0.37-144 63.8-144 141.73 0 77.93 64.69 140.8 144 140.43 24.66-0.11 43.7-5.26 65.4-13.25l68.95 27.47c6.87 2.76 14.01-3.47 12.22-10.67l-18.68-75.4c12-21.14 16.11-45.17 16.11-69.92 0-77.93-64.7-140.76-144-140.4Zm-68.25 127.8c8.62-0.04 15.6 6.92 15.6 15.53 0 8.62-6.98 15.64-15.6 15.68-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.64 15.6-15.68Zm68.47-0.31c8.62-0.04 15.6 6.9 15.6 15.53 0 8.61-6.98 15.63-15.6 15.67-8.61 0.04-15.6-6.91-15.6-15.53s6.99-15.64 15.6-15.68Zm68.66-0.32c8.61-0.05 15.6 6.9 15.6 15.53 0 8.61-6.99 15.63-15.6 15.67-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.63 15.6-15.67Z" />
</group>
-</vector>
+</vector>
@@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="1146.7721"
- android:viewportHeight="1146.7721">
- <group
- android:translateX="322.69516"
- android:translateY="317.38605">
- <path
- android:fillColor="#000000"
- android:pathData="M253.219,17.719C126.144,17.719 22.469,118.884 22.469,243.75C22.469,368.616 126.138,469.844 253.219,469.844C292.739,469.844 323.216,461.736 358,449.094L468.469,493.625A14.556,14.562 0,0 0,488.063 476.625L458.125,355.656C477.356,321.886 483.938,283.416 483.938,243.75C483.938,118.887 380.293,17.719 253.219,17.719zM143.844,222C157.651,222 168.844,233.193 168.844,247C168.844,260.807 157.651,272 143.844,272C130.037,272 118.844,260.807 118.844,247C118.844,233.193 130.037,222 143.844,222zM253.563,222C267.37,222 278.563,233.193 278.563,247C278.563,260.807 267.37,272 253.563,272C239.755,272 228.563,260.807 228.563,247C228.563,233.193 239.755,222 253.563,222zM363.563,222C377.37,222 388.563,233.193 388.563,247C388.563,260.807 377.37,272 363.563,272C349.755,272 338.563,260.807 338.563,247C338.563,233.193 349.755,222 363.563,222z" />
- </group>
-</vector>
+android:width="432dp"
+android:height="432dp"
+android:viewportWidth="732"
+android:viewportHeight="732">
+<group
+ android:translateX="150"
+ android:translateY="150">
+ <path
+ android:fillColor="#000000"
+ android:pathData="M216 74.92c-79.3 0.37-144 63.8-144 141.73 0 77.93 64.69 140.8 144 140.43 24.66-0.11 43.7-5.26 65.4-13.25l68.95 27.47c6.87 2.76 14.01-3.47 12.22-10.67l-18.68-75.4c12-21.14 16.11-45.17 16.11-69.92 0-77.93-64.7-140.76-144-140.4Zm-68.25 127.8c8.62-0.04 15.6 6.92 15.6 15.53 0 8.62-6.98 15.64-15.6 15.68-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.64 15.6-15.68Zm68.47-0.31c8.62-0.04 15.6 6.9 15.6 15.53 0 8.61-6.98 15.63-15.6 15.67-8.61 0.04-15.6-6.91-15.6-15.53s6.99-15.64 15.6-15.68Zm68.66-0.32c8.61-0.05 15.6 6.9 15.6 15.53 0 8.61-6.99 15.63-15.6 15.67-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.63 15.6-15.67Z" />
+</group>
+</vector>
@@ -1,2 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources></resources>
+<resources>
+ <string name="pick_a_server">Selecione o provedor XMPP</string>
+ <string name="use_conversations.im">Usar conversations.im</string>
+ <string name="create_new_account">Criar nova conta</string>
+ <string name="do_you_have_an_account">Já tem uma conta XMPP? Esse pode ser o caso se já usou um cliente de XMPP diferente ou tenha usado o Conversations antes. Caso contrário, pode criar uma agora. \nDica: Alguns provedores de e-mail também fornecem contas XMPP.</string>
+ <string name="your_server_invitation">O teu convite do servidor</string>
+ <string name="improperly_formatted_provisioning">Código de provisionamento formatado incorretamente</string>
+ <string name="tap_share_button_send_invite">Toque no botão de partilha para enviar ao seu contacto um convite para %1$s.</string>
+ <string name="if_contact_is_nearby_use_qr">Se o seu contacto está por perto, também podem scanerizar o código abaixo para aceitar o convite.</string>
+ <string name="easy_invite_share_text">Junte-se a %1$s e fale comigo: %2$s</string>
+ <string name="share_invite_with">Partilhar convite com…</string>
+ <string name="server_select_text">XMPP é um provedor independente de rede de mensagens instantânea. Pode usar esta aplicação com qualquer servidor XMPP que escolha.\nNo entanto, para a sua conveniência, facilitamos criar uma conta em conversations.im, um provedor especificamente adaptado para o uso com Conversations.</string>
+ <string name="magic_create_text_on_x">Foi convidado para %1$s. Iremos guiá-lo ao longo do processo de criar uma conta.\nQuando selecionando %1$s como um provedor irá conseguir comunicar com utilizadores de outros provedores ao dar o seu endereço completo XMPP.</string>
+ <string name="magic_create_text_fixed">Foi convidado para %1$s. Um nome de utilizador já foi escolhido para si. Iremos guiá-lo ao longo do processo de criar uma conta.\nIrá conseguir comunicar com utilizadores de outros provedores ao dar o seu endereço completo de XMPP.</string>
+</resources>
@@ -60,6 +60,8 @@
android:name="android.hardware.microphone"
android:required="false" />
+ <uses-sdk tools:overrideLibrary="androidx.heifwriter" />
+
<queries>
<package android:name="org.sufficientlysecure.keychain" />
<package android:name="org.torproject.android" />
@@ -400,6 +402,8 @@
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
+ <activity android:name=".ui.ViewProfilePictureActivity"
+ android:theme="@style/Theme.Conversations3.Dark"/>
<activity
android:name=".ui.MucUsersActivity"
android:label="@string/group_chat_members"
@@ -0,0 +1,42 @@
+package de.gultsch.common;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.Collection;
+import java.util.List;
+
+public class FutureMerger {
+
+ public static <T> ListenableFuture<List<T>> successfulAsList(
+ final Collection<ListenableFuture<List<T>>> futures) {
+ return Futures.transform(
+ Futures.successfulAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder<T>();
+ for (final Collection<T> list : lists) {
+ if (list == null) {
+ continue;
+ }
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public static <T> ListenableFuture<List<T>> allAsList(
+ final Collection<ListenableFuture<Collection<T>>> futures) {
+ return Futures.transform(
+ Futures.allAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder<T>();
+ for (final Collection<T> list : lists) {
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,100 @@
+package de.gultsch.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class IntMap<E> implements Map<E, Integer> {
+
+ private final ImmutableMap<E, Integer> inner;
+
+ public IntMap(ImmutableMap<E, Integer> inner) {
+ this.inner = inner;
+ }
+
+ @Override
+ public int size() {
+ return this.inner.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.inner.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(@Nullable Object key) {
+ return this.inner.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(@Nullable Object value) {
+ return this.inner.containsValue(value);
+ }
+
+ @Nullable
+ @Override
+ public Integer get(@Nullable Object key) {
+ return this.inner.get(key);
+ }
+
+ public int getInt(@Nullable E key) {
+ final var value = this.inner.get(key);
+ return value == null ? Integer.MIN_VALUE : value;
+ }
+
+ @Nullable
+ @Override
+ public Integer put(E key, Integer value) {
+ return this.inner.put(key, value);
+ }
+
+ @Nullable
+ @Override
+ public Integer remove(@Nullable Object key) {
+ return this.inner.remove(key);
+ }
+
+ @Override
+ public void putAll(@NonNull Map<? extends E, ? extends Integer> m) {
+ this.inner.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ this.inner.clear();
+ }
+
+ @NonNull
+ @Override
+ public Set<E> keySet() {
+ return this.inner.keySet();
+ }
+
+ @NonNull
+ @Override
+ public Collection<Integer> values() {
+ return this.inner.values();
+ }
+
+ @NonNull
+ @Override
+ public Set<Entry<E, Integer>> entrySet() {
+ return this.inner.entrySet();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof IntMap<?> intMap)) return false;
+ return Objects.equal(inner, intMap.inner);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(inner);
+ }
+}
@@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.BoolRes;
+import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.google.common.base.Joiner;
@@ -14,6 +15,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.Compatibility;
import java.security.SecureRandom;
+import java.util.Optional;
public class AppSettings {
@@ -25,13 +27,13 @@ public class AppSettings {
public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
public static final String BROADCAST_LAST_ACTIVITY = "last_activity";
+ public static final String SEND_CHAT_STATES = "chat_states";
public static final String THEME = "theme";
public static final String DYNAMIC_COLORS = "dynamic_colors";
public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags";
public static final String OMEMO = "omemo";
public static final String ALLOW_SCREENSHOTS = "allow_screenshots";
public static final String RINGTONE = "call_ringtone";
- public static final String BTBV = "btbv";
public static final String CONFIRM_MESSAGES = "confirm_messages";
public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
@@ -53,8 +55,10 @@ public class AppSettings {
public static final String CALL_INTEGRATION = "call_integration";
public static final String ALIGN_START = "align_start";
public static final String BACKUP_LOCATION = "backup_location";
+ public static final String AUTO_ACCEPT_FILE_SIZE = "auto_accept_file_size";
private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers";
+ private static final String NOTIFICATIONS_FROM_STRANGERS = "notifications_from_strangers";
private static final String INSTALLATION_ID = "im.conversations.android.install_id";
private static final String EXTERNAL_STORAGE_AUTHORITY =
@@ -100,7 +104,7 @@ public class AppSettings {
}
public boolean isBTBVEnabled() {
- return getBooleanPreference(BTBV, R.bool.btbv);
+ return getBooleanPreference(BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
}
public boolean isTrustSystemCAStore() {
@@ -147,11 +151,37 @@ public class AppSettings {
return getBooleanPreference(BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
}
+ public boolean isUserManagedAvailability() {
+ return getBooleanPreference(MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
+ }
+
+ public boolean isAutomaticAvailability() {
+ return !isUserManagedAvailability();
+ }
+
+ public boolean isDndOnSilentMode() {
+ return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
+ }
+
+ public boolean isTreatVibrateAsSilent() {
+ return getBooleanPreference(
+ AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
+ }
+
+ public boolean isAwayWhenScreenLocked() {
+ return getBooleanPreference(
+ AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
+ }
+
public boolean isUseTor() {
return QuickConversationsService.isConversations()
&& getBooleanPreference(USE_TOR, R.bool.use_tor);
}
+ public boolean isSendChatStates() {
+ return getBooleanPreference(SEND_CHAT_STATES, R.bool.chat_states);
+ }
+
public boolean isExtendedConnectionOptions() {
return QuickConversationsService.isConversations()
&& getBooleanPreference(
@@ -162,17 +192,33 @@ public class AppSettings {
return true;
}
+ public boolean isNotificationsFromStrangers() {
+ return getBooleanPreference(
+ NOTIFICATIONS_FROM_STRANGERS, R.bool.notifications_from_strangers);
+ }
+
public boolean isKeepForegroundService() {
return Compatibility.twentySix()
|| getBooleanPreference(KEEP_FOREGROUND_SERVICE, R.bool.enable_foreground_service);
}
- private boolean getBooleanPreference(@NonNull final String name, @BoolRes int res) {
+ private boolean getBooleanPreference(@NonNull final String name, @BoolRes final int res) {
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
return sharedPreferences.getBoolean(name, context.getResources().getBoolean(res));
}
+ private long getLongPreference(final String name, @IntegerRes final int res) {
+ final long defaultValue = context.getResources().getInteger(res);
+ final SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ try {
+ return Long.parseLong(sharedPreferences.getString(name, String.valueOf(defaultValue)));
+ } catch (final NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
public String getOmemo() {
final SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
@@ -250,6 +296,12 @@ public class AppSettings {
return installationId;
}
+ public Optional<Long> getAutoAcceptFileSize() {
+ final long autoAcceptFileSize =
+ getLongPreference(AUTO_ACCEPT_FILE_SIZE, R.integer.auto_accept_filesize);
+ return autoAcceptFileSize <= 0 ? Optional.empty() : Optional.of(autoAcceptFileSize);
+ }
+
public synchronized void resetInstallationId() {
final var secureRandom = new SecureRandom();
final var installationId = secureRandom.nextLong();
@@ -80,11 +80,9 @@ public final class Config {
public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int MINI_GRACE_PERIOD = 750;
- // media file formats. Homogenous Android or Conversations only deployments can switch to opus
- // and webp
- public static final int AVATAR_SIZE = 192;
- public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG;
- public static final int AVATAR_CHAR_LIMIT = 9400;
+ public static final int AVATAR_THUMBNAIL_SIZE = 192;
+ public static final int AVATAR_THUMBNAIL_CHAR_LIMIT = 9400;
+ public static final int AVATAR_FULL_SIZE = 1280;
public static final int IMAGE_SIZE = 1920;
public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG;
@@ -121,8 +119,8 @@ public final class Config {
public static final boolean ENABLE_CAPS_CACHE = true;
- public static final boolean DISABLE_HTTP_UPLOAD = false;
- public static final boolean EXTENDED_SM_LOGGING = true; // log stanza counts
+ public static final boolean ENABLE_HTTP_UPLOAD = true;
+ public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
public static final boolean BACKGROUND_STANZA_LOGGING =
false; // log all stanzas that were received while the app is in background
public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE =
@@ -136,6 +134,7 @@ public final class Config {
false; // require a/v calls to be verified with OMEMO
public static final boolean JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK = false;
public static final boolean JINGLE_MESSAGE_INIT_STRICT_DEVICE_TIMEOUT = false;
+ // TODO extend this to 12s
public static final long DEVICE_DISCOVERY_TIMEOUT = 6000; // in milliseconds
public static final boolean ONLY_INTERNAL_STORAGE =
@@ -0,0 +1,80 @@
+package eu.siacs.conversations.android;
+
+import android.app.KeyguardManager;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.PowerManager;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+
+public class Device {
+
+ private final Context context;
+
+ public Device(final Context context) {
+ this.context = context;
+ }
+
+ public boolean isScreenLocked() {
+ final var keyguardManager = context.getSystemService(KeyguardManager.class);
+ final var powerManager = context.getSystemService(PowerManager.class);
+ final var locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
+ final boolean interactive;
+ try {
+ interactive = powerManager != null && powerManager.isInteractive();
+ } catch (final Exception e) {
+ return false;
+ }
+ return locked || !interactive;
+ }
+
+ public boolean isPhoneSilenced(final boolean vibrateIsSilent) {
+ final var notificationManager = context.getSystemService(NotificationManager.class);
+ final int filter =
+ notificationManager == null
+ ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
+ : notificationManager.getCurrentInterruptionFilter();
+ final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+ final var audioManager = context.getSystemService(AudioManager.class);
+ final int ringerMode =
+ audioManager == null
+ ? AudioManager.RINGER_MODE_NORMAL
+ : audioManager.getRingerMode();
+ try {
+ if (vibrateIsSilent) {
+ return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
+ } else {
+ return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
+ }
+ } catch (final Throwable throwable) {
+ Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced", throwable);
+ return notificationDnd;
+ }
+ }
+
+ public boolean isPhysicalDevice() {
+ return !isEmulator();
+ }
+
+ private static boolean isEmulator() {
+ return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
+ || Build.FINGERPRINT.startsWith("generic")
+ || Build.FINGERPRINT.startsWith("unknown")
+ || Build.HARDWARE.contains("goldfish")
+ || Build.HARDWARE.contains("ranchu")
+ || Build.MODEL.contains("google_sdk")
+ || Build.MODEL.contains("Emulator")
+ || Build.MODEL.contains("Android SDK built for x86")
+ || Build.MANUFACTURER.contains("Genymotion")
+ || Build.PRODUCT.contains("sdk_google")
+ || Build.PRODUCT.contains("google_sdk")
+ || Build.PRODUCT.contains("sdk")
+ || Build.PRODUCT.contains("sdk_x86")
+ || Build.PRODUCT.contains("sdk_gphone64_arm64")
+ || Build.PRODUCT.contains("vbox86p")
+ || Build.PRODUCT.contains("emulator")
+ || Build.PRODUCT.contains("simulator");
+ }
+}
@@ -38,7 +38,6 @@ import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.http.ServiceOutageStatus;
import eu.siacs.conversations.services.AvatarService;
-import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
@@ -46,15 +45,18 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
import org.json.JSONException;
import org.json.JSONObject;
@@ -96,14 +98,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
public static final String KEY_SOS_URL = "sos_url";
public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
-
protected final JSONObject keys;
- private final Roster roster = new Roster(this);
- private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
- public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
- public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
- public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
- public final Set<Conversation> inProgressConferencePings = new HashSet<>();
protected Jid jid;
protected String password;
protected int options = 0;
@@ -116,8 +111,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
protected boolean online = false;
private String rosterVersion;
private String displayName = null;
- private AxolotlService axolotlService = null;
- private PgpDecryptionService pgpDecryptionService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
@@ -244,12 +237,14 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return mamPrefs;
}
- public boolean httpUploadAvailable(long size) {
- return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size);
+ // TODO remove this method and call HttpUploadManager directly i
+ public boolean httpUploadAvailable(final long fileSize) {
+ return xmppConnection.getManager(HttpUploadManager.class).isAvailableForSize(fileSize);
}
public boolean httpUploadAvailable() {
- return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0);
+ return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE)
+ || xmppConnection.getManager(HttpUploadManager.class).isAvailableForSize(0);
}
public String getDisplayName() {
@@ -265,11 +260,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public boolean hasPendingPgpIntent(Conversation conversation) {
- return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
+ return getPgpDecryptionService().hasPendingIntent(conversation);
}
public boolean isPgpDecryptionServiceConnected() {
- return pgpDecryptionService != null && pgpDecryptionService.isConnected();
+ return getPgpDecryptionService().isConnected();
}
public void setColor(Integer color) {
@@ -334,11 +329,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
final Jid prev = this.jid != null ? this.jid.asBareJid() : null;
final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid()));
if (changed) {
- final AxolotlService oldAxolotlService = this.axolotlService;
+ final AxolotlService oldAxolotlService = xmppConnection.getAxolotlService();
+ // TODO check that changing JID and recreating the AxolotlService still works
if (oldAxolotlService != null) {
oldAxolotlService.destroy();
this.jid = next;
- this.axolotlService = oldAxolotlService.makeNew();
+ xmppConnection.setAxolotlService(oldAxolotlService.makeNew());
}
}
this.jid = next;
@@ -594,35 +590,19 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public AxolotlService getAxolotlService() {
- return axolotlService;
- }
-
- public void initAccountServices(final XmppConnectionService context) {
- this.axolotlService = new AxolotlService(this, context);
- this.pgpDecryptionService = new PgpDecryptionService(context);
- if (xmppConnection != null) {
- xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
- }
+ return this.xmppConnection.getAxolotlService();
}
public PgpDecryptionService getPgpDecryptionService() {
- return this.pgpDecryptionService;
+ return this.xmppConnection.getPgpDecryptionService();
}
public XmppConnection getXmppConnection() {
return this.xmppConnection;
}
- public void setXmppConnection(final XmppConnection connection) {
- this.xmppConnection = connection;
- }
-
public String getRosterVersion() {
- if (this.rosterVersion == null) {
- return "";
- } else {
- return this.rosterVersion;
- }
+ return Strings.emptyToNull(this.rosterVersion);
}
public void setRosterVersion(final String version) {
@@ -696,7 +676,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public Roster getRoster() {
- return this.roster;
+ return xmppConnection.getManager(RosterManager.class);
}
public void refreshCapsFor(Contact contact) {
@@ -734,7 +714,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public boolean areBookmarksLoaded() {
// No way to tell if old PEP bookmarks are all loaded yet if they are empty
// because we don't manually fetch them...
- if (getXmppConnection().getFeatures().bookmarksConversion()) return true;
+ if (!getXmppConnection().getFeatures().bookmarks2()) return true;
return bookmarksLoaded;
}
@@ -828,9 +808,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private List<XmppUri.Fingerprint> getFingerprints() {
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
- if (axolotlService == null) {
- return fingerprints;
- }
+ final var axolotlService = getAxolotlService();
fingerprints.add(
new XmppUri.Fingerprint(
XmppUri.FingerprintType.OMEMO,
@@ -850,20 +828,22 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public boolean isBlocked(final ListItem contact) {
final Jid jid = contact.getJid();
+ final var blocklist = getBlocklist();
return jid != null
&& (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
}
public boolean isBlocked(final Jid jid) {
+ final var blocklist = getBlocklist();
return jid != null && blocklist.contains(jid.asBareJid());
}
- public Collection<Jid> getBlocklist() {
- return this.blocklist;
- }
-
- public void clearBlocklist() {
- getBlocklist().clear();
+ public Set<Jid> getBlocklist() {
+ final var connection = this.xmppConnection;
+ if (connection == null) {
+ return Collections.emptySet();
+ }
+ return connection.getManager(BlockingManager.class).getBlocklist();
}
public boolean isOnlineAndConnected() {
@@ -898,6 +878,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return false;
}
+ public void setXmppConnection(final XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
public enum State {
DISABLED(false, false),
LOGGED_OUT(false, false),
@@ -1,11 +1,17 @@
package eu.siacs.conversations.entities;
+import androidx.annotation.NonNull;
import eu.siacs.conversations.xmpp.Jid;
public interface Blockable {
- boolean isBlocked();
- boolean isDomainBlocked();
- Jid getBlockedJid();
- Jid getJid();
- Account getAccount();
+ boolean isBlocked();
+
+ boolean isDomainBlocked();
+
+ @NonNull
+ Jid getBlockedJid();
+
+ Jid getJid();
+
+ Account getAccount();
}
@@ -8,11 +8,9 @@ import com.google.common.collect.ImmutableList;
import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.model.bookmark.Storage;
-import im.conversations.android.xmpp.model.bookmark2.Conference;
-import im.conversations.android.xmpp.model.pubsub.PubSub;
+import im.conversations.android.xmpp.model.bookmark2.Extensions;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
@@ -26,7 +24,7 @@ public class Bookmark extends Element implements ListItem {
private final Account account;
private WeakReference<Conversation> conversation;
private Jid jid;
- protected Element extensions = new Element("extensions", Namespace.BOOKMARKS2);
+ protected Extensions extensions = new Extensions();
public Bookmark(final Account account, final Jid jid) {
super("conference");
@@ -35,13 +33,14 @@ public class Bookmark extends Element implements ListItem {
this.account = account;
}
- private Bookmark(Account account) {
+ public Bookmark(Account account) {
super("conference");
this.account = account;
}
public static Map<Jid, Bookmark> parseFromStorage(
final Storage storage, final Account account) {
+ // TODO refactor to use extensions. get rid of the 'old' handling
if (storage == null) {
return Collections.emptyMap();
}
@@ -62,26 +61,6 @@ public class Bookmark extends Element implements ListItem {
return bookmarks;
}
- public static Map<Jid, Bookmark> parseFromPubSub(final PubSub pubSub, final Account account) {
- if (pubSub == null) {
- return Collections.emptyMap();
- }
- final var items = pubSub.getItems();
- if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) {
- return Collections.emptyMap();
- }
- final Map<Jid, Bookmark> bookmarks = new HashMap<>();
- for (final var item : items.getItemMap(Conference.class).entrySet()) {
- final Bookmark bookmark =
- Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
- if (bookmark == null) {
- continue;
- }
- bookmarks.put(bookmark.jid, bookmark);
- }
- return bookmarks;
- }
-
public static Bookmark parse(Element element, Account account) {
Bookmark bookmark = new Bookmark(account);
bookmark.setAttributes(element.getAttributes());
@@ -93,34 +72,7 @@ public class Bookmark extends Element implements ListItem {
return bookmark;
}
- public static Bookmark parseFromItem(
- final String id, final Conference conference, final Account account) {
- if (id == null || conference == null) {
- return null;
- }
- final Bookmark bookmark = new Bookmark(account);
- bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
- // TODO verify that we only use bare jids and ignore full jids
- if (bookmark.jid == null) {
- return null;
- }
- bookmark.setBookmarkName(conference.getAttribute("name"));
- bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
- bookmark.setNick(conference.findChildContent("nick"));
- bookmark.setPassword(conference.findChildContent("password"));
- final Element extensions = conference.findChild("extensions", Namespace.BOOKMARKS2);
- if (extensions != null) {
- for (final Element ext : extensions.getChildren()) {
- if (ext.getName().equals("group") && ext.getNamespace().equals("jabber:iq:roster")) {
- bookmark.addGroup(ext.getContent());
- }
- }
- bookmark.extensions = extensions;
- }
- return bookmark;
- }
-
- public Element getExtensions() {
+ public Extensions getExtensions() {
return extensions;
}
@@ -343,4 +295,8 @@ public class Bookmark extends Element implements ListItem {
public String getAvatarName() {
return getDisplayName();
}
+
+ public void setExtensions(Extensions extensions) {
+ this.extensions = extensions;
+ }
}
@@ -31,7 +31,6 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
-import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Collection;
@@ -86,7 +85,7 @@ public class Contact implements ListItem, Blockable {
private JSONArray systemTags = new JSONArray();
private final Presences presences = new Presences(this);
protected Account account;
- protected Avatar avatar;
+ protected String avatar;
private boolean mActive = false;
private long mLastseen = 0;
@@ -94,7 +93,7 @@ public class Contact implements ListItem, Blockable {
private RtpCapability.Capability rtpCapability;
public Contact(Contact other) {
- this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar() == null ? null : other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability);
+ this(null, other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys == null ? null : other.keys.toString(), other.getAvatar(), other.mLastseen, other.mLastPresence, other.groups == null ? null : other.groups.toString(), other.rtpCapability);
setAccount(other.getAccount());
}
@@ -128,11 +127,7 @@ public class Contact implements ListItem, Blockable {
tmpJsonObject = new JSONObject();
}
this.keys = tmpJsonObject;
- if (avatar != null) {
- this.avatar = new Avatar();
- this.avatar.sha1sum = avatar;
- this.avatar.origin = Avatar.Origin.VCARD; // always assume worst
- }
+ this.avatar = avatar;
try {
this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
} catch (JSONException e) {
@@ -295,7 +290,7 @@ public class Contact implements ListItem, Blockable {
values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
values.put(PHOTOURI, photoUri);
values.put(KEYS, keys.toString());
- values.put(AVATAR, avatar == null ? null : avatar.getFilename());
+ values.put(AVATAR, avatar);
values.put(LAST_PRESENCE, mLastPresence);
values.put(LAST_TIME, mLastseen);
values.put(GROUPS, groups.toString());
@@ -393,7 +388,7 @@ public class Contact implements ListItem, Blockable {
this.groups = new JSONArray(groups);
}
- private Collection<String> getGroups(final boolean unique) {
+ public Collection<String> getGroups(final boolean unique) {
final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
for (int i = 0; i < this.groups.length(); ++i) {
try {
@@ -560,30 +555,16 @@ public class Contact implements ListItem, Blockable {
return getJid().getDomain().toString();
}
- public boolean setAvatar(final Avatar avatar) {
- return setAvatar(avatar, false);
- }
-
- public boolean setAvatar(final Avatar avatar, final boolean previouslyOmittedPepFetch) {
+ public boolean setAvatar(final String avatar) {
if (this.avatar != null && this.avatar.equals(avatar)) {
return false;
}
- if (!previouslyOmittedPepFetch
- && this.avatar != null
- && this.avatar.origin == Avatar.Origin.PEP
- && avatar.origin == Avatar.Origin.VCARD) {
- return false;
- }
this.avatar = avatar;
return true;
}
- public String getAvatarFilename() {
- return avatar == null ? null : avatar.getFilename();
- }
-
- public Avatar getAvatar() {
- return avatar;
+ public String getAvatar() {
+ return this.avatar;
}
public boolean mutualPresenceSubscription() {
@@ -601,6 +582,7 @@ public class Contact implements ListItem, Blockable {
}
@Override
+ @NonNull
public Jid getBlockedJid() {
if (isDomainBlocked()) {
return getJid().getDomain();
@@ -761,7 +743,7 @@ public class Contact implements ListItem, Blockable {
}
public boolean hasAvatarOrPresenceName() {
- return (avatar != null && avatar.getFilename() != null) || presenceName != null;
+ return avatar != null || presenceName != null;
}
public boolean refreshRtpCapability() {
@@ -870,6 +870,7 @@ public class Conversation extends AbstractEntity
}
@Override
+ @NonNull
public Jid getBlockedJid() {
return getContact().getBlockedJid();
}
@@ -3179,7 +3180,7 @@ public class Conversation extends AbstractEntity
Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
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);
+ xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()));
}
if (xmppConnectionService.isOnboarding() && mNode.equals("jabber:iq:register") && !"canceled".equals(command.getAttribute("status")) && xmppConnectionService.getPreferences().contains("onboarding_action")) {
@@ -3,6 +3,7 @@ package eu.siacs.conversations.entities;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,11 +12,14 @@ import io.ipfs.cid.Cid;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import de.gultsch.common.IntMap;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.ui.ConferenceDetailsActivity;
+import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Namespace;
@@ -27,18 +31,40 @@ import eu.siacs.conversations.xml.Element;
import im.conversations.android.xmpp.model.data.Data;
import im.conversations.android.xmpp.model.data.Field;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Item;
+import im.conversations.android.xmpp.model.muc.Role;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Objects;
import java.util.Set;
public class MucOptions {
+ private static final IntMap<Affiliation> AFFILIATION_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder<Affiliation, Integer>()
+ .put(Affiliation.OWNER, 4)
+ .put(Affiliation.ADMIN, 3)
+ .put(Affiliation.MEMBER, 2)
+ .put(Affiliation.NONE, 1)
+ .put(Affiliation.OUTCAST, 0)
+ .build());
+
+ private static final IntMap<Role> ROLE_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder<Role, Integer>()
+ .put(Role.MODERATOR, 3)
+ .put(Role.PARTICIPANT, 2)
+ .put(Role.VISITOR, 1)
+ .put(Role.NONE, 0)
+ .build());
+
public static final String STATUS_CODE_SELF_PRESENCE = "110";
public static final String STATUS_CODE_ROOM_CREATED = "201";
public static final String STATUS_CODE_BANNED = "301";
@@ -48,6 +74,7 @@ public class MucOptions {
public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
public static final String STATUS_CODE_SHUTDOWN = "332";
public static final String STATUS_CODE_TECHNICAL_REASONS = "333";
+ // TODO this should be a list
private final Set<User> users = new HashSet<>();
private final Conversation conversation;
public OnRenameListener onRenameListener = null;
@@ -64,8 +91,8 @@ public class MucOptions {
this.conversation = conversation;
final String nick = getProposedNick(conversation.getAttribute("mucNick"));
this.self = new User(this, createJoinJid(nick), null, nick, new HashSet<>());
- this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
- this.self.role = Role.of(conversation.getAttribute("role"));
+ this.self.affiliation = Item.affiliationOrNone(conversation.getAttribute("affiliation"));
+ this.self.role = Item.roleOrNone(conversation.getAttribute("role"));
}
public Account getAccount() {
@@ -94,8 +121,8 @@ public class MucOptions {
}
}
- public void flagNoAutoPushConfiguration() {
- mAutoPushConfiguration = false;
+ public void setAutoPushConfiguration(final boolean auto) {
+ this.mAutoPushConfiguration = auto;
}
public boolean autoPushConfiguration() {
@@ -128,12 +155,28 @@ public class MucOptions {
public boolean updateConfiguration(final InfoQuery serviceDiscoveryResult) {
this.infoQuery = serviceDiscoveryResult;
- final var roomInfo = getRoomInfoForm();
- String name;
- Field roomConfigName =
+ final String name = getName(serviceDiscoveryResult);
+ boolean changed = conversation.setAttribute("muc_name", name);
+ changed |=
+ conversation.setAttribute(
+ Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
+ changed |=
+ conversation.setAttribute(
+ Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
+ changed |=
+ conversation.setAttribute(
+ Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
+ return changed;
+ }
+
+ private String getName(final InfoQuery serviceDiscoveryResult) {
+ final var roomInfo =
+ serviceDiscoveryResult.getServiceDiscoveryExtension(
+ "http://jabber.org/protocol/muc#roominfo");
+ final Field roomConfigName =
roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_roomname");
if (roomConfigName != null) {
- name = roomConfigName.getValue();
+ return roomConfigName.getValue();
} else {
final var identities = serviceDiscoveryResult.getIdentities();
final String identityName =
@@ -142,34 +185,22 @@ public class MucOptions {
: null;
final Jid jid = conversation.getJid();
if (identityName != null && !identityName.equals(jid == null ? null : jid.getLocal())) {
- name = identityName;
+ return identityName;
} else {
- name = null;
+ return null;
}
}
- boolean changed = conversation.setAttribute("muc_name", name);
- changed |=
- conversation.setAttribute(
- Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly"));
- changed |=
- conversation.setAttribute(
- Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated"));
- changed |=
- conversation.setAttribute(
- Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous"));
- return changed;
}
private Data getRoomInfoForm() {
final var serviceDiscoveryResult = getServiceDiscoveryResult();
return serviceDiscoveryResult == null
? null
- : serviceDiscoveryResult.getServiceDiscoveryExtension(
- "http://jabber.org/protocol/muc#roominfo");
+ : serviceDiscoveryResult.getServiceDiscoveryExtension(Namespace.MUC_ROOM_INFO);
}
public String getAvatar() {
- return account.getRoster().getContact(conversation.getJid()).getAvatarFilename();
+ return account.getRoster().getContact(conversation.getJid()).getAvatar();
}
public boolean hasFeature(String feature) {
@@ -184,49 +215,63 @@ public class MucOptions {
public boolean canInvite() {
final boolean hasPermission =
- !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites();
+ !membersOnly() || self.ranks(Role.MODERATOR) || allowInvites();
return hasPermission && online();
}
public boolean allowInvites() {
- final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
+ final var roomInfo = getRoomInfoForm();
+ if (roomInfo == null) {
+ return false;
+ }
+ final var field = roomInfo.getFieldByName("muc#roomconfig_allowinvites");
return field != null && "1".equals(field.getValue());
}
public boolean canChangeSubject() {
- return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject();
+ return self.ranks(Role.MODERATOR) || participantsCanChangeSubject();
}
public boolean participantsCanChangeSubject() {
- final Field configField = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject");
- final Field infoField = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject");
+ final var roomInfo = getRoomInfoForm();
+ if (roomInfo == null) {
+ return false;
+ }
+ final Field configField = roomInfo.getFieldByName("muc#roomconfig_changesubject");
+ final Field infoField = roomInfo.getFieldByName("muc#roominfo_changesubject");
final Field field = configField != null ? configField : infoField;
return field != null && "1".equals(field.getValue());
}
public boolean allowPm() {
- final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
+ final var roomInfo = getRoomInfoForm();
+ if (roomInfo == null) {
+ return true;
+ }
+ final Field field = roomInfo.getFieldByName("muc#roomconfig_allowpm");
if (field == null) {
return true; // fall back if field does not exists
}
if ("anyone".equals(field.getValue())) {
return true;
} else if ("participants".equals(field.getValue())) {
- return self.getRole().ranks(Role.PARTICIPANT);
+ return self.ranks(Role.PARTICIPANT);
} else if ("moderators".equals(field.getValue())) {
- return self.getRole().ranks(Role.MODERATOR);
+ return self.ranks(Role.MODERATOR);
} else {
return false;
}
}
public boolean allowPmRaw() {
- final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm");
+ final var roomInfo = getRoomInfoForm();
+ final Field field =
+ roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_allowpm");
return field == null || Arrays.asList("anyone", "participants").contains(field.getValue());
}
public boolean participating() {
- return self.getRole().ranks(Role.PARTICIPANT) || !moderated();
+ return self.ranks(Role.PARTICIPANT) || !moderated();
}
public boolean membersOnly() {
@@ -277,7 +322,7 @@ public class MucOptions {
user.realJid != null && user.realJid.equals(account.getJid().asBareJid());
if (membersOnly()
&& nonanonymous()
- && user.affiliation.ranks(Affiliation.MEMBER)
+ && user.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !realJidInMuc
&& !self) {
@@ -468,7 +513,7 @@ public class MucOptions {
synchronized (users) {
ArrayList<User> users = new ArrayList<>();
for (User user : this.users) {
- if (!user.isDomain() && (includeOffline ? (includeOutcast || user.getAffiliation().ranks(Affiliation.NONE)) : user.getRole().ranks(Role.PARTICIPANT))) {
+ if (!user.isDomain() && (includeOffline ? (includeOutcast || user.ranks(Affiliation.NONE)) : user.ranks(Role.PARTICIPANT))) {
users.add(user);
}
}
@@ -480,7 +525,7 @@ public class MucOptions {
synchronized (users) {
ArrayList<User> list = new ArrayList<>();
for (User user : users) {
- if (user.getRole().ranks(role)) {
+ if (user.ranks(role)) {
list.add(user);
}
}
@@ -522,17 +567,17 @@ public class MucOptions {
return subset;
}
- public static List<User> sub(List<User> users, int max) {
- ArrayList<User> subset = new ArrayList<>();
- HashSet<Jid> jids = new HashSet<>();
- for (User user : users) {
- jids.add(user.getAccount().getJid().asBareJid());
- if (user.getRealJid() == null
- || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
+ public static List<User> sub(final List<User> users, final int max) {
+ final var subset = new ArrayList<User>();
+ final var addresses = new HashSet<Jid>();
+ for (final var user : users) {
+ addresses.add(user.getAccount().getJid().asBareJid());
+ final var address = user.getRealJid();
+ if (address == null || (address.getLocal() != null && addresses.add(address))) {
subset.add(user);
}
if (subset.size() >= max) {
- break;
+ return subset;
}
}
return subset;
@@ -781,7 +826,7 @@ public class MucOptions {
ArrayList<Jid> members = new ArrayList<>();
synchronized (users) {
for (User user : users) {
- if (user.affiliation.ranks(Affiliation.MEMBER)
+ if (user.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !user.realJid
.asBareJid()
@@ -794,89 +839,6 @@ public class MucOptions {
return members;
}
- public enum Affiliation {
- OWNER(4, R.string.owner),
- ADMIN(3, R.string.admin),
- MEMBER(2, R.string.member),
- OUTCAST(0, R.string.outcast),
- NONE(1, R.string.no_affiliation);
-
- private final int resId;
- private final int rank;
-
- Affiliation(int rank, int resId) {
- this.resId = resId;
- this.rank = rank;
- }
-
- public static Affiliation of(@Nullable String value) {
- if (value == null) {
- return NONE;
- }
- try {
- return Affiliation.valueOf(value.toUpperCase(Locale.US));
- } catch (IllegalArgumentException e) {
- return NONE;
- }
- }
-
- public int getResId() {
- return resId;
- }
-
- @Override
- public String toString() {
- return name().toLowerCase(Locale.US);
- }
-
- public boolean outranks(Affiliation affiliation) {
- return rank > affiliation.rank;
- }
-
- public boolean ranks(Affiliation affiliation) {
- return rank >= affiliation.rank;
- }
- }
-
- public enum Role {
- MODERATOR(R.string.moderator, 3),
- VISITOR(R.string.visitor, 1),
- PARTICIPANT(R.string.participant, 2),
- NONE(R.string.no_role, 0);
-
- private final int resId;
- private final int rank;
-
- Role(int resId, int rank) {
- this.resId = resId;
- this.rank = rank;
- }
-
- public static Role of(@Nullable String value) {
- if (value == null) {
- return NONE;
- }
- try {
- return Role.valueOf(value.toUpperCase(Locale.US));
- } catch (IllegalArgumentException e) {
- return NONE;
- }
- }
-
- public int getResId() {
- return resId;
- }
-
- @Override
- public String toString() {
- return name().toLowerCase(Locale.US);
- }
-
- public boolean ranks(Role role) {
- return rank >= role.rank;
- }
- }
-
public enum Error {
NO_RESPONSE,
SERVER_NOT_FOUND,
@@ -944,14 +906,14 @@ public class MucOptions {
private Jid fullJid;
protected String nick;
private long pgpKeyId = 0;
- protected Avatar avatar;
+ private String avatar;
private final MucOptions options;
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
protected Set<Hat> hats;
protected String occupantId;
protected boolean online = true;
- public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
+ public User(final MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
this.options = options;
this.fullJid = fullJid;
this.occupantId = occupantId;
@@ -959,12 +921,7 @@ public class MucOptions {
this.hats = hats;
if (occupantId != null && options != null) {
- final var sha1sum = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
- if (sha1sum != null) {
- avatar = new Avatar();
- avatar.sha1sum = sha1sum;
- avatar.owner = fullJid;
- }
+ avatar = options.getConversation().getAttribute("occupantAvatar/" + occupantId);
if (nick == null) {
this.nick = options.getConversation().getAttribute("occupantNick/" + occupantId);
@@ -984,10 +941,6 @@ public class MucOptions {
return fullJid == null ? (options.getConversation().getJid().asBareJid()) : fullJid.asBareJid();
}
- public String getOccupantId() {
- return occupantId;
- }
-
public String getNick() {
return nick == null ? getName() : nick;
}
@@ -1004,16 +957,16 @@ public class MucOptions {
return this.role;
}
- public void setRole(String role) {
- this.role = Role.of(role);
+ public void setRole(final Role role) {
+ this.role = role;
}
public Affiliation getAffiliation() {
return this.affiliation;
}
- public void setAffiliation(String affiliation) {
- this.affiliation = Affiliation.of(affiliation);
+ public void setAffiliation(final Affiliation affiliation) {
+ this.affiliation = affiliation;
}
public Set<Hat> getHats() {
@@ -1022,11 +975,11 @@ public class MucOptions {
public List<MucOptions.Hat> getPseudoHats(Context context) {
List<MucOptions.Hat> hats = new ArrayList<>();
- if (getAffiliation() != MucOptions.Affiliation.NONE) {
- hats.add(new MucOptions.Hat(null, context.getString(getAffiliation().getResId())));
+ if (getAffiliation() != Affiliation.NONE) {
+ hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.affiliationToStringRes(getAffiliation()))));
}
- if (getRole() != MucOptions.Role.PARTICIPANT) {
- hats.add(new MucOptions.Hat(null, context.getString(getRole().getResId())));
+ if (getRole() != Role.PARTICIPANT) {
+ hats.add(new MucOptions.Hat(null, context.getString(ConferenceDetailsActivity.roleToStringRes(getRole()))));
}
return hats;
}
@@ -1047,7 +1000,9 @@ public class MucOptions {
public Contact getContact() {
if (fullJid != null) {
- return getAccount().getRoster().getContactFromContactList(realJid);
+ return realJid == null
+ ? null
+ : getAccount().getRoster().getContactFromContactList(realJid);
} else if (realJid != null) {
return getAccount().getRoster().getContact(realJid);
} else {
@@ -1055,9 +1010,9 @@ public class MucOptions {
}
}
- public boolean setAvatar(final Avatar avatar) {
+ public boolean setAvatar(final String avatar) {
if (occupantId != null) {
- options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar.sha1sum : null);
+ options.getConversation().setAttribute("occupantAvatar/" + occupantId, getContact() == null && avatar != null ? avatar : null);
}
if (this.avatar != null && this.avatar.equals(avatar)) {
return false;
@@ -1068,28 +1023,41 @@ public class MucOptions {
}
public String getAvatar() {
+
+ // TODO prefer potentially better quality avatars from contact
+ // TODO use getContact and if that’s not null and avatar is set use that
+
+ getContact();
+
if (avatar != null) {
- return avatar.getFilename();
+ return avatar;
}
- Avatar avatar =
- realJid != null
- ? getAccount().getRoster().getContact(realJid).getAvatar()
- : null;
- return avatar == null ? null : avatar.getFilename();
+ if (realJid == null) {
+ return null;
+ }
+ final var contact = getAccount().getRoster().getContact(realJid);
+ return contact.getAvatar();
}
public Cid getAvatarCid() {
- if (avatar != null) {
- return avatar.cid();
+ final var sha1 = getAvatar();
+ if (sha1 == null) return null;
+ try {
+ return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1), "sha-1");
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(Config.LOGTAG, "" + e);
+ return null;
}
- Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null;
- return avatar == null ? null : avatar.cid();
}
public Account getAccount() {
return options.getAccount();
}
+ public MucOptions getMucOptions() {
+ return this.options;
+ }
+
public Conversation getConversation() {
return options.getConversation();
}
@@ -1149,9 +1117,9 @@ public class MucOptions {
if (pseudoId && !anotherPseudoId) {
return -1;
}
- if (another.getAffiliation().outranks(getAffiliation())) {
+ if (another.outranks(getAffiliation())) {
return 1;
- } else if (getAffiliation().outranks(another.getAffiliation())) {
+ } else if (outranks(another.getAffiliation())) {
return -1;
} else {
return getComparableName().compareToIgnoreCase(another.getComparableName());
@@ -1198,5 +1166,23 @@ public class MucOptions {
public void setOccupantId(final String occupantId) {
this.occupantId = occupantId;
}
+
+ public String getOccupantId() {
+ return this.occupantId;
+ }
+
+ public boolean ranks(final Role role) {
+ return ROLE_RANKS.getInt(this.role) >= ROLE_RANKS.getInt(role);
+ }
+
+ public boolean ranks(final Affiliation affiliation) {
+ return AFFILIATION_RANKS.getInt(this.affiliation)
+ >= AFFILIATION_RANKS.getInt(affiliation);
+ }
+
+ public boolean outranks(final Affiliation affiliation) {
+ return AFFILIATION_RANKS.getInt(this.affiliation)
+ > AFFILIATION_RANKS.getInt(affiliation);
+ }
}
}
@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
import android.content.Context;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import java.util.Collections;
@@ -13,7 +14,7 @@ public class RawBlockable implements ListItem, Blockable {
private final Account account;
private final Jid jid;
- public RawBlockable(Account account, Jid jid) {
+ public RawBlockable(@NonNull Account account, @NonNull Jid jid) {
this.account = account;
this.jid = jid;
}
@@ -29,6 +30,7 @@ public class RawBlockable implements ListItem, Blockable {
}
@Override
+ @NonNull
public Jid getBlockedJid() {
return this.jid;
}
@@ -1,98 +1,17 @@
package eu.siacs.conversations.entities;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
+import androidx.annotation.NonNull;
import eu.siacs.conversations.android.AbstractPhoneContact;
import eu.siacs.conversations.xmpp.Jid;
+import java.util.List;
+public interface Roster {
-public class Roster {
- private final Account account;
- private final HashMap<Jid, Contact> contacts = new HashMap<>();
- private String version = null;
-
- public Roster(Account account) {
- this.account = account;
- }
-
- public Contact getContactFromContactList(Jid jid) {
- if (jid == null) {
- return null;
- }
- synchronized (this.contacts) {
- Contact contact = contacts.get(jid.asBareJid());
- if (contact != null && contact.showInContactList()) {
- return contact;
- } else {
- return null;
- }
- }
- }
-
- public Contact getContact(final Jid jid) {
- synchronized (this.contacts) {
- if (!contacts.containsKey(jid.asBareJid())) {
- Contact contact = new Contact(jid.asBareJid());
- contact.setAccount(account);
- contacts.put(contact.getJid().asBareJid(), contact);
- return contact;
- }
- return contacts.get(jid.asBareJid());
- }
- }
-
- public void clearPresences() {
- for (Contact contact : getContacts()) {
- contact.clearPresences();
- }
- }
-
- public void markAllAsNotInRoster() {
- for (Contact contact : getContacts()) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- }
- }
-
- public List<Contact> getWithSystemAccounts(Class<?extends AbstractPhoneContact> clazz) {
- int option = Contact.getOption(clazz);
- List<Contact> with = getContacts();
- for(Iterator<Contact> iterator = with.iterator(); iterator.hasNext();) {
- Contact contact = iterator.next();
- if (!contact.getOption(option)) {
- iterator.remove();
- }
- }
- return with;
- }
-
- public List<Contact> getContacts() {
- synchronized (this.contacts) {
- return new ArrayList<>(this.contacts.values());
- }
- }
-
- public void initContact(final Contact contact) {
- if (contact == null) {
- return;
- }
- contact.setAccount(account);
- synchronized (this.contacts) {
- contacts.put(contact.getJid().asBareJid(), contact);
- }
- }
+ List<Contact> getContacts();
- public void setVersion(String version) {
- this.version = version;
- }
+ List<Contact> getWithSystemAccounts(Class<? extends AbstractPhoneContact> clazz);
- public String getVersion() {
- return this.version;
- }
+ Contact getContact(@NonNull final Jid jid);
- public Account getAccount() {
- return this.account;
- }
+ Contact getContactFromContactList(@NonNull final Jid jid);
}
@@ -5,26 +5,27 @@ import java.util.List;
public interface Transferable {
- List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
- List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
+ List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
+ List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
- int STATUS_UNKNOWN = 0x200;
- int STATUS_CHECKING = 0x201;
- int STATUS_FAILED = 0x202;
- int STATUS_OFFER = 0x203;
- int STATUS_DOWNLOADING = 0x204;
- int STATUS_OFFER_CHECK_FILESIZE = 0x206;
- int STATUS_UPLOADING = 0x207;
- int STATUS_CANCELLED = 0x208;
+ int GCM_AUTHENTICATION_TAG_LENGTH = 16;
+ int STATUS_UNKNOWN = 0x200;
+ int STATUS_CHECKING = 0x201;
+ int STATUS_FAILED = 0x202;
+ int STATUS_OFFER = 0x203;
+ int STATUS_DOWNLOADING = 0x204;
+ int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+ int STATUS_UPLOADING = 0x207;
+ int STATUS_CANCELLED = 0x208;
- boolean start();
+ boolean start();
- int getStatus();
+ int getStatus();
- Long getFileSize();
+ Long getFileSize();
- int getProgress();
+ int getProgress();
- void cancel();
+ void cancel();
}
@@ -20,25 +20,15 @@ import java.io.FileInputStream;
import java.io.IOException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Bookmark;
-import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Request;
-import java.nio.ByteBuffer;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
@@ -62,6 +52,10 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.stanza.Iq;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+import org.whispersystems.libsignal.state.SignedPreKeyRecord;
public class IqGenerator extends AbstractGenerator {
@@ -69,32 +63,6 @@ public class IqGenerator extends AbstractGenerator {
super(service);
}
- public Iq entityTimeResponse(final Iq request) {
- final Iq packet = request.generateResponse(Iq.Type.RESULT);
- Element time = packet.addChild("time", "urn:xmpp:time");
- final long now = System.currentTimeMillis();
- time.addChild("utc").setContent(getTimestamp(now));
- TimeZone ourTimezone = TimeZone.getDefault();
- long offsetSeconds = ourTimezone.getOffset(now) / 1000;
- long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
- long offsetHours = offsetSeconds / 3600;
- String hours;
- if (offsetHours < 0) {
- hours = String.format(Locale.US, "%03d", offsetHours);
- } else {
- hours = String.format(Locale.US, "%02d", offsetHours);
- }
- String minutes = String.format(Locale.US, "%02d", offsetMinutes);
- time.addChild("tzo").setContent(hours + ":" + minutes);
- return packet;
- }
-
- public static Iq purgeOfflineMessages() {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
- return packet;
- }
-
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);
@@ -129,97 +97,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq retrieveBookmarks() {
- return retrieve(Namespace.BOOKMARKS2, null);
- }
-
- public Iq retrieveMds() {
- return retrieve(Namespace.MDS_DISPLAYED, null);
- }
-
- public Iq publishNick(String nick) {
- final Element item = new Element("item");
- item.setAttribute("id", "current");
- item.addChild("nick", Namespace.NICK).setContent(nick);
- return publish(Namespace.NICK, item);
- }
-
- public Iq deleteNode(final String node) {
- final var packet = new Iq(Iq.Type.SET);
- final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
- pubsub.addChild("delete").setAttribute("node", node);
- return packet;
- }
-
- public Iq deleteItem(final String node, final String id) {
- final var packet = new Iq(Iq.Type.SET);
- final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
- final Element retract = pubsub.addChild("retract");
- retract.setAttribute("node", node);
- retract.setAttribute("notify", "true");
- retract.addChild("item").setAttribute("id", id);
- return packet;
- }
-
- public Iq publishAvatar(Avatar avatar, Bundle options) {
- final Element item = new Element("item");
- item.setAttribute("id", avatar.sha1sum);
- final Element data = item.addChild("data", Namespace.AVATAR_DATA);
- data.setContent(avatar.image);
- return publish(Namespace.AVATAR_DATA, item, options);
- }
-
- public Iq publishElement(
- final String namespace, final Element element, String id, final Bundle options) {
- final Element item = new Element("item");
- item.setAttribute("id", id);
- item.addChild(element);
- return publish(namespace, item, options);
- }
-
- public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) {
- final Element item = new Element("item");
- item.setAttribute("id", avatar.sha1sum);
- final Element metadata = item.addChild("metadata", Namespace.AVATAR_METADATA);
- final Element info = metadata.addChild("info");
- info.setAttribute("bytes", avatar.size);
- info.setAttribute("id", avatar.sha1sum);
- info.setAttribute("height", avatar.height);
- info.setAttribute("width", avatar.height);
- info.setAttribute("type", avatar.type);
- return publish(Namespace.AVATAR_METADATA, item, options);
- }
-
- public Iq retrievePepAvatar(final Avatar avatar) {
- final Element item = new Element("item");
- item.setAttribute("id", avatar.sha1sum);
- final var packet = retrieve(Namespace.AVATAR_DATA, item);
- packet.setTo(avatar.owner);
- return packet;
- }
-
- 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 Iq retrieveVcardAvatar(final Jid to) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(to);
- packet.addChild("vCard", "vcard-temp");
- return packet;
- }
-
- public Iq retrieveAvatarMetaData(final Jid to) {
- final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
- if (to != null) {
- packet.setTo(to);
- }
- return packet;
- }
-
public Iq retrieveDeviceIds(final Jid to) {
final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if (to != null) {
@@ -252,44 +129,6 @@ public class IqGenerator extends AbstractGenerator {
return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
}
- public Element publishBookmarkItem(final Bookmark bookmark) {
- final String name = bookmark.getBookmarkName();
- final String nick = bookmark.getNick();
- final String password = bookmark.getPassword();
- final boolean autojoin = bookmark.autojoin();
- final Element conference = new Element("conference", Namespace.BOOKMARKS2);
- if (!Strings.isNullOrEmpty(name)) {
- conference.setAttribute("name", name);
- }
- if (!Strings.isNullOrEmpty(nick)) {
- conference.addChild("nick").setContent(nick);
- }
- if (password != null) {
- conference.addChild("password").setContent(password);
- }
- conference.setAttribute("autojoin", String.valueOf(autojoin));
- conference.addChild(bookmark.getExtensions());
- return conference;
- }
-
- public Element mdsDisplayed(final String stanzaId, final Conversation conversation) {
- final Jid by;
- if (conversation.getMode() == Conversation.MODE_MULTI) {
- by = conversation.getJid().asBareJid();
- } else {
- by = conversation.getAccount().getJid().asBareJid();
- }
- return mdsDisplayed(stanzaId, by);
- }
-
- private Element mdsDisplayed(final String stanzaId, final Jid by) {
- final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED);
- final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS);
- stanzaIdElement.setAttribute("id", stanzaId);
- stanzaIdElement.setAttribute("by", by);
- return displayed;
- }
-
public Iq publishBundles(
final SignedPreKeyRecord signedPreKeyRecord,
final IdentityKey identityKey,
@@ -346,7 +185,7 @@ public class IqGenerator extends AbstractGenerator {
public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final Iq packet = new Iq(Iq.Type.SET);
- final Element query = packet.query(mam.version.namespace);
+ final Element query = packet.addChild("query", mam.version.namespace);
query.setAttribute("queryid", mam.getQueryId());
final Data data = new Data();
data.setFormType(mam.version.namespace);
@@ -375,77 +214,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq generateGetBlockList() {
- final Iq iq = new Iq(Iq.Type.GET);
- iq.addChild("blocklist", Namespace.BLOCKING);
-
- return iq;
- }
-
- public Iq generateSetBlockRequest(
- final Jid jid, final boolean reportSpam, final String serverMsgId) {
- final Iq iq = new Iq(Iq.Type.SET);
- final Element block = iq.addChild("block", Namespace.BLOCKING);
- final Element item = block.addChild("item").setAttribute("jid", jid);
- if (reportSpam) {
- final Element report = item.addChild("report", Namespace.REPORTING);
- report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM);
- if (serverMsgId != null) {
- final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS);
- stanzaId.setAttribute("by", jid);
- stanzaId.setAttribute("id", serverMsgId);
- }
- }
- Log.d(Config.LOGTAG, iq.toString());
- return iq;
- }
-
- public Iq generateSetUnblockRequest(final Jid jid) {
- final Iq iq = new Iq(Iq.Type.SET);
- final Element block = iq.addChild("unblock", Namespace.BLOCKING);
- block.addChild("item").setAttribute("jid", jid);
- return iq;
- }
-
- public Iq generateSetPassword(final Account account, final String newPassword) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(account.getDomain());
- final Element query = packet.addChild("query", Namespace.REGISTER);
- final Jid jid = account.getJid();
- query.addChild("username").setContent(jid.getLocal());
- query.addChild("password").setContent(newPassword);
- return packet;
- }
-
- public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
- List<Jid> jids = new ArrayList<>();
- jids.add(jid);
- return changeAffiliation(conference, jids, affiliation);
- }
-
- public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(conference.getJid().asBareJid());
- packet.setFrom(conference.getAccount().getJid());
- Element query = packet.query("http://jabber.org/protocol/muc#admin");
- for (Jid jid : jids) {
- Element item = query.addChild("item");
- item.setAttribute("jid", jid);
- item.setAttribute("affiliation", affiliation);
- }
- return packet;
- }
-
- public Iq changeRole(Conversation conference, String nick, String role) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(conference.getJid().asBareJid());
- packet.setFrom(conference.getAccount().getJid());
- Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
- item.setAttribute("nick", nick);
- item.setAttribute("role", role);
- return packet;
- }
-
public Iq moderateMessage(Account account, Message m, String reason) {
final var packet = new Iq(Iq.Type.SET);
packet.setTo(m.getConversation().getJid().asBareJid());
@@ -459,58 +227,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- 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);
- request.setAttribute("size", file.getExpectedSize());
- request.setAttribute("content-type", mime);
- return packet;
- }
-
- public Iq requestHttpUploadSlot(
- final Jid host, final DownloadableFile file, final String mime) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(host);
- final var request = packet.addExtension(new Request());
- request.setFilename(convertFilename(file.getName()));
- request.setSize(file.getExpectedSize());
- return packet;
- }
-
- private static String convertFilename(String name) {
- int pos = name.indexOf('.');
- if (pos != -1) {
- try {
- UUID uuid = UUID.fromString(name.substring(0, pos));
- ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
- bb.putLong(uuid.getMostSignificantBits());
- bb.putLong(uuid.getLeastSignificantBits());
- return Base64.encodeToString(
- bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
- + name.substring(pos);
- } catch (Exception e) {
- return name;
- }
- } else {
- return name;
- }
- }
-
- public static Iq generateCreateAccountWithCaptcha(
- final Account account, final String id, final Data data) {
- final Iq register = new Iq(Iq.Type.SET);
- register.setFrom(account.getJid().asBareJid());
- register.setTo(account.getDomain());
- register.setId(id);
- Element query = register.query(Namespace.REGISTER);
- if (data != null) {
- query.addChild(data);
- }
- return register;
- }
-
public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
return pushTokenToAppServer(appServer, token, deviceId, null);
}
@@ -569,42 +285,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq queryAffiliation(Conversation conversation, String affiliation) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(conversation.getJid().asBareJid());
- packet.query("http://jabber.org/protocol/muc#admin")
- .addChild("item")
- .setAttribute("affiliation", affiliation);
- return packet;
- }
-
- public static Bundle defaultGroupChatConfiguration() {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_membersonly", "1");
- options.putString("muc#roomconfig_publicroom", "0");
- options.putString("muc#roomconfig_whois", "anyone");
- options.putString("muc#roomconfig_changesubject", "0");
- options.putString("muc#roomconfig_allowinvites", "0");
- options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
- options.putString("mam", "1"); // ejabberd community
- options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
- return options;
- }
-
- public static Bundle defaultChannelConfiguration() {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_membersonly", "0");
- options.putString("muc#roomconfig_publicroom", "1");
- options.putString("muc#roomconfig_whois", "moderators");
- options.putString("muc#roomconfig_changesubject", "0");
- options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
- options.putString("mam", "1"); // ejabberd community
- options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
- return options;
- }
-
public Iq requestPubsubConfiguration(Jid jid, String node) {
return pubsubConfiguration(jid, node, null);
}
@@ -18,6 +18,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import im.conversations.android.xmpp.model.correction.Replace;
+import im.conversations.android.xmpp.model.hints.NoStore;
import im.conversations.android.xmpp.model.hints.Store;
import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions;
@@ -107,7 +108,7 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setAxolotlMessage(axolotlMessage.toElement());
packet.setBody(OMEMO_FALLBACK_MESSAGE);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
packet.addChild("encryption", "urn:xmpp:eme:0")
.setAttribute("name", "OMEMO")
.setAttribute("namespace", AxolotlService.PEP_PREFIX);
@@ -121,7 +122,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(to);
packet.setAxolotlMessage(axolotlMessage.toElement());
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addChild(new Store());
return packet;
}
@@ -191,8 +192,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
- packet.addChild("no-store", "urn:xmpp:hints");
- packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store*
+ packet.addExtension(new NoStore());
return packet;
}
@@ -218,7 +218,7 @@ public class MessageGenerator extends AbstractGenerator {
} else {
displayed.setAttribute("id", message.getRemoteMsgId());
}
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -244,18 +244,7 @@ public class MessageGenerator extends AbstractGenerator {
final var thread = inReplyTo.getThread();
if (thread != null) packet.addChild(thread);
- packet.addChild("store", "urn:xmpp:hints");
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(
- Conversation conversation, String subject) {
- im.conversations.android.xmpp.model.stanza.Message packet =
- new im.conversations.android.xmpp.model.stanza.Message();
- packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
- packet.setTo(conversation.getJid().asBareJid());
- packet.addChild("subject").setContent(subject);
- packet.setFrom(conversation.getAccount().getJid().asBareJid());
+ packet.addExtension(new Store());
return packet;
}
@@ -271,40 +260,6 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
- public im.conversations.android.xmpp.model.stanza.Message directInvite(
- final Conversation conversation, final Jid contact) {
- im.conversations.android.xmpp.model.stanza.Message packet =
- new im.conversations.android.xmpp.model.stanza.Message();
- packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
- packet.setTo(contact);
- packet.setFrom(conversation.getAccount().getJid());
- Element x = packet.addChild("x", "jabber:x:conference");
- x.setAttribute("jid", conversation.getJid().asBareJid());
- String password = conversation.getMucOptions().getPassword();
- if (password != null) {
- x.setAttribute("password", password);
- }
- if (contact.isFullJid()) {
- packet.addChild("no-store", "urn:xmpp:hints");
- packet.addChild("no-copy", "urn:xmpp:hints");
- }
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Message invite(
- final Conversation conversation, final Jid contact) {
- final var packet = new im.conversations.android.xmpp.model.stanza.Message();
- packet.setTo(conversation.getJid().asBareJid());
- packet.setFrom(conversation.getAccount().getJid());
- Element x = new Element("x");
- x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
- Element invite = new Element("invite");
- invite.setAttribute("to", contact.asBareJid());
- x.addChild(invite);
- packet.addChild(x);
- return packet;
- }
-
public im.conversations.android.xmpp.model.stanza.Message received(
final Jid from,
final String id,
@@ -324,7 +279,7 @@ public class MessageGenerator extends AbstractGenerator {
packet.setFrom(account.getJid());
packet.setTo(to);
packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -338,7 +293,7 @@ public class MessageGenerator extends AbstractGenerator {
finish.setAttribute("id", sessionId);
final Element reasonElement = finish.addChild("reason", Namespace.JINGLE);
reasonElement.addChild(reason.toString());
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -358,7 +313,7 @@ public class MessageGenerator extends AbstractGenerator {
.setAttribute("media", media.toString());
}
packet.addChild("request", "urn:xmpp:receipts");
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -373,7 +328,7 @@ public class MessageGenerator extends AbstractGenerator {
final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
@@ -388,7 +343,7 @@ public class MessageGenerator extends AbstractGenerator {
final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", sessionId);
propose.addChild("description", Namespace.JINGLE_APPS_RTP);
- packet.addChild("store", "urn:xmpp:hints");
+ packet.addExtension(new Store());
return packet;
}
}
@@ -1,11 +1,8 @@
package eu.siacs.conversations.generator;
-import android.text.TextUtils;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
@@ -15,50 +12,6 @@ public class PresenceGenerator extends AbstractGenerator {
super(service);
}
- private im.conversations.android.xmpp.model.stanza.Presence subscription(
- String type, Contact contact) {
- im.conversations.android.xmpp.model.stanza.Presence packet =
- new im.conversations.android.xmpp.model.stanza.Presence();
- packet.setAttribute("type", type);
- packet.setTo(contact.getJid());
- packet.setFrom(contact.getAccount().getJid().asBareJid());
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
- final Contact contact) {
- return requestPresenceUpdatesFrom(contact, null);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
- final Contact contact, final String preAuth) {
- im.conversations.android.xmpp.model.stanza.Presence packet =
- subscription("subscribe", contact);
- String displayName = contact.getAccount().getDisplayName();
- if (!TextUtils.isEmpty(displayName)) {
- packet.addChild("nick", Namespace.NICK).setContent(displayName);
- }
- if (preAuth != null) {
- packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
- }
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(
- Contact contact) {
- return subscription("unsubscribe", contact);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(
- Contact contact) {
- return subscription("unsubscribed", contact);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(
- Contact contact) {
- return subscription("subscribed", contact);
- }
-
public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Availability status) {
return selfPresence(account, status, true, null);
}
@@ -1,22 +1,14 @@
package eu.siacs.conversations.http;
-import android.util.Log;
+import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
+import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Longs;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Locale;
-
-import javax.net.ssl.SSLHandshakeException;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -28,13 +20,23 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+import javax.net.ssl.SSLHandshakeException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
-
-import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
public class HttpDownloadConnection implements Transferable {
@@ -90,7 +92,8 @@ public class HttpDownloadConnection implements Transferable {
} else {
mUrl = AesGcmURL.of(message.getRawBody().split("\n")[0]);
}
- final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
+ final AbstractConnectionManager.Extension extension =
+ AbstractConnectionManager.Extension.of(mUrl.encodedPath());
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
this.message.setEncryption(Message.ENCRYPTION_PGP);
} else if (message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
@@ -100,23 +103,27 @@ public class HttpDownloadConnection implements Transferable {
if (ext == null && fileParams.getMediaType() != null) {
ext = MimeUtils.guessExtensionFromMimeType(fileParams.getMediaType());
}
- final String filename = Strings.isNullOrEmpty(ext) ? message.getUuid() : String.format("%s.%s", message.getUuid(), ext);
+ final String filename =
+ Strings.isNullOrEmpty(ext)
+ ? message.getUuid()
+ : String.format("%s.%s", message.getUuid(), ext);
mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename);
setupFile();
- if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
+ if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL
+ && this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
final Long knownFileSize;
- if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP
+ || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
knownFileSize = null;
} else {
knownFileSize = message.getFileParams().size;
}
- Log.d(Config.LOGTAG,"knownFileSize: "+knownFileSize+", body="+message.getRawBody());
if (knownFileSize != null && interactive) {
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL
&& this.file.getKey() != null) {
- this.file.setExpectedSize(knownFileSize + 16);
+ this.file.setExpectedSize(knownFileSize + GCM_AUTHENTICATION_TAG_LENGTH);
} else {
this.file.setExpectedSize(knownFileSize);
}
@@ -132,9 +139,16 @@ public class HttpDownloadConnection implements Transferable {
private void setupFile() {
final String reference = mUrl.fragment();
if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
- this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid());
+ this.file =
+ new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid());
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
- Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
+ Log.d(
+ Config.LOGTAG,
+ "create temporary OMEMO encrypted file: "
+ + this.file.getAbsolutePath()
+ + "("
+ + message.getMimeType()
+ + ")");
} else {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
}
@@ -163,29 +177,30 @@ public class HttpDownloadConnection implements Transferable {
}
private void decryptFile() throws IOException {
- final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
+ final DownloadableFile outputFile =
+ mXmppConnectionService.getFileBackend().getFile(message, true);
- if (outputFile.getParentFile().mkdirs()) {
+ final var directory = outputFile.getParentFile();
+ if (directory != null && directory.mkdirs()) {
Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
}
if (!outputFile.createNewFile()) {
Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath());
}
+ final var cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
+ cipher.init(
+ false, new AEADParameters(new KeyParameter(this.file.getKey()), 128, file.getIv()));
+ try (final InputStream is = new FileInputStream(this.file);
+ final CipherOutputStream outputStream =
+ new CipherOutputStream(new FileOutputStream(outputFile), cipher)) {
+ ByteStreams.copy(is, outputStream);
+ }
- final InputStream is = new FileInputStream(this.file);
-
- outputFile.setKey(this.file.getKey());
- outputFile.setIv(this.file.getIv());
- final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true);
-
- ByteStreams.copy(is, os);
-
- FileBackend.close(is);
- FileBackend.close(os);
-
- if (!file.delete()) {
- Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
+ if (file.delete()) {
+ Log.w(
+ Config.LOGTAG,
+ "deleted temporary OMEMO encrypted file " + file.getAbsolutePath());
}
file = outputFile;
@@ -194,7 +209,11 @@ public class HttpDownloadConnection implements Transferable {
private void finish() {
boolean notify = acceptedAutomatically && !message.isRead() && cb == null;
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
+ notify =
+ message.getConversation()
+ .getAccount()
+ .getPgpDecryptionService()
+ .decrypt(message, notify);
}
final DownloadableFile tmp = file;
final String extension = MimeUtils.extractRelevantExtension(tmp.getName());
@@ -216,11 +235,17 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.updateMessage(message);
mHttpConnectionManager.finishConnection(this);
final boolean notifyAfterScan = notify;
- mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
- if (notifyAfterScan) {
- mXmppConnectionService.getNotificationService().push(message);
- }
- });
+ final DownloadableFile file =
+ mXmppConnectionService.getFileBackend().getFile(message, true);
+ mXmppConnectionService
+ .getFileBackend()
+ .updateMediaScanner(
+ file,
+ () -> {
+ if (notifyAfterScan) {
+ mXmppConnectionService.getNotificationService().push(message);
+ }
+ });
}
private void decryptIfNeeded() throws IOException {
@@ -245,7 +270,8 @@ public class HttpDownloadConnection implements Transferable {
} else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else if (e instanceof FileWriterException) {
- mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
+ mXmppConnectionService.showErrorToastInUi(
+ R.string.download_failed_could_not_write_file);
} else if (e instanceof InvalidFileException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_invalid_file);
} else {
@@ -289,7 +315,6 @@ public class HttpDownloadConnection implements Transferable {
this.interactive = interactive;
}
-
@Override
public void run() {
check();
@@ -301,7 +326,10 @@ public class HttpDownloadConnection implements Transferable {
showToastForException(e);
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
- HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ HttpDownloadConnection.this
+ .mXmppConnectionService
+ .getNotificationService()
+ .push(message);
}
cancel();
}
@@ -311,7 +339,7 @@ public class HttpDownloadConnection implements Transferable {
try {
size = retrieveFileSize();
} catch (final Exception e) {
- Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
+ Log.d(Config.LOGTAG, "could not retrieve file size", e);
retrieveFailed(e);
return;
}
@@ -339,47 +367,59 @@ public class HttpDownloadConnection implements Transferable {
private long retrieveFileSize() throws IOException {
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
changeStatus(STATUS_CHECKING);
- final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
- mUrl,
- message.getConversation().getAccount(),
- interactive
- );
- final Request request = new Request.Builder()
- .url(URL.stripFragment(mUrl))
- .addHeader("Accept-Encoding", "identity")
- .head()
- .build();
+ final OkHttpClient client =
+ mHttpConnectionManager.buildHttpClient(
+ mUrl, message.getConversation().getAccount(), interactive);
+ final Request request =
+ new Request.Builder()
+ .url(URL.stripFragment(mUrl))
+ .addHeader("Accept-Encoding", "identity")
+ .head()
+ .build();
mostRecentCall = client.newCall(request);
- try {
- final Response response = mostRecentCall.execute();
+ try (final Response response = mostRecentCall.execute()) {
throwOnInvalidCode(response);
final String contentLength = response.header("Content-Length");
final String contentType = response.header("Content-Type");
- final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
+ final AbstractConnectionManager.Extension extension =
+ AbstractConnectionManager.Extension.of(mUrl.encodedPath());
if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
if (fileExtension != null) {
- mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType);
- Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type");
+ mXmppConnectionService
+ .getFileBackend()
+ .setupRelativeFilePath(
+ message,
+ String.format("%s.%s", message.getUuid(), fileExtension),
+ contentType);
+ Log.d(
+ Config.LOGTAG,
+ "rewriting name after not finding extension in url but in content"
+ + " type");
setupFile();
}
}
- if (Strings.isNullOrEmpty(contentLength)) {
+ final Long size = Longs.tryParse(Strings.nullToEmpty(contentLength));
+ if (size == null || size < 0) {
throw new IOException("no content-length found in HEAD response");
}
- final long size = Long.parseLong(contentLength, 10);
- if (size < 0) {
- throw new IOException("Server reported negative file size");
- }
return size;
- } catch (final IOException e) {
- Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
- throw e;
- } catch (final NumberFormatException e) {
- throw new IOException(e);
}
}
+ }
+ private void persistFileSize(final long size) {
+ final Message.FileParams fileParams = message.getFileParams();
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && file.getKey() != null) {
+ // store the file size of the clear text file. If we resume the download we will add the
+ // auth tag size again
+ // this is equivalent to use updating file params *after* download (which would take the
+ // clear text size as well)
+ FileBackend.updateFileParams(
+ message, fileParams.url, size - GCM_AUTHENTICATION_TAG_LENGTH);
+ } else {
+ FileBackend.updateFileParams(message, fileParams.url, size);
+ }
}
private class FileDownloader implements Runnable {
@@ -401,12 +441,19 @@ public class HttpDownloadConnection implements Transferable {
} catch (final SSLHandshakeException e) {
changeStatus(STATUS_OFFER);
} catch (final Exception e) {
- Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": unable to download file", e);
+ Log.d(
+ Config.LOGTAG,
+ message.getConversation().getAccount().getJid().asBareJid()
+ + ": unable to download file",
+ e);
if (interactive) {
showToastForException(e);
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
- HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+ HttpDownloadConnection.this
+ .mXmppConnectionService
+ .getNotificationService()
+ .push(message);
}
cancel();
} finally {
@@ -415,53 +462,98 @@ public class HttpDownloadConnection implements Transferable {
}
private void download() throws Exception {
- final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
- mUrl,
- message.getConversation().getAccount(),
- interactive
- );
+ final long expected = file.getExpectedSize();
+ final var fileExists = file.exists();
+ final var existingFileSize = fileExists ? file.length() : -1L;
- final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl));
+ if (fileExists) {
+ if (expected > 0 && existingFileSize == expected) {
+ Log.d(Config.LOGTAG, "file already exits (presumably decryption failure)");
+ return;
+ }
+ }
+ final OkHttpClient client =
+ mHttpConnectionManager.buildHttpClient(
+ mUrl, message.getConversation().getAccount(), interactive);
- final long expected = file.getExpectedSize();
- final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
+ final Request.Builder requestBuilder =
+ new Request.Builder().url(URL.stripFragment(mUrl));
+
+ final boolean tryResume =
+ fileExists && existingFileSize > 0 && existingFileSize < expected;
final long resumeSize;
if (tryResume) {
- resumeSize = file.getSize();
- Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
- requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
+ resumeSize = existingFileSize;
+ Log.d(
+ Config.LOGTAG,
+ "http download trying resume after " + resumeSize + " of " + expected);
+ requestBuilder.addHeader(
+ "Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize));
} else {
resumeSize = 0;
}
final Request request = requestBuilder.build();
mostRecentCall = client.newCall(request);
- final Response response = mostRecentCall.execute();
- throwOnInvalidCode(response);
- final String contentRange = response.header("Content-Range");
- final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
- final InputStream inputStream = response.body().byteStream();
- final OutputStream outputStream;
- long transmitted = 0;
- if (tryResume && serverResumed) {
- Log.d(Config.LOGTAG, "server resumed");
- transmitted = file.getSize();
- updateProgress(Math.round(((double) transmitted / expected) * 100));
- outputStream = AbstractConnectionManager.createOutputStream(file, true, false);
- } else {
- final String contentLength = response.header("Content-Length");
- final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength);
- if (expected != size) {
- Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")");
+ try (final Response response = mostRecentCall.execute()) {
+ throwOnInvalidCode(response);
+ final String contentRange = response.header("Content-Range");
+ final boolean serverResumed =
+ tryResume
+ && contentRange != null
+ && contentRange.startsWith("bytes " + resumeSize + "-");
+ final var body = response.body();
+ if (body == null) {
+ throw new IOException("response body was null");
}
- file.getParentFile().mkdirs();
- Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath());
- if (!file.exists() && !file.createNewFile()) {
- throw new FileWriterException(file);
+ final InputStream inputStream = body.byteStream();
+ if (tryResume && serverResumed) {
+ Log.d(Config.LOGTAG, "server resumed");
+ final var offset = file.getSize();
+ try (final OutputStream os = new FileOutputStream(file, true)) {
+ copy(inputStream, os, offset, expected);
+ }
+ } else {
+ final String contentLength = response.header("Content-Length");
+ final Long size = Longs.tryParse(Strings.nullToEmpty(contentLength));
+ if (size == null) {
+ Log.d(Config.LOGTAG, "no content-length in GET response (probably gzip)");
+ } else {
+ if (expected != size) {
+ if (expected == 0) {
+ // this means we got 0 (unknown) on HEAD. We won't download the file
+ // but we update the file size so the user can try it again now that
+ // the actual file size is known
+ persistFileSize(size);
+ }
+ throw new IOException(
+ "Content-Length in GET response did not match expected size");
+ }
+ }
+ final var directory = file.getParentFile();
+ if (directory != null && directory.mkdirs()) {
+ Log.d(Config.LOGTAG, "create directory " + directory.getAbsolutePath());
+ }
+ Log.d(Config.LOGTAG, "creating file: " + file.getAbsolutePath());
+ if (!file.exists() && !file.createNewFile()) {
+ throw new FileWriterException(file);
+ }
+ try (final OutputStream os = new FileOutputStream(file)) {
+ copy(inputStream, os, 0, expected);
+ }
}
- outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
}
+ }
+
+ private void copy(
+ final InputStream inputStream,
+ final OutputStream outputStream,
+ final long offset,
+ final long expected)
+ throws IOException, FileWriterException {
+ long transmitted = offset;
int count;
final byte[] buffer = new byte[4096];
+ updateProgress(Math.round(((double) transmitted / expected) * 100));
while ((count = inputStream.read(buffer)) != -1) {
transmitted += count;
try {
@@ -470,11 +562,11 @@ public class HttpDownloadConnection implements Transferable {
throw new FileWriterException(file);
}
if (transmitted > expected) {
- throw new InvalidFileException(String.format("File exceeds expected size of %d", expected));
+ throw new InvalidFileException(
+ String.format("File exceeds expected size of %d", expected));
}
updateProgress(Math.round(((double) transmitted / expected) * 100));
}
- outputStream.flush();
}
private void updateImageBounds() {
@@ -490,7 +582,6 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
mXmppConnectionService.updateMessage(message);
}
-
}
private static void throwOnInvalidCode(final Response response) throws IOException {
@@ -505,6 +596,5 @@ public class HttpDownloadConnection implements Transferable {
private InvalidFileException(final String message) {
super(message);
}
-
}
}
@@ -17,6 +17,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -39,12 +40,12 @@ public class HttpUploadConnection
private boolean delayed = false;
private DownloadableFile file;
private final Message message;
- private SlotRequester.Slot slot;
+ private HttpUploadManager.Slot slot;
private byte[] key = null;
private long transmitted = 0;
private Call mostRecentCall;
- private ListenableFuture<SlotRequester.Slot> slotFuture;
+ private ListenableFuture<HttpUploadManager.Slot> slotFuture;
private Runnable cb;
public HttpUploadConnection(final Message message, final HttpConnectionManager httpConnectionManager, final Runnable cb) {
@@ -79,7 +80,7 @@ public class HttpUploadConnection
@Override
public void cancel() {
- final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
+ final ListenableFuture<HttpUploadManager.Slot> slotFuture = this.slotFuture;
if (slotFuture != null && !slotFuture.isDone()) {
if (slotFuture.cancel(true)) {
Log.d(Config.LOGTAG, "cancelled slot requester");
@@ -95,7 +96,7 @@ public class HttpUploadConnection
private void fail(String errorMessage) {
finish();
final Call call = this.mostRecentCall;
- final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
+ final Future<HttpUploadManager.Slot> slotFuture = this.slotFuture;
final boolean cancelled =
(call != null && call.isCanceled())
|| (slotFuture != null && slotFuture.isCancelled());
@@ -111,8 +112,9 @@ public class HttpUploadConnection
message.setTransferable(null);
}
- public void init(boolean delay) {
+ public void init(final boolean delay) {
final Account account = message.getConversation().getAccount();
+ final var connection = account.getXmppConnection();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
final String mime;
if (message.getEncryption() == Message.ENCRYPTION_PGP
@@ -131,12 +133,12 @@ public class HttpUploadConnection
}
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
message.resetFileParams();
- this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime);
+ this.slotFuture = connection.getManager(HttpUploadManager.class).request(file, mime);
Futures.addCallback(
this.slotFuture,
new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable SlotRequester.Slot result) {
+ public void onSuccess(@Nullable HttpUploadManager.Slot result) {
HttpUploadConnection.this.slot = result;
try {
HttpUploadConnection.this.upload();
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.http;
-
-import android.util.Log;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Header;
-import im.conversations.android.xmpp.model.upload.Slot;
-import java.util.Map;
-import okhttp3.Headers;
-import okhttp3.HttpUrl;
-
-public class SlotRequester {
-
- private final XmppConnectionService service;
-
- public SlotRequester(XmppConnectionService service) {
- this.service = service;
- }
-
- public ListenableFuture<Slot> request(
- final Account account, final DownloadableFile file, final String mime) {
- final var result =
- account.getXmppConnection()
- .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
- if (result == null) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("No HTTP upload host found"));
- }
- return requestHttpUpload(account, result.getKey(), file, mime);
- }
-
- private ListenableFuture<Slot> requestHttpUpload(
- final Account account, final Jid host, final DownloadableFile file, final String mime) {
- final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
- final var iqFuture = service.sendIqPacket(account, request);
- return Futures.transform(
- iqFuture,
- response -> {
- final var slot =
- response.getExtension(
- im.conversations.android.xmpp.model.upload.Slot.class);
- if (slot == null) {
- Log.d(Config.LOGTAG, "-->" + response);
- throw new IllegalStateException("Slot not found in IQ response");
- }
- final var getUrl = slot.getGetUrl();
- final var put = slot.getPut();
- if (getUrl == null || put == null) {
- throw new IllegalStateException("Missing get or put in slot response");
- }
- final var putUrl = put.getUrl();
- if (putUrl == null) {
- throw new IllegalStateException("Missing put url");
- }
- final var headers = new ImmutableMap.Builder<String, String>();
- for (final Header header : put.getHeaders()) {
- final String name = header.getHeaderName();
- final String value = header.getContent();
- if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
- continue;
- }
- headers.put(name, value.trim());
- }
- headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
- return new Slot(putUrl, getUrl, headers.buildKeepingLast());
- },
- MoreExecutors.directExecutor());
- }
-
- public static class Slot {
- public final HttpUrl put;
- public final HttpUrl get;
- public final Headers headers;
-
- private Slot(HttpUrl put, HttpUrl get, Headers headers) {
- this.put = put;
- this.get = get;
- this.headers = headers;
- }
-
- private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
- this.put = put;
- this.get = getUrl;
- this.headers = Headers.of(headers);
- }
- }
-}
@@ -136,7 +136,7 @@ public abstract class AbstractParser extends XmppConnection.Delegate {
return item.findChildContent("data", "urn:xmpp:avatar:data");
}
- public static MucOptions.User parseItem(Conversation conference, Element item) {
+ public static MucOptions.User parseItem(final Conversation conference, Element item) {
return parseItem(conference,item,null,null,null,new Element("hats", "urn:xmpp:hats:0"));
}
@@ -172,8 +172,8 @@ public abstract class AbstractParser extends XmppConnection.Delegate {
if (Jid.Invalid.isValid(realJid)) {
user.setRealJid(realJid);
}
- user.setAffiliation(affiliation);
- user.setRole(role);
+ // user.setAffiliation(affiliation);
+ // user.setRole(role);
return user;
}
@@ -7,24 +7,33 @@ import com.google.common.base.CharMatcher;
import com.google.common.io.BaseEncoding;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
+import im.conversations.android.xmpp.model.blocking.Block;
+import im.conversations.android.xmpp.model.blocking.Unblock;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.ibb.InBandByteStream;
+import im.conversations.android.xmpp.model.ping.Ping;
+import im.conversations.android.xmpp.model.roster.Query;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.time.Time;
+import im.conversations.android.xmpp.model.up.Push;
import im.conversations.android.xmpp.model.version.Version;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -58,76 +67,6 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
super(service, connection);
}
- 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) {
- return items;
- }
- for (Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- Jid jid = child.getAttributeAsJid("jid");
- if (jid != null) {
- items.add(jid);
- }
- }
- }
- return items;
- }
-
- private void rosterItems(final Account account, final Element query) {
- final String version = query.getAttribute("ver");
- if (version != null) {
- account.getRoster().setVersion(version);
- }
- for (final Element item : query.getChildren()) {
- if (item.getName().equals("item")) {
- final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid == null) {
- continue;
- }
- final String name = item.getAttribute("name");
- final String subscription = item.getAttribute("subscription");
- final Contact contact = account.getRoster().getContact(jid);
- boolean bothPre =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
- contact.setServerName(name);
- contact.parseGroupsFromElement(item);
- }
- if ("remove".equals(subscription)) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_DELETE);
- contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
- } else {
- contact.setOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_PUSH);
- contact.parseSubscriptionFromElement(item);
- }
- boolean both =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if ((both != bothPre) && both) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": gained mutual presence subscription with "
- + contact.getJid());
- AxolotlService axolotlService = account.getAxolotlService();
- if (axolotlService != null) {
- axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
- }
- }
- mXmppConnectionService.getAvatarService().clear(contact);
- }
- }
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.PUSH);
- mXmppConnectionService.getShortcutService().refresh();
- mXmppConnectionService.syncRoster(account);
- }
-
public static String avatarData(final Iq packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) {
@@ -396,143 +335,49 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
@Override
public void accept(final Iq packet) {
- final var account = getAccount();
- final boolean isGet = packet.getType() == Iq.Type.GET;
- if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
- return;
+ final var type = packet.getType();
+ switch (type) {
+ case SET -> acceptPush(packet);
+ case GET -> acceptRequest(packet);
+ default ->
+ throw new AssertionError(
+ "IQ results and errors should are handled in callbacks");
}
- if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
- final Element query = packet.findChild("query");
- // If this is in response to a query for the whole roster:
- if (packet.getType() == Iq.Type.RESULT) {
- account.getRoster().markAllAsNotInRoster();
- }
- this.rosterItems(account, query);
- } else if ((packet.hasChild("block", Namespace.BLOCKING)
- || packet.hasChild("blocklist", Namespace.BLOCKING))
- && packet.fromServer(account)) {
- // Block list or block push.
- Log.d(Config.LOGTAG, "Received blocklist update from server");
- final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING);
- final Element block = packet.findChild("block", Namespace.BLOCKING);
- final Collection<Element> items =
- blocklist != null
- ? blocklist.getChildren()
- : (block != null ? block.getChildren() : null);
- // If this is a response to a blocklist query, clear the block list and replace with the
- // new one.
- // Otherwise, just update the existing blocklist.
- if (packet.getType() == Iq.Type.RESULT) {
- account.clearBlocklist();
- connection.getFeatures().setBlockListRequested(true);
- }
- if (items != null) {
- final Collection<Jid> jids = new ArrayList<>(items.size());
- // Create a collection of Jids from the packet
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().addAll(jids);
- if (packet.getType() == Iq.Type.SET) {
- boolean removed = false;
- for (Jid jid : jids) {
- removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
- }
- if (removed) {
- mXmppConnectionService.updateConversationUi();
- }
- }
- }
- // Update the UI
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
- if (packet.getType() == Iq.Type.SET) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- }
- } else if (packet.hasChild("unblock", Namespace.BLOCKING)
- && packet.fromServer(account)
- && packet.getType() == Iq.Type.SET) {
- Log.d(Config.LOGTAG, "Received unblock update from server");
- final Collection<Element> items =
- packet.findChild("unblock", Namespace.BLOCKING).getChildren();
- if (items.isEmpty()) {
- // No children to unblock == unblock all
- account.getBlocklist().clear();
- } else {
- final Collection<Jid> jids = new ArrayList<>(items.size());
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().removeAll(jids);
- }
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
- || packet.hasChild("data", "http://jabber.org/protocol/ibb")
- || packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
- mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet);
- } else if (packet.hasExtension(InfoQuery.class) && isGet) {
+ }
+
+ private void acceptPush(final Iq packet) {
+ if (packet.hasExtension(Query.class)) {
+ this.getManager(RosterManager.class).push(packet);
+ } else if (packet.hasExtension(Block.class)) {
+ this.getManager(BlockingManager.class).pushBlock(packet);
+ } else if (packet.hasExtension(Unblock.class)) {
+ this.getManager(BlockingManager.class).pushUnblock(packet);
+ } else if (packet.hasExtension(InBandByteStream.class)) {
+ mXmppConnectionService
+ .getJingleConnectionManager()
+ .deliverIbbPacket(getAccount(), packet);
+ } else if (packet.hasExtension(Push.class)) {
+ this.getManager(UnifiedPushManager.class).push(packet);
+ } else {
+ this.connection.sendErrorFor(
+ packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
+ }
+ }
+
+ private void acceptRequest(final Iq packet) {
+ if (packet.hasExtension(InfoQuery.class)) {
this.getManager(DiscoManager.class).handleInfoQuery(packet);
- } else if (packet.hasExtension(Version.class) && isGet) {
+ } else if (packet.hasExtension(Version.class)) {
this.getManager(DiscoManager.class).handleVersionRequest(packet);
- } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
- final Iq response;
- if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
- } else {
- response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH)
- && packet.getType() == Iq.Type.SET) {
- final Jid transport = packet.getFrom();
- final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
- final boolean success =
- push != null
- && mXmppConnectionService.processUnifiedPushMessage(
- account, transport, push);
- final Iq response;
- if (success) {
- response = packet.generateResponse(Iq.Type.RESULT);
- } else {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.setAttribute("code", "404");
- error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.getFrom() != null) {
- final Contact contact = account.getRoster().getContact(packet.getFrom());
- 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() == 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");
- connection.sendIqPacket(response, null);
- }
+ } else if (packet.hasExtension(Time.class)) {
+ this.getManager(EntityTimeManager.class).request(packet);
+ } else if (packet.hasExtension(Ping.class)) {
+ this.getManager(PingManager.class).pong(packet);
+ } else if (packet.hasChild("data", "urn:xmpp:bob")) {
+ mXmppConnectionService.sendIqPacket(getAccount(), mXmppConnectionService.getIqGenerator().bobResponse(packet), null);
+ } else {
+ this.connection.sendErrorFor(
+ packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
}
}
}
@@ -40,7 +40,6 @@ import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
@@ -53,7 +52,6 @@ import eu.siacs.conversations.entities.ReceiptRequest;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService;
-import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
@@ -66,25 +64,21 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
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.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.PubSubManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.Extension;
-import im.conversations.android.xmpp.model.avatar.Metadata;
-import im.conversations.android.xmpp.model.axolotl.DeviceList;
import im.conversations.android.xmpp.model.axolotl.Encrypted;
-import im.conversations.android.xmpp.model.bookmark.Storage;
-import im.conversations.android.xmpp.model.bookmark2.Conference;
import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent;
+import im.conversations.android.xmpp.model.conference.DirectInvite;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.forward.Forwarded;
import im.conversations.android.xmpp.model.markers.Displayed;
-import im.conversations.android.xmpp.model.nick.Nick;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import im.conversations.android.xmpp.model.oob.OutOfBandData;
-import im.conversations.android.xmpp.model.pubsub.Items;
-import im.conversations.android.xmpp.model.pubsub.event.Delete;
import im.conversations.android.xmpp.model.pubsub.event.Event;
-import im.conversations.android.xmpp.model.pubsub.event.Purge;
import im.conversations.android.xmpp.model.reactions.Reactions;
import im.conversations.android.xmpp.model.receipts.Request;
import im.conversations.android.xmpp.model.unique.StanzaId;
@@ -92,10 +86,8 @@ import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
-import java.util.HashSet;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
@@ -253,7 +245,7 @@ public class MessageParser extends AbstractParser
return null;
}
- private Invite extractInvite(final Element message) {
+ private Invite extractInvite(final im.conversations.android.xmpp.model.stanza.Message message) {
final Element mucUser = message.findChild("x", Namespace.MUC_USER);
if (mucUser != null) {
final Element invite = mucUser.findChild("invite");
@@ -272,7 +264,7 @@ public class MessageParser extends AbstractParser
return new Invite(room, password, false, from);
}
}
- final Element conference = message.findChild("x", "jabber:x:conference");
+ final var conference = message.getExtension(DirectInvite.class);
if (conference != null) {
Jid from = Jid.Invalid.getNullForInvalid(message.getAttributeAsJid("from"));
Jid room = Jid.Invalid.getNullForInvalid(conference.getAttributeAsJid("jid"));
@@ -284,169 +276,6 @@ public class MessageParser extends AbstractParser
return null;
}
- private void parseEvent(final Items items, final Jid from, final Account account) {
- final String node = items.getNode();
- if ("urn:xmpp:avatar:metadata".equals(node)) {
- // TODO support retract
- final var entry = items.getFirstItemWithId(Metadata.class);
- final var avatar =
- entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
- if (avatar != null) {
- avatar.owner = from.asBareJid();
- if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
- if (account.getJid().asBareJid().equals(from)) {
- if (account.setAvatar(avatar.getFilename())) {
- mXmppConnectionService.databaseBackend.updateAccount(account);
- mXmppConnectionService.notifyAccountAvatarHasChanged(account);
- }
- mXmppConnectionService.getAvatarService().clear(account);
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateAccountUi();
- } else {
- final Contact contact = account.getRoster().getContact(from);
- if (contact.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(account);
- mXmppConnectionService.getAvatarService().clear(contact);
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
- }
- }
- } else if (mXmppConnectionService.isDataSaverDisabled()) {
- mXmppConnectionService.fetchAvatar(account, avatar);
- }
- }
- } else if (Namespace.NICK.equals(node)) {
- final var nickItem = items.getFirstItem(Nick.class);
- final String nick = nickItem == null ? null : nickItem.getContent();
- if (nick != null) {
- setNick(account, from, nick);
- }
- } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
- final var deviceList = items.getFirstItem(DeviceList.class);
- if (deviceList != null) {
- final Set<Integer> deviceIds = deviceList.getDeviceIds();
- Log.d(
- Config.LOGTAG,
- AxolotlService.getLogprefix(account)
- + "Received PEP device list "
- + deviceIds
- + " update from "
- + from
- + ", processing... ");
- final AxolotlService axolotlService = account.getAxolotlService();
- axolotlService.registerDevices(from, new HashSet<>(deviceIds));
- }
-
- } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
- final var connection = account.getXmppConnection();
- if (connection.getFeatures().bookmarksConversion()) {
- if (connection.getFeatures().bookmarks2()) {
- Log.w(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received storage:bookmark notification even though we"
- + " opted into bookmarks:1");
- }
- final var storage = items.getFirstItem(Storage.class);
- final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
- mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": processing bookmark PEP event");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ignoring bookmark PEP event because bookmark conversion was"
- + " not detected");
- }
- } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- final var retractions = items.getRetractions();
- for (final var item : items.getItemMap(Conference.class).entrySet()) {
- final Bookmark bookmark =
- Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
- if (bookmark == null) {
- continue;
- }
- account.putBookmark(bookmark);
- mXmppConnectionService.processModifiedBookmark(bookmark);
- mXmppConnectionService.updateConversationUi();
- }
- for (final var retract : retractions) {
- final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
- if (id != null) {
- account.removeBookmark(id);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": deleted bookmark for " + id);
- mXmppConnectionService.processDeletedBookmark(account, id);
- mXmppConnectionService.updateConversationUi();
- }
- }
- } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
- && Namespace.MDS_DISPLAYED.equals(node)
- && account.getJid().asBareJid().equals(from)) {
- for (final var item :
- items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
- .entrySet()) {
- mXmppConnectionService.processMdsItem(account, item);
- }
- }
- }
-
- private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
- final String node = delete.getNode();
- if (Namespace.NICK.equals(node)) {
- setNick(account, from, null);
- } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
- deleteAllBookmarks(account);
- } else if (Namespace.AVATAR_METADATA.equals(node)) {
- final boolean isAccount = account.getJid().asBareJid().equals(from);
- if (isAccount) {
- account.setAvatar(null);
- mXmppConnectionService.databaseBackend.updateAccount(account);
- mXmppConnectionService.getAvatarService().clear(account);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": deleted avatar metadata node");
- }
- }
- }
-
- private void parsePurgeEvent(
- @NonNull final Purge purge, final Jid from, final Account account) {
- final String node = purge.getNode();
- if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
- deleteAllBookmarks(account);
- }
- }
-
- private void deleteAllBookmarks(final Account account) {
- final var previous = account.getBookmarkedJids();
- account.setBookmarks(Collections.emptyMap());
- mXmppConnectionService.processDeletedBookmarks(account, previous);
- }
-
- private void setNick(final Account account, final Jid user, final String nick) {
- if (user.asBareJid().equals(account.getJid().asBareJid())) {
- account.setDisplayName(nick);
- if (QuickConversationsService.isQuicksy()) {
- mXmppConnectionService.getAvatarService().clear(account);
- }
- mXmppConnectionService.checkMucRequiresRename();
- } else {
- Contact contact = account.getRoster().getContact(user);
- if (contact.setPresenceName(nick)) {
- mXmppConnectionService.syncRoster(account);
- mXmppConnectionService.getAvatarService().clear(contact);
- }
- }
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateAccountUi();
- }
-
private boolean handleErrorMessage(
final Account account,
final im.conversations.android.xmpp.model.stanza.Message packet) {
@@ -507,7 +336,7 @@ public class MessageParser extends AbstractParser
+ ": received ping worthy error for seemingly online"
+ " muc at "
+ from);
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
}
}
}
@@ -551,6 +380,12 @@ public class MessageParser extends AbstractParser
packet = f.first;
serverMsgId = result.getAttribute("id");
query.incrementMessageCount();
+
+ if (query.isImplausibleFrom(packet.getFrom())) {
+ Log.d(Config.LOGTAG, "found implausible from in MUC MAM archive");
+ return;
+ }
+
if (handleErrorMessage(account, packet)) {
return;
}
@@ -1375,69 +1210,11 @@ public class MessageParser extends AbstractParser
}
}
}
- if (conversation != null
- && mucUserElement != null
- && Jid.Invalid.hasValidFrom(packet)
- && from.isBareJid()) {
- for (Element child : mucUserElement.getChildren()) {
- if ("status".equals(child.getName())) {
- try {
- int code = Integer.parseInt(child.getAttribute("code"));
- if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
- mXmppConnectionService.fetchConferenceConfiguration(conversation);
- break;
- }
- } catch (Exception e) {
- // ignored
- }
- } else if ("item".equals(child.getName())) {
- final var user = AbstractParser.parseItem(conversation, child);
- Log.d(
- Config.LOGTAG,
- account.getJid()
- + ": changing affiliation for "
- + user.getRealJid()
- + " to "
- + user.getAffiliation()
- + " in "
- + conversation.getJid().asBareJid());
- if (!user.realJidMatchesAccount()) {
- final var mucOptions = conversation.getMucOptions();
- final boolean isNew = mucOptions.updateUser(user);
- final var avatarService = mXmppConnectionService.getAvatarService();
- if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
- avatarService.clear(mucOptions);
- }
- avatarService.clear(user);
- mXmppConnectionService.updateMucRosterUi();
- mXmppConnectionService.updateConversationUi();
- Contact contact = user.getContact();
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
- Jid jid = user.getRealJid();
- List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
- if (cryptoTargets.remove(user.getRealJid())) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removed "
- + jid
- + " from crypto targets of "
- + conversation.getName());
- conversation.setAcceptedCryptoTargets(cryptoTargets);
- mXmppConnectionService.updateConversation(conversation);
- }
- } else if (isNew
- && user.getRealJid() != null
- && conversation.getMucOptions().isPrivateAndNonAnonymous()
- && (contact == null || !contact.mutualPresenceSubscription())
- && account.getAxolotlService()
- .hasEmptyDeviceList(user.getRealJid())) {
- account.getAxolotlService().fetchDeviceIds(user.getRealJid());
- }
- }
- }
- }
+
+ if (original.hasExtension(MucUser.class)) {
+ getManager(MultiUserChatManager.class).handleStatusMessage(original);
}
+
if (!isTypeGroupChat) {
for (Element child : packet.getChildren()) {
if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace())
@@ -1595,45 +1372,18 @@ public class MessageParser extends AbstractParser
// end no body
}
- if (reactions != null) {
- processReactions(
- reactions,
- mXmppConnectionService.find(account, from.asBareJid()),
- isTypeGroupChat,
- occupant,
- counterpart,
- mucTrueCounterPart,
- status,
- packet);
- }
-
- final var event = original.getExtension(Event.class);
- if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
- final var action = event.getAction();
- final var node = action == null ? null : action.getNode();
- if (node == null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": no node found in PubSub event from "
- + original.getFrom());
- } else if (action instanceof Items items) {
- parseEvent(items, original.getFrom(), account);
- } else if (action instanceof Purge purge) {
- parsePurgeEvent(purge, original.getFrom(), account);
- } else if (action instanceof Delete delete) {
- parseDeleteEvent(delete, from, account);
- }
+ if (original.hasExtension(Event.class)) {
+ getManager(PubSubManager.class).handleEvent(original);
}
final String nick = packet.findChildContent("nick", Namespace.NICK);
- if (nick != null && Jid.Invalid.hasValidFrom(original)) {
+ if (nick != null && Jid.Invalid.isValid(from)) {
if (mXmppConnectionService.isMuc(account, from)) {
return;
}
final Contact contact = account.getRoster().getContact(from);
if (contact.setPresenceName(nick)) {
- mXmppConnectionService.syncRoster(account);
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
}
}
@@ -1978,16 +1728,30 @@ public class MessageParser extends AbstractParser
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked");
return false;
}
- final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
- conversation.setAttribute("inviter", inviter.toString());
- if (conversation.getMucOptions().online()) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online");
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
- } else {
- conversation.getMucOptions().setPassword(password);
- mXmppConnectionService.databaseBackend.updateConversation(conversation);
- mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList());
- mXmppConnectionService.updateConversationUi();
+ final AppSettings appSettings = new AppSettings(mXmppConnectionService);
+ if ((contact != null && contact.showInContactList())
+ || appSettings.isAcceptInvitesFromStrangers()) {
+ final Conversation conversation =
+ mXmppConnectionService.findOrCreateConversation(account, jid, true, false);
+ if (conversation.getMucOptions().online()) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": received invite to "
+ + jid
+ + " but muc is considered to be online");
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
+ } else {
+ conversation.getMucOptions().setPassword(password);
+ mXmppConnectionService.databaseBackend.updateConversation(conversation);
+ if (contact != null && contact.showInContactList()) {
+ getManager(MultiUserChatManager.class).joinFollowingInvite(conversation);
+ } else {
+ getManager(MultiUserChatManager.class).join(conversation);
+ }
+ mXmppConnectionService.updateConversationUi();
+ }
+ return true;
}
return true;
}
@@ -15,7 +15,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.XmppUri;
@@ -23,10 +22,15 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.AvatarManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
-import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
import im.conversations.android.xmpp.model.occupant.OccupantId;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
@@ -48,6 +52,7 @@ public class PresenceParser extends AbstractParser
? null
: mXmppConnectionService.find(account, packet.getFrom().asBareJid());
if (conversation == null) {
+ Log.d(Config.LOGTAG, "conversation not found for parsing conference presence");
return;
}
final MucOptions mucOptions = conversation.getMucOptions();
@@ -77,27 +82,26 @@ public class PresenceParser extends AbstractParser
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
final String type = packet.getAttribute("type");
- final Element x = packet.findChild("x", Namespace.MUC_USER);
+ final var x = packet.getExtension(MucUser.class);
+ final var vCardUpdate = packet.getExtension(VCardUpdate.class);
final Element nick = packet.findChild("nick", Namespace.NICK);
Element hats = packet.findChild("hats", "urn:xmpp:hats:0");
if (hats == null) {
hats = packet.findChild("hats", "xmpp:prosody.im/protocol/hats:1");
}
if (hats == null) hats = new Element("hats", "urn:xmpp:hats:0");
- final Element occupantIdEl = packet.findChild("occupant-id", "urn:xmpp:occupant-id:0");
- Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+ final var occupant = packet.getOnlyExtension(OccupantId.class);
+ final String occupantId =
+ mucOptions.occupantId() && occupant != null
+ ? occupant.getId()
+ : null;
final List<String> codes = getStatusCodes(x);
if (type == null) {
if (x != null) {
- Element item = x.findChild("item");
+ final var item = x.getItem();
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- final MucOptions.User user = parseItem(conversation, item, from, occupantIdEl, nick == null ? null : nick.getContent(), hats);
- final var occupant = packet.getOnlyExtension(OccupantId.class);
- final String occupantId =
- mucOptions.occupantId() && occupant != null
- ? occupant.getId()
- : null;
+ final var user = MultiUserChatManager.itemToUser(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats);
user.setOccupantId(occupantId);
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)
|| (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
@@ -137,16 +141,17 @@ public class PresenceParser extends AbstractParser
}
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
&& mucOptions.autoPushConfiguration()) {
+ final var address = mucOptions.getConversation().getJid().asBareJid();
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": room '"
- + mucOptions.getConversation().getJid().asBareJid()
+ + address
+ "' created. pushing default configuration");
- mXmppConnectionService.pushConferenceConfiguration(
- mucOptions.getConversation(),
- IqGenerator.defaultChannelConfiguration(),
- null);
+ getManager(MultiUserChatManager.class)
+ .pushConfiguration(
+ conversation,
+ MultiUserChatManager.defaultChannelConfiguration());
}
if (mXmppConnectionService.getPgpEngine() != null) {
Element signed = packet.findChild("x", "jabber:x:signed");
@@ -165,28 +170,8 @@ public class PresenceParser extends AbstractParser
}
}
}
- if (avatar != null) {
- avatar.owner = from;
- if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
- if (user.setAvatar(avatar)) {
- mXmppConnectionService.getAvatarService().clear(user);
- }
- if (user.getRealJid() != null) {
- final Contact c =
- conversation
- .getAccount()
- .getRoster()
- .getContact(user.getRealJid());
- if (c.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(
- conversation.getAccount());
- mXmppConnectionService.getAvatarService().clear(c);
- }
- mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
- }
- } else if (mXmppConnectionService.isDataSaverDisabled()) {
- mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
- }
+ if (vCardUpdate != null) {
+ getManager(AvatarManager.class).handleVCardUpdate(from, vCardUpdate);
}
}
}
@@ -221,7 +206,7 @@ public class PresenceParser extends AbstractParser
+ " online="
+ wasOnline);
if (wasOnline) {
- mXmppConnectionService.mucSelfPingAndRejoin(conversation);
+ getManager(MultiUserChatManager.class).pingAndRejoin(conversation);
}
} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
mucOptions.setError(MucOptions.Error.KICKED);
@@ -238,12 +223,13 @@ public class PresenceParser extends AbstractParser
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
}
} else if (!from.isBareJid()) {
- Element item = x.findChild("item");
+ final var item = x.getItem();
if (item != null) {
- mucOptions.updateUser(parseItem(conversation, item, from, occupantIdEl, nick == null ? null : nick.getContent(), hats));
+ mucOptions.updateUser(
+ MultiUserChatManager.itemToUser(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats));
}
MucOptions.User user = mucOptions.deleteUser(from);
- if (user != null && occupantIdEl == null) {
+ if (user != null && occupantId == null) {
mXmppConnectionService.getAvatarService().clear(user);
}
}
@@ -344,29 +330,6 @@ public class PresenceParser extends AbstractParser
final Contact contact = account.getRoster().getContact(from);
if (type == null) {
final String resource = from.isBareJid() ? "" : from.getResource();
- final Avatar avatar =
- Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
- if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
- avatar.owner = from.asBareJid();
- if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
- if (avatar.owner.equals(account.getJid().asBareJid())) {
- account.setAvatar(avatar.getFilename());
- mXmppConnectionService.databaseBackend.updateAccount(account);
- mXmppConnectionService.getAvatarService().clear(account);
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateAccountUi();
- } else {
- if (contact.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(account);
- mXmppConnectionService.getAvatarService().clear(contact);
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
- }
- }
- } else if (mXmppConnectionService.isDataSaverDisabled()) {
- mXmppConnectionService.fetchAvatar(account, avatar);
- }
- }
if (mXmppConnectionService.isMuc(account, from)) {
return;
@@ -377,8 +340,7 @@ public class PresenceParser extends AbstractParser
contact.updatePresence(resource, packet);
final var nodeHash = packet.getCapabilities();
- final var connection = account.getXmppConnection();
- if (nodeHash != null && connection != null) {
+ if (nodeHash != null) {
final var discoFuture =
this.getManager(DiscoManager.class)
.infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash);
@@ -416,7 +378,7 @@ public class PresenceParser extends AbstractParser
+ contact.getJid()
+ " "
+ OpenPgpUtils.convertKeyIdToHex(keyId));
- mXmppConnectionService.syncRoster(account);
+ this.connection.getManager(RosterManager.class).writeToDatabaseAsync();
}
}
boolean online = sizeBefore < contact.getPresences().size();
@@ -446,12 +408,13 @@ public class PresenceParser extends AbstractParser
return;
}
if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
- mXmppConnectionService.syncRoster(account);
+ this.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
}
if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
- mXmppConnectionService.sendPresencePacket(
- account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ connection
+ .getManager(PresenceManager.class)
+ .subscribed(contact.getJid().asBareJid());
} else {
contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
final Conversation conversation =
@@ -49,6 +49,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import io.ipfs.cid.Cid;
+import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -60,7 +61,6 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.PresenceTemplate;
-import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.ShortcutService;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -2232,23 +2232,29 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.delete("cheogram." + Message.TABLENAME, Message.UUID + "=?", args) == 1;
}
- public void readRoster(Roster roster) {
+ public Map<Jid, Contact> readRoster(final Account account) {
+ final var builder = new ImmutableMap.Builder<Jid, Contact>();
final SQLiteDatabase db = this.getReadableDatabase();
- final String[] args = {roster.getAccount().getUuid()};
+ final String[] args = {account.getUuid()};
try (final Cursor cursor =
db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
while (cursor.moveToNext()) {
- roster.initContact(Contact.fromCursor(cursor));
+ final var contact = Contact.fromCursor(cursor);
+ if (contact != null) {
+ contact.setAccount(account);
+ builder.put(contact.getJid(), contact);
+ }
}
}
+ return builder.buildKeepingLast();
}
- public void writeRoster(final Roster roster) {
- long start = SystemClock.elapsedRealtime();
- final Account account = roster.getAccount();
+ public void writeRoster(
+ final Account account, final String version, final List<Contact> contacts) {
+ final long start = SystemClock.elapsedRealtime();
final SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
- for (Contact contact : roster.getContacts()) {
+ for (final Contact contact : contacts) {
if (contact.getOption(Contact.Options.IN_ROSTER)
|| contact.hasAvatarOrPresenceName()
|| contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
@@ -2261,7 +2267,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
db.setTransactionSuccessful();
db.endTransaction();
- account.setRosterVersion(roster.getVersion());
+ account.setRosterVersion(version);
updateAccount(account);
long duration = SystemClock.elapsedRealtime() - start;
Log.d(
@@ -28,7 +28,6 @@ import android.provider.OpenableColumns;
import android.system.Os;
import android.system.StructStat;
import android.util.Base64;
-import android.util.Base64OutputStream;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.Log;
@@ -149,11 +148,11 @@ public class FileBackend {
}
public static boolean allFilesUnderSize(
- Context context, List<Attachment> attachments, long max) {
+ Context context, List<Attachment> attachments, final Long max) {
final boolean compressVideo =
!AttachFileToConversationRunnable.getVideoCompression(context)
.equals("uncompressed");
- if (max <= 0) {
+ if (max == null || max <= 0) {
Log.d(Config.LOGTAG, "server did not report max file size for http upload");
return true; // exception to be compatible with HTTP Upload < v0.2
}
@@ -262,7 +261,7 @@ public class FileBackend {
return context.getPackageName() + FILE_PROVIDER;
}
- private static boolean hasAlpha(final Bitmap bitmap) {
+ public static boolean hasAlpha(final Bitmap bitmap) {
final int w = bitmap.getWidth();
final int h = bitmap.getHeight();
final int yStep = Math.max(1, w / 100);
@@ -927,7 +926,7 @@ public class FileBackend {
throw new ImageCompressionException("Source file had alpha channel");
}
Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
- final int rotation = getRotation(image);
+ final int rotation = getRotation(mXmppConnectionService, image);
scaledBitmap = rotate(scaledBitmap, rotation);
boolean targetSizeReached = false;
int quality = Config.IMAGE_QUALITY;
@@ -1166,9 +1165,8 @@ public class FileBackend {
}
}
- private int getRotation(final Uri image) {
- try (final InputStream is =
- mXmppConnectionService.getContentResolver().openInputStream(image)) {
+ private static int getRotation(final Context context, final Uri image) {
+ try (final InputStream is = context.getContentResolver().openInputStream(image)) {
return is == null ? 0 : getRotation(is);
} catch (final Exception e) {
return 0;
@@ -1559,7 +1557,6 @@ public class FileBackend {
}
}
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private Bitmap cropCenterSquarePdf(final Uri uri, final int size) {
try {
ParcelFileDescriptor fileDescriptor =
@@ -1573,7 +1570,6 @@ public class FileBackend {
}
}
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private Bitmap renderPdfDocument(
ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException {
final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor);
@@ -1609,240 +1605,6 @@ public class FileBackend {
return getUriForFile(mXmppConnectionService, file, filename);
}
- public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
-
- final Pair<Avatar,Boolean> uncompressAvatar = getUncompressedAvatar(image);
- if (uncompressAvatar != null && uncompressAvatar.first != null &&
- (uncompressAvatar.first.image.length() <= Config.AVATAR_CHAR_LIMIT || uncompressAvatar.second)) {
- return uncompressAvatar.first;
- }
- if (uncompressAvatar != null && uncompressAvatar.first != null) {
- Log.d(
- Config.LOGTAG,
- "uncompressed avatar exceeded char limit by "
- + (uncompressAvatar.first.image.length() - Config.AVATAR_CHAR_LIMIT));
- }
-
- Bitmap bm = cropCenterSquare(image, size);
- if (bm == null) {
- return null;
- }
- if (hasAlpha(bm)) {
- Log.d(Config.LOGTAG, "alpha in avatar detected; uploading as PNG");
- bm.recycle();
- bm = cropCenterSquare(image, 96);
- return getPepAvatar(bm, Bitmap.CompressFormat.PNG, 100);
- }
- return getPepAvatar(bm, format, 100);
- }
-
- private Pair<Avatar,Boolean> getUncompressedAvatar(Uri uri) {
- try {
- if (android.os.Build.VERSION.SDK_INT >= 28) {
- ImageDecoder.Source source = ImageDecoder.createSource(mXmppConnectionService.getContentResolver(), uri);
- int[] size = new int[] { 0, 0 };
- boolean[] animated = new boolean[] { false };
- String[] mimeType = new String[] { null };
- Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
- mimeType[0] = info.getMimeType();
- animated[0] = info.isAnimated();
- size[0] = info.getSize().getWidth();
- size[1] = info.getSize().getHeight();
- });
-
- if (animated[0]) {
- Avatar avatar = getPepAvatar(uri, size[0], size[1], mimeType[0]);
- if (avatar != null) return new Pair(avatar, true);
- }
-
- return new Pair(getPepAvatar(drawDrawable(drawable), Bitmap.CompressFormat.PNG, 100), false);
- } else {
- Bitmap bitmap =
- BitmapFactory.decodeStream(
- mXmppConnectionService.getContentResolver().openInputStream(uri));
- return new Pair(getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100), false);
- }
- } catch (Exception e) {
- try {
- final SVG svg = SVG.getFromInputStream(mXmppConnectionService.getContentResolver().openInputStream(uri));
- return new Pair(getPepAvatar(uri, (int) svg.getDocumentWidth(), (int) svg.getDocumentHeight(), "image/svg+xml"), true);
- } catch (Exception e2) {
- return null;
- }
- }
- }
-
- private Avatar getPepAvatar(Uri uri, int width, int height, final String mimeType) throws IOException, NoSuchAlgorithmException {
- AssetFileDescriptor fd = mXmppConnectionService.getContentResolver().openAssetFileDescriptor(uri, "r");
- if (fd.getLength() > 100000) return null; // Too big to use raw file
-
- ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
- Base64OutputStream mBase64OutputStream =
- new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- DigestOutputStream mDigestOutputStream =
- new DigestOutputStream(mBase64OutputStream, digest);
-
- ByteStreams.copy(fd.createInputStream(), mDigestOutputStream);
- mDigestOutputStream.flush();
- mDigestOutputStream.close();
-
- final Avatar avatar = new Avatar();
- avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
- avatar.image = new String(mByteArrayOutputStream.toByteArray());
- avatar.type = mimeType;
- avatar.width = width;
- avatar.height = height;
- return avatar;
- }
-
- private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
- try {
- ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
- Base64OutputStream mBase64OutputStream =
- new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- DigestOutputStream mDigestOutputStream =
- new DigestOutputStream(mBase64OutputStream, digest);
- if (!bitmap.compress(format, quality, mDigestOutputStream)) {
- return null;
- }
- mDigestOutputStream.flush();
- mDigestOutputStream.close();
- long chars = mByteArrayOutputStream.size();
- if (format != Bitmap.CompressFormat.PNG
- && quality >= 50
- && chars >= Config.AVATAR_CHAR_LIMIT) {
- int q = quality - 2;
- Log.d(
- Config.LOGTAG,
- "avatar char length was " + chars + " reducing quality to " + q);
- return getPepAvatar(bitmap, format, q);
- }
- Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality);
- final Avatar avatar = new Avatar();
- avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
- avatar.image = mByteArrayOutputStream.toString();
- if (format.equals(Bitmap.CompressFormat.WEBP)) {
- avatar.type = "image/webp";
- } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
- avatar.type = "image/jpeg";
- } else if (format.equals(Bitmap.CompressFormat.PNG)) {
- avatar.type = "image/png";
- }
- avatar.width = bitmap.getWidth();
- avatar.height = bitmap.getHeight();
- return avatar;
- } catch (OutOfMemoryError e) {
- Log.d(Config.LOGTAG, "unable to convert avatar to base64 due to low memory");
- return null;
- } catch (Exception e) {
- return null;
- }
- }
-
- public Avatar getStoredPepAvatar(String hash) {
- if (hash == null) {
- return null;
- }
- Avatar avatar = new Avatar();
- final File file = getAvatarFile(hash);
- FileInputStream is = null;
- try {
- avatar.size = file.length();
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- is = new FileInputStream(file);
- ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
- Base64OutputStream mBase64OutputStream =
- new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
- byte[] buffer = new byte[4096];
- int length;
- while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
- }
- os.flush();
- os.close();
- avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
- avatar.image = mByteArrayOutputStream.toString();
- avatar.height = options.outHeight;
- avatar.width = options.outWidth;
- avatar.type = options.outMimeType;
- return avatar;
- } catch (NoSuchAlgorithmException | IOException e) {
- return null;
- } finally {
- close(is);
- }
- }
-
- public boolean isAvatarCached(Avatar avatar) {
- final File file = getAvatarFile(avatar.getFilename());
- return file.exists();
- }
-
- public boolean save(final Avatar avatar) {
- File file;
- if (isAvatarCached(avatar)) {
- file = getAvatarFile(avatar.getFilename());
- avatar.size = file.length();
- } else {
- file =
- new File(
- mXmppConnectionService.getCacheDir().getAbsolutePath()
- + "/"
- + UUID.randomUUID().toString());
- if (file.getParentFile().mkdirs()) {
- Log.d(Config.LOGTAG, "created cache directory");
- }
- OutputStream os = null;
- try {
- if (!file.createNewFile()) {
- Log.d(
- Config.LOGTAG,
- "unable to create temporary file " + file.getAbsolutePath());
- }
- os = new FileOutputStream(file);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
- final byte[] bytes = avatar.getImageAsBytes();
- mDigestOutputStream.write(bytes);
- mDigestOutputStream.flush();
- mDigestOutputStream.close();
- String sha1sum = CryptoHelper.bytesToHex(digest.digest());
- if (sha1sum.equals(avatar.sha1sum)) {
- final File outputFile = getAvatarFile(avatar.getFilename());
- if (outputFile.getParentFile().mkdirs()) {
- Log.d(Config.LOGTAG, "created avatar directory");
- }
- final File avatarFile = getAvatarFile(avatar.getFilename());
- if (!file.renameTo(avatarFile)) {
- Log.d(
- Config.LOGTAG,
- "unable to rename " + file.getAbsolutePath() + " to " + outputFile);
- return false;
- }
- } else {
- Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
- if (!file.delete()) {
- Log.d(Config.LOGTAG, "unable to delete temporary file");
- }
- return false;
- }
- avatar.size = bytes.length;
- } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
- return false;
- } finally {
- close(os);
- }
- }
- return true;
- }
-
public void deleteHistoricAvatarPath() {
delete(getHistoricAvatarPath());
}
@@ -1865,8 +1627,8 @@ public class FileBackend {
return new File(mXmppConnectionService.getFilesDir(), "/avatars/");
}
- public File getAvatarFile(String avatar) {
- final var f = new File(mXmppConnectionService.getCacheDir(), "/avatars/" + avatar);
+ public static File getAvatarFile(Context context, String avatar) {
+ final var f = new File(context.getCacheDir(), "/avatars/" + avatar);
if (Build.VERSION.SDK_INT < 26) return f; // Doesn't support file.toPath
try {
if (f.exists()) java.nio.file.Files.setAttribute(f.toPath(), "lastAccessTime", java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis()));
@@ -1876,6 +1638,10 @@ public class FileBackend {
return f;
}
+ public File getAvatarFile(final String avatar) {
+ return getAvatarFile(mXmppConnectionService, avatar);
+ }
+
public Uri getAvatarUri(String avatar) {
return Uri.fromFile(getAvatarFile(avatar));
}
@@ -1905,12 +1671,16 @@ public class FileBackend {
}
public Bitmap cropCenterSquare(final Uri image, final int size) {
+ return cropCenterSquare(mXmppConnectionService, image, size);
+ }
+
+ public Bitmap cropCenterSquare(final Context context, final Uri image, final int size) {
if (image == null) {
return null;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
try {
- options.inSampleSize = calcSampleSize(image, size);
+ options.inSampleSize = calcSampleSize(context, image, size);
} catch (final IOException | SecurityException e) {
Log.d(Config.LOGTAG, "unable to calculate sample size for " + image, e);
return null;
@@ -1924,7 +1694,7 @@ public class FileBackend {
if (originalBitmap == null) {
return null;
} else {
- final var bitmap = rotate(originalBitmap, getRotation(image));
+ final var bitmap = rotate(originalBitmap, getRotation(context, image));
return cropCenterSquare(bitmap, size);
}
} catch (final SecurityException | IOException e) {
@@ -1976,20 +1746,28 @@ public class FileBackend {
}
}
- public Bitmap cropCenterSquare(Bitmap input, int size) {
- int w = input.getWidth();
- int h = input.getHeight();
-
- float scale = Math.max((float) size / h, (float) size / w);
-
- float outWidth = scale * w;
- float outHeight = scale * h;
+ public static Bitmap cropCenterSquare(final Bitmap input, final int sizeIn) {
+ final int w = input.getWidth();
+ final int h = input.getHeight();
+ final int size;
+ final float outWidth;
+ final float outHeight;
+ if (w < sizeIn || h < sizeIn) {
+ size = Math.min(w, h);
+ outWidth = w;
+ outHeight = h;
+ } else {
+ size = sizeIn;
+ final float scale = Math.max((float) sizeIn / h, (float) sizeIn / w);
+ outWidth = scale * w;
+ outHeight = scale * h;
+ }
float left = (size - outWidth) / 2;
float top = (size - outHeight) / 2;
- RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+ final var target = new RectF(left, top, left + outWidth, top + outHeight);
- Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(output);
+ final var output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ final var canvas = new Canvas(output);
canvas.drawBitmap(input, null, target, createAntiAliasingPaint());
if (!input.isRecycled()) {
input.recycle();
@@ -1998,6 +1776,11 @@ public class FileBackend {
}
private int calcSampleSize(final Uri image, int size) throws IOException, SecurityException {
+ return calcSampleSize(mXmppConnectionService, image, size);
+ }
+
+ private int calcSampleSize(final Context context, final Uri image, int size)
+ throws IOException, SecurityException {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try (final InputStream inputStream =
@@ -6,19 +6,15 @@ import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIO
import android.os.PowerManager;
import android.os.SystemClock;
-import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
-import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.Compatibility;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicLong;
import okhttp3.MediaType;
import okhttp3.RequestBody;
@@ -27,7 +23,6 @@ import okio.Okio;
import okio.Source;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
-import org.bouncycastle.crypto.io.CipherOutputStream;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
@@ -73,7 +68,7 @@ public class AbstractConnectionManager {
}
@Override
- public void writeTo(final BufferedSink sink) throws IOException {
+ public void writeTo(@NonNull final BufferedSink sink) throws IOException {
long transmitted = 0;
try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
long read;
@@ -91,29 +86,6 @@ public class AbstractConnectionManager {
void onProgress(long progress);
}
- public static OutputStream createOutputStream(
- DownloadableFile file, boolean append, boolean decrypt) {
- FileOutputStream os;
- try {
- os = new FileOutputStream(file, append);
- if (file.getKey() == null || !decrypt) {
- return os;
- }
- } catch (FileNotFoundException e) {
- Log.d(Config.LOGTAG, "unable to create output stream", e);
- return null;
- }
- try {
- AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
- cipher.init(
- false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
- return new CipherOutputStream(os, cipher);
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
- return null;
- }
- }
-
public XmppConnectionService getXmppConnectionService() {
return this.mXmppConnectionService;
}
@@ -16,13 +16,11 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.util.DisplayMetrics;
-import android.util.Log;
import android.util.LruCache;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.google.common.base.Strings;
-import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
@@ -37,15 +35,13 @@ import eu.siacs.conversations.entities.Room;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
-import eu.siacs.conversations.xmpp.XmppConnection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
-public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
+public class AvatarService {
private static final int FG_COLOR = 0xFFFAFAFA;
private static final int TRANSPARENT = 0x00000000;
@@ -109,14 +105,14 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
if (avatar != null || cachedOnly) {
return avatar;
}
- if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
- avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
+ if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) {
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
}
if (avatar == null && contact.getProfilePhoto() != null) {
avatar = new BitmapDrawable(mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size));
}
- if (avatar == null && contact.getAvatarFilename() != null) {
- avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
+ if (avatar == null && contact.getAvatar() != null) {
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
}
if (avatar == null) {
avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, cachedOnly);
@@ -205,7 +201,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
public Drawable get(final MucOptions.User user, final int size, boolean cachedOnly) {
Contact c = user.getContact();
- if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null || user.getAvatar() == null)) {
+ if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null || user.getAvatar() == null)) {
return get(c, size, cachedOnly);
} else {
return getImpl(user, size, cachedOnly);
@@ -296,7 +292,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Jid jid = bookmark.getJid();
Account account = bookmark.getAccount();
Contact contact = jid == null ? null : account.getRoster().getContact(jid);
- if (contact != null && contact.getAvatarFilename() != null) {
+ if (contact != null && contact.getAvatar() != null) {
return get(contact, size, cachedOnly);
}
String seed = jid != null ? jid.asBareJid().toString() : null;
@@ -488,7 +484,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
} else if (message.getStatus() == Message.STATUS_RECEIVED) {
Contact c = message.getContact();
if (message.getModerated() != null) c = null;
- if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null)) {
+ if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
return get(c, size, cachedOnly);
} else if (conversation instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) {
final Jid trueCounterpart = message.getTrueCounterpart();
@@ -589,12 +585,12 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
Contact contact = user.getContact();
if (contact != null) {
Uri uri = null;
- if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
- uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
+ if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar());
} else if (contact.getProfilePhoto() != null) {
uri = Uri.parse(contact.getProfilePhoto());
- } else if (contact.getAvatarFilename() != null) {
- uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
+ } else if (contact.getAvatar() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar());
}
if (drawTile(canvas, uri, left, top, right, bottom)) {
return true;
@@ -666,17 +662,6 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
return true;
}
- @Override
- public void onAdvancedStreamFeaturesAvailable(Account account) {
- XmppConnection.Features features = account.getXmppConnection().getFeatures();
- if (features.pep() && !features.pepPersistent()) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has pep but is not persistent");
- if (account.getAvatar() != null) {
- mXmppConnectionService.republishAvatarIfNeeded(account);
- }
- }
- }
-
private static String emptyOnNull(@Nullable Jid value) {
return value == null ? "" : value.toString();
}
@@ -20,6 +20,7 @@ import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.services.MuclumbusService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
@@ -307,10 +308,10 @@ public class ChannelDiscoveryService {
for (final var account : service.getAccounts()) {
final var connection = account.getXmppConnection();
if (connection != null && account.isEnabled()) {
- for (final String mucService : connection.getMucServers()) {
- final Jid jid = Jid.ofOrInvalid(mucService);
- if (Jid.Invalid.isValid(jid)) {
- localMucServices.put(jid, connection);
+ for (final var mucService :
+ connection.getManager(MultiUserChatManager.class).getServices()) {
+ if (Jid.Invalid.isValid(mucService)) {
+ localMucServices.put(mucService, connection);
}
}
}
@@ -134,7 +134,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.execute(query);
}
- void catchupMUC(final Conversation conversation) {
+ public void catchupMUC(final Conversation conversation) {
if (conversation.getLastMessageTransmitted().getTimestamp() < 0
&& conversation.countMessages() == 0) {
query(conversation, new MamReference(0), 0, true);
@@ -219,7 +219,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
- void executePendingQueries(final Account account) {
+ public void executePendingQueries(final Account account) {
final List<Query> pending = new ArrayList<>();
synchronized (this.pendingQueries) {
for (Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) {
@@ -761,5 +761,16 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
boolean hasCallback() {
return this.callback != null;
}
+
+ public boolean isImplausibleFrom(final Jid from) {
+ if (muc()) {
+ if (from == null) {
+ return true;
+ }
+ return !from.asBareJid().equals(getWith());
+ } else {
+ return false;
+ }
+ }
}
}
@@ -57,6 +57,7 @@ import com.google.common.primitives.Ints;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@@ -79,6 +80,7 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
+import im.conversations.android.xmpp.model.muc.Affiliation;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -405,6 +407,7 @@ public class NotificationService {
}
private boolean notifyMessage(final Message message) {
+ final var appSettings = new AppSettings(mXmppConnectionService.getApplicationContext());
final Conversation conversation = (Conversation) message.getConversation();
final var chatRequestsPref = mXmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
return message.getStatus() == Message.STATUS_RECEIVED
@@ -578,9 +581,8 @@ public class NotificationService {
public void pushFailedDelivery(final Message message) {
final Conversation conversation = (Conversation) message.getConversation();
- final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked();
if (this.mIsInForeground
- && isScreenLocked
+ && !new Device(mXmppConnectionService).isScreenLocked()
&& this.mOpenConversation == message.getConversation()) {
Log.d(
Config.LOGTAG,
@@ -850,7 +852,7 @@ public class NotificationService {
+ ": suppressing notification because turned off");
return;
}
- final boolean isScreenLocked = mXmppConnectionService.isScreenLocked();
+ final boolean isScreenLocked = new Device(mXmppConnectionService).isScreenLocked();
if (this.mIsInForeground
&& !isScreenLocked
&& this.mOpenConversation == message.getConversation()) {
@@ -1924,7 +1926,7 @@ public class NotificationService {
final MucOptions.User sender = conversation.getMucOptions().findUserByFullJid(message.getCounterpart());
final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && mXmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
if (muted) return false;
- if (sender != null && sender.getAffiliation().ranks(MucOptions.Affiliation.MEMBER) && message.isAttention()) {
+ if (sender != null && sender.ranks(Affiliation.MEMBER) && message.isAttention()) {
return true;
}
final String nick = conversation.getMucOptions().getActualNick();
@@ -2045,7 +2047,7 @@ public class NotificationService {
}
}
- void updateErrorNotification() {
+ public void updateErrorNotification() {
if (Config.SUPPRESS_ERROR_NOTIFICATION) {
cancel(ERROR_NOTIFICATION_ID);
return;
@@ -29,8 +29,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.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.stanza.Presence;
+import im.conversations.android.xmpp.model.up.Push;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.List;
@@ -68,7 +69,10 @@ public class UnifiedPushBroker {
if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) {
final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service);
if (database.hasEndpoints(transport)) {
- sendDirectedPresence(transportAccount, transport.transport);
+ transportAccount
+ .getXmppConnection()
+ .getManager(PresenceManager.class)
+ .available(transport.transport);
}
Log.d(
Config.LOGTAG,
@@ -78,12 +82,6 @@ public class UnifiedPushBroker {
}
}
- private void sendDirectedPresence(final Account account, Jid to) {
- final var presence = new Presence();
- presence.setTo(to);
- service.sendPresencePacket(account, presence);
- }
-
public void renewUnifiedPushEndpoints() {
renewUnifiedPushEndpoints(null);
}
@@ -320,8 +318,7 @@ public class UnifiedPushBroker {
service.sendBroadcast(intent);
}
- public boolean processPushMessage(
- final Account account, final Jid transport, final Element push) {
+ public boolean processPushMessage(final Account account, final Jid transport, final Push push) {
final String instance = push.getAttribute("instance");
final String application = push.getAttribute("application");
if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
@@ -1,12 +1,10 @@
package eu.siacs.conversations.services;
import static eu.siacs.conversations.utils.Compatibility.s;
-import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
-import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -43,12 +41,9 @@ import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.DocumentsContract;
import android.security.KeyChain;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
-import androidx.annotation.BoolRes;
import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -138,7 +133,6 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Reaction;
import eu.siacs.conversations.generator.AbstractGenerator;
@@ -147,7 +141,6 @@ import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.ServiceOutageStatus;
-import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
@@ -175,10 +168,8 @@ import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.ReplacingTaskManager;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.TorServiceUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.WakeLockHelper;
@@ -186,35 +177,40 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.IqErrorResponseException;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnGatewayResult;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
-import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
-import eu.siacs.conversations.xmpp.OnStatusChanged;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
-import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import eu.siacs.conversations.xmpp.mam.MamReference;
+import eu.siacs.conversations.xmpp.manager.AvatarManager;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.NickManager;
+import eu.siacs.conversations.xmpp.manager.PepManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.VCardManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
-import eu.siacs.conversations.xmpp.pep.PublishOptions;
import im.conversations.android.xmpp.Entity;
-import im.conversations.android.xmpp.model.avatar.Metadata;
-import im.conversations.android.xmpp.model.bookmark.Storage;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
-import im.conversations.android.xmpp.model.mds.Displayed;
-import im.conversations.android.xmpp.model.pubsub.PubSub;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Presence;
import im.conversations.android.xmpp.model.storage.PrivateStorage;
+import im.conversations.android.xmpp.model.up.Push;
import java.io.File;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -226,7 +222,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
@@ -288,7 +283,7 @@ public class XmppConnectionService extends Service {
private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
- private final ScheduledExecutorService internalPingExecutor =
+ public final ScheduledExecutorService internalPingExecutor =
Executors.newSingleThreadScheduledExecutor();
private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
new SerialSingleThreadExecutor("VideoCompression");
@@ -298,23 +293,12 @@ public class XmppConnectionService extends Service {
new SerialSingleThreadExecutor("DatabaseReader");
private final SerialSingleThreadExecutor mNotificationExecutor =
new SerialSingleThreadExecutor("NotificationExecutor");
- private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
private final IBinder mBinder = new XmppConnectionBinder();
private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
private final IqGenerator mIqGenerator = new IqGenerator(this);
private final Set<String> mInProgressAvatarFetches = new HashSet<>();
private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
- private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
- 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, "received iq error: " + text);
- }
- }
- };
+ public final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
public DatabaseBackend databaseBackend;
private Multimap<String, String> mutedMucUsers;
private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
@@ -366,42 +350,6 @@ public class XmppConnectionService extends Service {
markFileDeleted(file);
}
};
- private final OnMessageAcknowledged mOnMessageAcknowledgedListener =
- new OnMessageAcknowledged() {
-
- @Override
- public boolean onMessageAcknowledged(
- final Account account, final Jid to, final String id) {
- if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
- final String sessionId =
- id.substring(
- JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX
- .length());
- mJingleConnectionManager.updateProposedSessionDiscovered(
- account,
- to,
- sessionId,
- JingleConnectionManager.DeviceDiscoveryState
- .SEARCHING_ACKNOWLEDGED);
- }
-
- final Jid bare = to.asBareJid();
-
- for (final Conversation conversation : getConversations()) {
- if (conversation.getAccount() == account
- && conversation.getJid().asBareJid().equals(bare)) {
- final Message message = conversation.findUnsentMessageWithUuid(id);
- if (message != null) {
- message.setStatus(Message.STATUS_SEND);
- message.setErrorMessage(null);
- databaseBackend.updateMessage(message, false);
- return true;
- }
- }
- }
- return false;
- }
- };
private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
@@ -438,146 +386,6 @@ public class XmppConnectionService extends Service {
public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
private final AtomicLong mLastExpiryRun = new AtomicLong(0);
- private final OnStatusChanged statusListener =
- new OnStatusChanged() {
-
- @Override
- public void onStatusChanged(final Account account) {
- final var status = account.getStatus();
- if (ServiceOutageStatus.isPossibleOutage(status)) {
- fetchServiceOutageStatus(account);
- }
- XmppConnection connection = account.getXmppConnection();
- updateAccountUi();
-
- if (account.getStatus() == Account.State.ONLINE
- || account.getStatus().isError()) {
- mQuickConversationsService.signalAccountStateChange();
- }
-
- if (account.getStatus() == Account.State.ONLINE) {
- synchronized (mLowPingTimeoutMode) {
- if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": leaving low ping timeout mode");
- }
- }
- if (account.setShowErrorNotification(true)) {
- databaseBackend.updateAccount(account);
- }
- mMessageArchiveService.executePendingQueries(account);
- if (connection != null && connection.getFeatures().csi()) {
- if (checkListeners()) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + " sending csi//inactive");
- connection.sendInactive();
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + " sending csi//active");
- connection.sendActive();
- }
- }
- List<Conversation> conversations = getConversations();
- for (Conversation conversation : conversations) {
- final boolean inProgressJoin;
- synchronized (account.inProgressConferenceJoins) {
- inProgressJoin =
- account.inProgressConferenceJoins.contains(conversation);
- }
- final boolean pendingJoin;
- synchronized (account.pendingConferenceJoins) {
- pendingJoin = account.pendingConferenceJoins.contains(conversation);
- }
- if (conversation.getAccount() == account
- && !pendingJoin
- && !inProgressJoin) {
- sendUnsentMessages(conversation);
- }
- }
- final List<Conversation> pendingLeaves;
- synchronized (account.pendingConferenceLeaves) {
- pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
- account.pendingConferenceLeaves.clear();
- }
- for (Conversation conversation : pendingLeaves) {
- leaveMuc(conversation);
- }
- final List<Conversation> pendingJoins;
- synchronized (account.pendingConferenceJoins) {
- pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
- account.pendingConferenceJoins.clear();
- }
- for (Conversation conversation : pendingJoins) {
- joinMuc(conversation);
- }
- scheduleWakeUpCall(
- Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
- } else if (account.getStatus() == Account.State.OFFLINE
- || account.getStatus() == Account.State.DISABLED
- || account.getStatus() == Account.State.LOGGED_OUT) {
- resetSendingToWaiting(account);
- if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": went into offline state during low ping mode."
- + " reconnecting now");
- reconnectAccount(account, true, false);
- } else {
- final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
- scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
- }
- } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
- databaseBackend.updateAccount(account);
- reconnectAccount(account, true, false);
- } else if (account.getStatus() != Account.State.CONNECTING
- && account.getStatus() != Account.State.NO_INTERNET) {
- resetSendingToWaiting(account);
- if (connection != null && account.getStatus().isAttemptReconnect()) {
- final boolean aggressive =
- account.getStatus() == Account.State.SEE_OTHER_HOST
- || hasJingleRtpConnection(account);
- final int next = connection.getTimeToNextAttempt(aggressive);
- final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
- if (next <= 0) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error connecting account. reconnecting now."
- + " lowPingTimeout="
- + lowPingTimeoutMode);
- reconnectAccount(account, true, false);
- } else {
- final int attempt = connection.getAttempt() + 1;
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error connecting account. try again in "
- + next
- + "s for the "
- + attempt
- + " time. lowPingTimeout="
- + lowPingTimeoutMode
- + ", aggressive="
- + aggressive);
- scheduleWakeUpCall(next, account.getUuid().hashCode());
- if (aggressive) {
- internalPingExecutor.schedule(
- XmppConnectionService.this
- ::manageAccountConnectionStatesInternal,
- (next * 1000L) + 50,
- TimeUnit.MILLISECONDS);
- }
- }
- }
- }
- getNotificationService().updateErrorNotification();
- }
- };
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
@@ -593,7 +401,7 @@ public class XmppConnectionService extends Service {
return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
}
- private boolean isInLowPingTimeoutMode(Account account) {
+ public boolean isInLowPingTimeoutMode(Account account) {
synchronized (mLowPingTimeoutMode) {
return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
}
@@ -1128,7 +936,7 @@ public class XmppConnectionService extends Service {
});
case AudioManager.RINGER_MODE_CHANGED_ACTION:
case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
- if (dndOnSilentMode()) {
+ if (appSettings.isDndOnSilentMode() && appSettings.isAutomaticAvailability()) {
refreshAllPresences();
}
break;
@@ -1136,7 +944,7 @@ public class XmppConnectionService extends Service {
deactivateGracePeriod();
case Intent.ACTION_USER_PRESENT:
case Intent.ACTION_SCREEN_OFF:
- if (awayWhenScreenLocked()) {
+ if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
refreshAllPresences();
}
break;
@@ -1221,7 +1029,7 @@ public class XmppConnectionService extends Service {
updateConversationUi();
}
- private void manageAccountConnectionStatesInternal() {
+ public void manageAccountConnectionStatesInternal() {
manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
}
@@ -1273,7 +1081,7 @@ public class XmppConnectionService extends Service {
mLastMucPing = SystemClock.elapsedRealtime();
for (Conversation c : getConversations()) {
if (c.getMode() == Conversation.MODE_MULTI && (c.getMucOptions().online() || c.getMucOptions().getError() == MucOptions.Error.SHUTDOWN)) {
- mucSelfPingAndRejoin(c);
+ c.getAccount().getXmppConnection().getManager(MultiUserChatManager.class).pingAndRejoin(c);
}
}
}
@@ -1288,16 +1096,11 @@ public class XmppConnectionService extends Service {
final var conversation = message.getConversation();
final var account = conversation.getAccount();
- final boolean inProgressJoin;
- synchronized (account.inProgressConferenceJoins) {
- inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
- }
- final boolean pendingJoin;
- synchronized (account.pendingConferenceJoins) {
- pendingJoin = account.pendingConferenceJoins.contains(conversation);
- }
+ final boolean inProgressJoin =
+ conversation instanceof Conversation ? conversation.getAccount().getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .isJoinInProgress((Conversation) conversation) : false;
if (conversation.getAccount() == account
- && !pendingJoin
&& !inProgressJoin) {
resendMessage(message, false);
}
@@ -1318,17 +1121,16 @@ public class XmppConnectionService extends Service {
final boolean isUiAction,
final boolean isAccountPushed,
final HashSet<Account> pingCandidates) {
+ final var connection = account.getXmppConnection();
if (!account.getStatus().isAttemptReconnect()) {
return false;
}
final var requestCode = account.getUuid().hashCode();
if (!hasInternetConnection()) {
- account.setStatus(Account.State.NO_INTERNET);
- statusListener.onStatusChanged(account);
+ connection.setStatusAndTriggerProcessor(Account.State.NO_INTERNET);
} else {
if (account.getStatus() == Account.State.NO_INTERNET) {
- account.setStatus(Account.State.OFFLINE);
- statusListener.onStatusChanged(account);
+ connection.setStatusAndTriggerProcessor(Account.State.OFFLINE);
}
if (account.getStatus() == Account.State.ONLINE) {
synchronized (mLowPingTimeoutMode) {
@@ -1379,7 +1181,6 @@ public class XmppConnectionService extends Service {
} else if (account.getStatus() == Account.State.OFFLINE) {
reconnectAccount(account, true, interactive);
} else if (account.getStatus() == Account.State.CONNECTING) {
- final var connection = account.getXmppConnection();
final var connectionDuration = connection.getConnectionDuration();
final var discoDuration = connection.getDiscoDuration();
final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
@@ -1396,7 +1197,7 @@ public class XmppConnectionService extends Service {
final boolean aggressive =
account.getStatus() == Account.State.SEE_OTHER_HOST
|| hasJingleRtpConnection(account);
- if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
+ if (connection.getTimeToNextAttempt(aggressive) <= 0) {
reconnectAccount(account, true, interactive);
}
}
@@ -1414,7 +1215,7 @@ public class XmppConnectionService extends Service {
}
}
- private void fetchServiceOutageStatus(final Account account) {
+ public void fetchServiceOutageStatus(final Account account) {
final var sosUrl = account.getKey(Account.KEY_SOS_URL);
if (Strings.isNullOrEmpty(sosUrl)) {
return;
@@ -1443,7 +1244,7 @@ public class XmppConnectionService extends Service {
}
public boolean processUnifiedPushMessage(
- final Account account, final Jid transport, final Element push) {
+ final Account account, final Jid transport, final Push push) {
return unifiedPushBroker.processPushMessage(account, transport, push);
}
@@ -1522,25 +1323,6 @@ public class XmppConnectionService extends Service {
}
}
- private boolean dndOnSilentMode() {
- return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
- }
-
- private boolean manuallyChangePresence() {
- return getBooleanPreference(
- AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
- }
-
- private boolean treatVibrateAsSilent() {
- return getBooleanPreference(
- AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
- }
-
- private boolean awayWhenScreenLocked() {
- return getBooleanPreference(
- AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
- }
-
private String getCompressPicturesPreference() {
return getPreferences()
.getString(
@@ -1548,55 +1330,6 @@ public class XmppConnectionService extends Service {
getResources().getString(R.string.picture_compression));
}
- private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
- if (dndOnSilentMode() && isPhoneSilenced()) {
- return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
- } else if (awayWhenScreenLocked() && isScreenLocked()) {
- return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
- } else {
- return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
- }
- }
-
- public boolean isScreenLocked() {
- final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
- final PowerManager powerManager = getSystemService(PowerManager.class);
- final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
- final boolean interactive;
- try {
- interactive = powerManager != null && powerManager.isInteractive();
- } catch (final Exception e) {
- return false;
- }
- return locked || !interactive;
- }
-
- private boolean isPhoneSilenced() {
- final NotificationManager notificationManager = getSystemService(NotificationManager.class);
- final int filter =
- notificationManager == null
- ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
- : notificationManager.getCurrentInterruptionFilter();
- final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
- final AudioManager audioManager = getSystemService(AudioManager.class);
- final int ringerMode =
- audioManager == null
- ? AudioManager.RINGER_MODE_NORMAL
- : audioManager.getRingerMode();
- try {
- if (treatVibrateAsSilent()) {
- return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
- } else {
- return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
- }
- } catch (final Throwable throwable) {
- Log.d(
- Config.LOGTAG,
- "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
- return notificationDnd;
- }
- }
-
private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
Log.d(Config.LOGTAG, "resetting all attempt counts");
for (Account account : accounts) {
@@ -1729,9 +1462,10 @@ public class XmppConnectionService extends Service {
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
Log.d(Config.LOGTAG, "restoring accounts...");
this.accounts = databaseBackend.getAccounts();
- for (Account account : this.accounts) {
+ for (final var account : this.accounts) {
final int color = getPreferences().getInt("account_color:" + account.getUuid(), 0);
if (color != 0) account.setColor(color);
+ account.setXmppConnection(createConnection(account));
}
final SharedPreferences.Editor editor = getPreferences().edit();
final boolean hasEnabledAccounts = hasEnabledAccounts();
@@ -1920,7 +1654,7 @@ public class XmppConnectionService extends Service {
}
public void toggleScreenEventReceiver() {
- if (awayWhenScreenLocked() && !manuallyChangePresence()) {
+ if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
@@ -2052,7 +1786,7 @@ public class XmppConnectionService extends Service {
int activeAccounts = 0;
for (final Account account : accounts) {
if (account.isConnectionEnabled()) {
- databaseBackend.writeRoster(account.getRoster());
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabase();
activeAccounts++;
}
if (account.getXmppConnection() != null) {
@@ -2085,13 +1819,8 @@ public class XmppConnectionService extends Service {
? PendingIntent.FLAG_IMMUTABLE
| PendingIntent.FLAG_UPDATE_CURRENT
: PendingIntent.FLAG_UPDATE_CURRENT);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- alarmManager.setAndAllowWhileIdle(
- AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
- } else {
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
- }
+ alarmManager.setAndAllowWhileIdle(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
} catch (RuntimeException e) {
Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
}
@@ -2150,20 +1879,13 @@ public class XmppConnectionService extends Service {
public XmppConnection createConnection(final Account account) {
final XmppConnection connection = new XmppConnection(account, this);
- connection.setOnStatusChangedListener(this.statusListener);
connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
- connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
- connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
- AxolotlService axolotlService = account.getAxolotlService();
- if (axolotlService != null) {
- connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
- }
return connection;
}
- public void sendChatState(Conversation conversation) {
- if (sendChatStates()) {
+ public void sendChatState(final Conversation conversation) {
+ if (appSettings.isSendChatStates()) {
final var packet = mMessageGenerator.generateChatState(conversation);
sendMessagePacket(conversation.getAccount(), packet);
}
@@ -2222,7 +1944,7 @@ public class XmppConnectionService extends Service {
+ ": adding "
+ contact.getJid()
+ " on sending message");
- createContact(contact, true);
+ createContact(contact);
}
}
@@ -2240,7 +1962,10 @@ public class XmppConnectionService extends Service {
}
}
- final boolean inProgressJoin = isJoinInProgress(conversation);
+ final boolean inProgressJoin =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .isJoinInProgress(conversation);
if (message.getCounterpart() == null && !message.isPrivateMessage()) {
message.setCounterpart(message.getConversation().getJid().asBareJid());
@@ -2498,45 +2223,17 @@ public class XmppConnectionService extends Service {
mMessageGenerator.addDelay(packet, message.getTimeSent());
}
if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
- if (this.sendChatStates()) {
+ if (this.appSettings.isSendChatStates()) {
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
}
}
sendMessagePacket(account, packet);
- if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
- if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
- }
}
if (!waitForPreview && !passedCbOn && cb != null) cb.run();
}
- private boolean isJoinInProgress(final Conversation conversation) {
- final Account account = conversation.getAccount();
- synchronized (account.inProgressConferenceJoins) {
- if (conversation.getMode() == Conversational.MODE_MULTI) {
- final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
- final boolean pending = account.pendingConferenceJoins.contains(conversation);
- final boolean inProgressJoin = inProgress || pending;
- if (inProgressJoin) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": holding back message to group. inProgress="
- + inProgress
- + ", pending="
- + pending);
- }
- return inProgressJoin;
- } else {
- return false;
- }
- }
- }
-
- private void sendUnsentMessages(final Conversation conversation) {
- synchronized (conversation) {
- conversation.findWaitingMessages(message -> resendMessage(message, true));
- }
+ public void sendUnsentMessages(final Conversation conversation) {
+ conversation.findWaitingMessages(message -> resendMessage(message, true));
}
public void resendMessage(final Message message, final boolean delay) {
@@ -2576,18 +2273,16 @@ public class XmppConnectionService extends Service {
public void requestEasyOnboardingInvite(
final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
- final XmppConnection connection = account.getXmppConnection();
- final Jid jid =
- connection == null
- ? null
- : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
- if (jid == null) {
+ final var connection = account.getXmppConnection();
+ final var discoManager = connection.getManager(DiscoManager.class);
+ final var address = discoManager.getAddressForCommand(Namespace.EASY_ONBOARDING_INVITE);
+ if (address == null) {
callback.inviteRequestFailed(
getString(R.string.server_does_not_support_easy_onboarding_invites));
return;
}
final Iq request = new Iq(Iq.Type.SET);
- request.setTo(jid);
+ request.setTo(address);
final Element command = request.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
command.setAttribute("action", "execute");
@@ -2609,7 +2304,7 @@ public class XmppConnectionService extends Service {
if (uri != null) {
final EasyOnboardingInvite invite =
new EasyOnboardingInvite(
- jid.getDomain().toString(), uri, landingUrl);
+ address.getDomain().toString(), uri, landingUrl);
callback.inviteRequested(invite);
return;
}
@@ -2624,89 +2319,6 @@ public class XmppConnectionService extends Service {
});
}
- public void fetchBookmarks(final Account account) {
- final Iq iqPacket = new Iq(Iq.Type.GET);
- iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
- final Consumer<Iq> callback =
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final var privateStorage = response.getExtension(PrivateStorage.class);
- if (privateStorage == null) {
- return;
- }
- final var bookmarkStorage = privateStorage.getExtension(Storage.class);
- Map<Jid, Bookmark> bookmarks =
- Bookmark.parseFromStorage(bookmarkStorage, account);
- processBookmarksInitial(account, bookmarks, false);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": could not fetch bookmarks");
- }
- };
- sendIqPacket(account, iqPacket, callback);
- }
-
- public void fetchBookmarks2(final Account account) {
- final Iq retrieve = mIqGenerator.retrieveBookmarks();
- sendIqPacket(
- account,
- retrieve,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final var pubsub = response.getExtension(PubSub.class);
- final Map<Jid, Bookmark> bookmarks =
- Bookmark.parseFromPubSub(pubsub, account);
- processBookmarksInitial(account, bookmarks, true);
- }
- });
- }
-
- public void fetchMessageDisplayedSynchronization(final Account account) {
- Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
- final var retrieve = mIqGenerator.retrieveMds();
- sendIqPacket(
- account,
- retrieve,
- (response) -> {
- if (response.getType() != Iq.Type.RESULT) {
- return;
- }
- final var pubsub = response.getExtension(PubSub.class);
- if (pubsub == null) {
- return;
- }
- final var items = pubsub.getItems();
- if (items == null) {
- return;
- }
- if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
- for (final var item :
- items.getItemMap(
- im.conversations.android.xmpp.model.mds.Displayed
- .class)
- .entrySet()) {
- processMdsItem(account, item);
- }
- }
- });
- }
-
- public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
- final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
- if (jid == null) {
- return;
- }
- final var displayed = item.getValue();
- final var stanzaId = displayed.getStanzaId();
- final String id = stanzaId == null ? null : stanzaId.getId();
- final Conversation conversation = find(account, jid);
- if (id != null && conversation != null) {
- conversation.setDisplayState(id);
- markReadUpToStanzaId(conversation, id);
- }
- }
-
public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
final Message message = conversation.findMessageWithServerMsgId(stanzaId);
if (message == null) { // do we want to check if isRead?
@@ -2739,250 +2351,15 @@ public class XmppConnectionService extends Service {
return true;
}
- public void processBookmarksInitial(
- final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
- final Set<Jid> previousBookmarks = account.getBookmarkedJids();
- for (final Bookmark bookmark : bookmarks.values()) {
- previousBookmarks.remove(bookmark.getJid().asBareJid());
- processModifiedBookmark(bookmark, pep);
- }
- if (pep) {
- processDeletedBookmarks(account, previousBookmarks);
- }
- account.setBookmarks(bookmarks);
- }
-
- public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": "
- + bookmarks.size()
- + " bookmarks have been removed");
- for (final Jid bookmark : bookmarks) {
- processDeletedBookmark(account, bookmark);
- }
- }
-
- public void processDeletedBookmark(final Account account, final Jid jid) {
- final Conversation conversation = find(account, jid);
- if (conversation == null) {
- return;
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
- archiveConversation(conversation, false);
- }
-
- private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
- final Account account = bookmark.getAccount();
- Conversation conversation = find(bookmark);
- if (conversation != null) {
- if (conversation.getMode() != Conversation.MODE_MULTI) {
- return;
- }
- bookmark.setConversation(conversation);
- if (pep && !bookmark.autojoin()) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": archiving conference ("
- + conversation.getJid()
- + ") after receiving pep");
- archiveConversation(conversation, false);
- } else {
- final MucOptions mucOptions = conversation.getMucOptions();
- if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
- final String current = mucOptions.getActualNick();
- final String proposed = mucOptions.getProposedNickPure();
- if (current != null && !current.equals(proposed)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": proposed nick changed after bookmark push "
- + current
- + "->"
- + proposed);
- joinMuc(conversation);
- }
- } else {
- checkMucRequiresRename(conversation);
- }
- }
- } else if (bookmark.autojoin()) {
- conversation =
- findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
- bookmark.setConversation(conversation);
- }
- }
-
- public void processModifiedBookmark(final Bookmark bookmark) {
- processModifiedBookmark(bookmark, true);
- }
-
- public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
- final var account = conversation.getAccount();
- final var existingBookmark = conversation.getBookmark();
- if (existingBookmark == null) {
- final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
- bookmark.setAutojoin(true);
- createBookmark(account, bookmark);
- } else {
- if (existingBookmark.autojoin()) {
- return;
- }
- existingBookmark.setAutojoin(true);
- createBookmark(account, existingBookmark);
- }
- }
-
public void createBookmark(final Account account, final Bookmark bookmark) {
- account.putBookmark(bookmark);
- final XmppConnection connection = account.getXmppConnection();
- if (connection == null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
- } else if (connection.getFeatures().bookmarks2()) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
- final Element item = mIqGenerator.publishBookmarkItem(bookmark);
- pushNodeAndEnforcePublishOptions(
- account,
- Namespace.BOOKMARKS2,
- item,
- bookmark.getJid().asBareJid().toString(),
- PublishOptions.persistentWhitelistAccessMaxItems());
- } else if (connection.getFeatures().bookmarksConversion()) {
- pushBookmarksPep(account);
- } else {
- pushBookmarksPrivateXml(account);
- }
+ account.getXmppConnection().getManager(BookmarkManager.class).create(bookmark);
}
public void deleteBookmark(final Account account, final Bookmark bookmark) {
if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
}
- account.removeBookmark(bookmark);
- final XmppConnection connection = account.getXmppConnection();
- if (connection == null) return;
-
- if (connection.getFeatures().bookmarks2()) {
- final Iq request =
- mIqGenerator.deleteItem(
- Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
- sendIqPacket(
- account,
- request,
- (response) -> {
- if (response.getType() == Iq.Type.ERROR) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to delete bookmark "
- + response.getErrorCondition());
- }
- });
- } else if (connection.getFeatures().bookmarksConversion()) {
- pushBookmarksPep(account);
- } else {
- pushBookmarksPrivateXml(account);
- }
- }
-
- private void pushBookmarksPrivateXml(Account account) {
- if (!account.areBookmarksLoaded()) return;
-
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
- 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()) {
- storage.addChild(bookmark);
- }
- sendIqPacket(account, iqPacket, mDefaultIqHandler);
- }
-
- private void pushBookmarksPep(Account account) {
- if (!account.areBookmarksLoaded()) return;
-
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
- final Element storage = new Element("storage", "storage:bookmarks");
- for (final Bookmark bookmark : account.getBookmarks()) {
- storage.addChild(bookmark);
- }
- pushNodeAndEnforcePublishOptions(
- account,
- Namespace.BOOKMARKS,
- storage,
- "current",
- PublishOptions.persistentWhitelistAccess());
- }
-
- private void pushNodeAndEnforcePublishOptions(
- final Account account,
- final String node,
- final Element element,
- final String id,
- final Bundle options) {
- pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
- }
-
- private void pushNodeAndEnforcePublishOptions(
- final Account account,
- final String node,
- final Element element,
- final String id,
- final Bundle options,
- final boolean retry) {
- final Iq packet = mIqGenerator.publishElement(node, element, id, options);
- sendIqPacket(
- account,
- packet,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- return;
- }
- if (retry && PublishOptions.preconditionNotMet(response)) {
- pushNodeConfiguration(
- account,
- node,
- options,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- pushNodeAndEnforcePublishOptions(
- account, node, element, id, options, false);
- }
-
- @Override
- public void onPushFailed() {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to push node configuration ("
- + node
- + ")");
- }
- });
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error publishing "
- + node
- + " (retry="
- + retry
- + ") "
- + response);
- }
- });
+ account.getXmppConnection().getManager(BookmarkManager.class).delete(bookmark);
}
private void restoreFromDatabase() {
@@ -17,7 +17,14 @@ public final class Activities {
public static void setStatusAndNavigationBarColors(
final Activity activity, final View view, final boolean raisedStatusBar) {
- final var isLightMode = isLightMode(activity);
+ setStatusAndNavigationBarColors(activity, view, isLightMode(activity), raisedStatusBar);
+ }
+
+ public static void setStatusAndNavigationBarColors(
+ final Activity activity,
+ final View view,
+ final boolean isLightMode,
+ final boolean raisedStatusBar) {
final var window = activity.getWindow();
final var flags = view.getSystemUiVisibility();
// an elevation of 4 matches the MaterialToolbar elevation
@@ -1,10 +1,8 @@
package eu.siacs.conversations.ui;
import android.util.Log;
-
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
-
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.ui.util.SettingsUtils;
@@ -23,7 +21,7 @@ public abstract class BaseActivity extends AppCompatActivity {
}
@Override
- protected void onResume(){
+ protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotSetting(this);
}
@@ -2,121 +2,132 @@ package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.widget.Toast;
-
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
-
import com.google.android.material.textfield.TextInputLayout;
-
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityChangePasswordBinding;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.widget.DisabledActionModeCallback;
-public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged {
-
- private ActivityChangePasswordBinding binding;
-
- private final View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() {
- @Override
- public void onClick(final View view) {
- final var account = mAccount;
- if (account == null) {
- return;
- }
- final String currentPassword = binding.currentPassword.getText().toString();
- final String newPassword = binding.newPassword.getText().toString();
- if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE) && !didUnlock && !currentPassword.equals(account.getPassword())) {
- binding.currentPassword.requestFocus();
- binding.currentPasswordLayout.setError(getString(R.string.account_status_unauthorized));
- removeErrorsOnAllBut(binding.currentPasswordLayout);
- } else if (newPassword.trim().isEmpty()) {
- binding.newPassword.requestFocus();
- binding.newPasswordLayout.setError(getString(R.string.password_should_not_be_empty));
- removeErrorsOnAllBut(binding.newPasswordLayout);
- } else {
- binding.currentPasswordLayout.setError(null);
- binding.newPasswordLayout.setError(null);
- xmppConnectionService.updateAccountPasswordOnServer(account, newPassword, ChangePasswordActivity.this);
- binding.changePasswordButton.setEnabled(false);
- binding.changePasswordButton.setText(R.string.updating);
- }
- }
- };
-
-
-
- private Account mAccount;
- private boolean didUnlock = false;
-
- @Override
+public class ChangePasswordActivity extends XmppActivity {
+
+ private ActivityChangePasswordBinding binding;
+
+ private final FutureCallback<? super Void> passwordChangedCallback =
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ Toast.makeText(
+ ChangePasswordActivity.this,
+ R.string.password_changed,
+ Toast.LENGTH_LONG)
+ .show();
+ finish();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not change password", t);
+ binding.newPasswordLayout.setError(
+ getString(R.string.could_not_change_password));
+ binding.changePasswordButton.setEnabled(true);
+ binding.changePasswordButton.setText(R.string.change_password);
+ }
+ };
+ private final View.OnClickListener mOnChangePasswordButtonClicked =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(final View view) {
+ final var account = mAccount;
+ if (account == null) {
+ return;
+ }
+ final String currentPassword = binding.currentPassword.getText().toString();
+ final String newPassword = binding.newPassword.getText().toString();
+ if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE)
+ && !currentPassword.equals(account.getPassword())) {
+ binding.currentPassword.requestFocus();
+ binding.currentPasswordLayout.setError(
+ getString(R.string.account_status_unauthorized));
+ removeErrorsOnAllBut(binding.currentPasswordLayout);
+ } else if (newPassword.trim().isEmpty()) {
+ binding.newPassword.requestFocus();
+ binding.newPasswordLayout.setError(
+ getString(R.string.password_should_not_be_empty));
+ removeErrorsOnAllBut(binding.newPasswordLayout);
+ } else {
+ binding.currentPasswordLayout.setError(null);
+ binding.newPasswordLayout.setError(null);
+ final var future =
+ xmppConnectionService.updateAccountPasswordOnServer(
+ account, newPassword);
+ Futures.addCallback(
+ future,
+ ChangePasswordActivity.this.passwordChangedCallback,
+ ContextCompat.getMainExecutor(getApplication()));
+ binding.changePasswordButton.setEnabled(false);
+ binding.changePasswordButton.setText(R.string.updating);
+ }
+ }
+ };
+
+ private Account mAccount;
+ private boolean didUnlock = false;
+
+ @Override
protected void onBackendConnected() {
- this.mAccount = extractAccount(getIntent());
- if (this.mAccount != null && (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || didUnlock)) {
- this.binding.currentPasswordLayout.setVisibility(View.GONE);
- } else {
- this.binding.currentPasswordLayout.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password);
- Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
- setSupportActionBar(binding.toolbar);
- configureActionBar(getSupportActionBar());
- binding.cancelButton.setOnClickListener(view -> finish());
- binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
- binding.currentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
- binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Intent intent = getIntent();
- this.didUnlock = intent.getBooleanExtra("did_unlock", false);
- String password = intent != null ? intent.getStringExtra("password") : null;
- if (password != null) {
- binding.newPassword.getEditableText().clear();
- binding.newPassword.getEditableText().append(password);
- }
- }
-
- @Override
- public void onPasswordChangeSucceeded() {
- runOnUiThread(() -> {
- Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show();
- finish();
- });
- }
-
- @Override
- public void onPasswordChangeFailed() {
- runOnUiThread(() -> {
- binding.newPasswordLayout.setError(getString(R.string.could_not_change_password));
- binding.changePasswordButton.setEnabled(true);
- binding.changePasswordButton.setText(R.string.change_password);
- });
-
- }
-
- private void removeErrorsOnAllBut(TextInputLayout exception) {
- if (this.binding.currentPasswordLayout != exception) {
- this.binding.currentPasswordLayout.setErrorEnabled(false);
- this.binding.currentPasswordLayout.setError(null);
- }
- if (this.binding.newPasswordLayout != exception) {
- this.binding.newPasswordLayout.setErrorEnabled(false);
- this.binding.newPasswordLayout.setError(null);
- }
-
- }
-
- public void refreshUiReal() {
-
- }
+ this.mAccount = extractAccount(getIntent());
+ if (this.mAccount != null && (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || didUnlock)) {
+ this.binding.currentPasswordLayout.setVisibility(View.GONE);
+ } else {
+ this.binding.currentPasswordLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password);
+ Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ binding.cancelButton.setOnClickListener(view -> finish());
+ binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
+ binding.currentPassword.setCustomSelectionActionModeCallback(
+ new DisabledActionModeCallback());
+ binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Intent intent = getIntent();
+ this.didUnlock = intent.getBooleanExtra("did_unlock", false);
+ String password = intent != null ? intent.getStringExtra("password") : null;
+ if (password != null) {
+ binding.newPassword.getEditableText().clear();
+ binding.newPassword.getEditableText().append(password);
+ }
+ }
+
+ private void removeErrorsOnAllBut(TextInputLayout exception) {
+ if (this.binding.currentPasswordLayout != exception) {
+ this.binding.currentPasswordLayout.setErrorEnabled(false);
+ this.binding.currentPasswordLayout.setError(null);
+ }
+ if (this.binding.newPasswordLayout != exception) {
+ this.binding.newPasswordLayout.setErrorEnabled(false);
+ this.binding.newPasswordLayout.setError(null);
+ }
+ }
+
+ public void refreshUiReal() {}
}
@@ -42,6 +42,10 @@ import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
public class ChannelDiscoveryActivity extends XmppActivity
implements MenuItem.OnActionExpandListener,
@@ -317,7 +321,9 @@ public class ChannelDiscoveryActivity extends XmppActivity
final Conversation conversation =
xmppConnectionService.findOrCreateConversation(
account, result.getRoom(), true, true, true);
- xmppConnectionService.ensureBookmarkIsAutoJoin(conversation);
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .ensureBookmarkIsAutoJoin(conversation);
switchToConversation(conversation);
}
}
@@ -4,7 +4,6 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue;
import static eu.siacs.conversations.utils.StringUtils.changed;
import android.app.Activity;
-import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -29,8 +28,10 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
+import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.cheogram.android.Util;
@@ -49,6 +50,10 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import eu.siacs.conversations.Config;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
import de.gultsch.common.Linkify;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
@@ -76,7 +81,6 @@ import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
@@ -84,14 +88,19 @@ import eu.siacs.conversations.utils.XEP0392Helper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import me.drakeet.support.toast.ToastCompat;
public class ConferenceDetailsActivity extends XmppActivity
implements OnConversationUpdate,
OnMucRosterUpdate,
XmppConnectionService.OnAffiliationChanged,
- XmppConnectionService.OnConfigurationPushed,
- XmppConnectionService.OnRoomDestroy,
TextWatcher,
OnMediaLoaded {
public static final String ACTION_VIEW_MUC = "view_muc";
@@ -105,24 +114,20 @@ public class ConferenceDetailsActivity extends XmppActivity
private boolean mAdvancedMode = false;
private boolean showDynamicTags = true;
- private final UiCallback<Conversation> renameCallback =
- new UiCallback<Conversation>() {
+ private FutureCallback<Void> renameCallback =
+ new FutureCallback<Void>() {
@Override
- public void success(Conversation object) {
+ public void onSuccess(Void result) {
displayToast(getString(R.string.your_nick_has_been_changed));
- runOnUiThread(
- () -> {
- updateView();
- });
+ updateView();
}
@Override
- public void error(final int errorCode, Conversation object) {
- displayToast(getString(errorCode));
- }
+ public void onFailure(Throwable t) {
- @Override
- public void userInputRequired(PendingIntent pi, Conversation object) {}
+ // TODO check for NickInUseException and NickInvalid exception
+
+ }
};
public static void open(final Activity activity, final Conversation conversation) {
@@ -176,6 +181,20 @@ public class ConferenceDetailsActivity extends XmppActivity
}
};
+ private final FutureCallback<Void> onConfigurationPushed =
+ new FutureCallback<Void>() {
+
+ @Override
+ public void onSuccess(Void result) {
+ displayToast(getString(R.string.modified_conference_options));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ displayToast(getString(R.string.could_not_modify_conference_options));
+ }
+ };
+
private final OnClickListener mChangeConferenceSettings =
new OnClickListener() {
@Override
@@ -196,15 +215,17 @@ public class ConferenceDetailsActivity extends XmppActivity
builder.setPositiveButton(
R.string.confirm,
(dialog, which) -> {
- final Bundle options = configuration.toBundle(values);
- options.putString("muc#roomconfig_persistentroom", "1");
- if (options.containsKey("muc#roomconfig_allowinvites")) {
- options.putString(
- "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites",
- options.getString("muc#roomconfig_allowinvites"));
- }
- xmppConnectionService.pushConferenceConfiguration(
- mConversation, options, ConferenceDetailsActivity.this);
+ final var options = configuration.toBundle(values);
+ final var future =
+ mConversation
+ .getAccount()
+ .getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .pushConfiguration(mConversation, options);
+ Futures.addCallback(
+ future,
+ onConfigurationPushed,
+ ContextCompat.getMainExecutor(getApplication()));
});
builder.create().show();
}
@@ -241,12 +262,21 @@ public class ConferenceDetailsActivity extends XmppActivity
mConversation.getMucOptions().getActualNick(),
R.string.nickname,
value -> {
- if (xmppConnectionService.renameInMuc(
- mConversation, value, renameCallback)) {
- return null;
- } else {
+ if (mConversation.getMucOptions().createJoinJid(value)
+ == null) {
return getString(R.string.invalid_muc_nick);
}
+ final var future =
+ mConversation
+ .getAccount()
+ .getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .changeUsername(mConversation, value);
+ Futures.addCallback(
+ future,
+ renameCallback,
+ ContextCompat.getMainExecutor(this));
+ return null;
}));
this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false);
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
@@ -262,10 +292,7 @@ public class ConferenceDetailsActivity extends XmppActivity
.show();
return;
}
- if (!mucOptions
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER)) {
+ if (!mucOptions.getSelf().ranks(Affiliation.OWNER)) {
Toast.makeText(
this,
R.string.only_the_owner_can_change_group_chat_avatar,
@@ -288,8 +315,8 @@ public class ConferenceDetailsActivity extends XmppActivity
.setTitle(R.string.block_media)
.setMessage("Do you really want to block this avatar?")
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
- xmppConnectionService.blockMedia(xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatarFilename()));
- xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatarFilename()).delete();
+ xmppConnectionService.blockMedia(xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatar()));
+ xmppConnectionService.getFileBackend().getAvatarFile(mConversation.getContact().getAvatar()).delete();
avatarService().clear(mConversation);
mConversation.getContact().setAvatar(null);
xmppConnectionService.updateConversationUi();
@@ -417,8 +444,7 @@ public class ConferenceDetailsActivity extends XmppActivity
this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel));
final String name = mucOptions.getName();
this.binding.mucEditTitle.setText("");
- final boolean owner =
- mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER);
+ final boolean owner = mucOptions.getSelf().ranks(Affiliation.OWNER);
if (owner || printableValue(name)) {
this.binding.mucEditTitle.setVisibility(View.VISIBLE);
if (name != null) {
@@ -501,16 +527,23 @@ public class ConferenceDetailsActivity extends XmppActivity
}
private void onMucInfoUpdated(String subject, String name) {
+ final var account = mConversation.getAccount();
final MucOptions mucOptions = mConversation.getMucOptions();
if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) {
xmppConnectionService.pushSubjectToConference(mConversation, subject);
}
- if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)
- && changed(mucOptions.getName(), name)) {
- Bundle options = new Bundle();
- options.putString("muc#roomconfig_persistentroom", "1");
- options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name));
- xmppConnectionService.pushConferenceConfiguration(mConversation, options, this);
+ if (mucOptions.getSelf().ranks(Affiliation.OWNER) && changed(mucOptions.getName(), name)) {
+ final var options =
+ new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_roomname", Strings.nullToEmpty(name))
+ .build();
+ final var future =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .pushConfiguration(mConversation, options);
+ Futures.addCallback(
+ future, onConfigurationPushed, ContextCompat.getMainExecutor(getApplication()));
}
}
@@ -539,11 +572,7 @@ public class ConferenceDetailsActivity extends XmppActivity
}
menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null);
menuItemDestroyRoom.setVisible(
- mConversation
- .getMucOptions()
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER));
+ mConversation.getMucOptions().getSelf().ranks(Affiliation.OWNER));
return true;
}
@@ -574,11 +603,33 @@ public class ConferenceDetailsActivity extends XmppActivity
}
protected void saveAsBookmark() {
- xmppConnectionService.saveConversationAsBookmark(
- mConversation, mConversation.getMucOptions().getName());
+ final var account = mConversation.getAccount();
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .save(mConversation, mConversation.getMucOptions().getName());
}
protected void destroyRoom() {
+ final var destroyCallBack =
+ new FutureCallback<Void>() {
+
+ @Override
+ public void onSuccess(Void result) {
+ finish();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ final boolean groupChat =
+ mConversation != null && mConversation.isPrivateAndNonAnonymous();
+ // TODO show toast directly
+ displayToast(
+ getString(
+ groupChat
+ ? R.string.could_not_destroy_room
+ : R.string.could_not_destroy_channel));
+ }
+ };
final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel);
@@ -587,8 +638,11 @@ public class ConferenceDetailsActivity extends XmppActivity
builder.setPositiveButton(
R.string.ok,
(dialog, which) -> {
- xmppConnectionService.destroyRoom(
- mConversation, ConferenceDetailsActivity.this);
+ final var future = xmppConnectionService.destroyRoom(mConversation);
+ Futures.addCallback(
+ future,
+ destroyCallBack,
+ ContextCompat.getMainExecutor(getApplication()));
});
builder.setNegativeButton(R.string.cancel, null);
final AlertDialog dialog = builder.create();
@@ -649,7 +703,7 @@ public class ConferenceDetailsActivity extends XmppActivity
: R.string.channel_details);
final Bookmark bookmark = mConversation.getBookmark();
final XmppConnection connection = mConversation.getAccount().getXmppConnection();
- this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE);
+ this.binding.editMucNameButton.setVisibility((self.ranks(Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE);
this.binding.detailsAccount.setText(getString(R.string.using_account, account));
this.binding.truejid.setVisibility(View.GONE);
if (mConversation.isPrivateAndNonAnonymous()) {
@@ -700,7 +754,7 @@ public class ConferenceDetailsActivity extends XmppActivity
this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
this.binding.mucRole.setVisibility(View.VISIBLE);
this.binding.mucRole.setText(getStatus(self));
- if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (mucOptions.getSelf().ranks(Affiliation.OWNER)) {
this.binding.mucSettings.setVisibility(View.VISIBLE);
this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions));
} else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) {
@@ -715,7 +769,7 @@ public class ConferenceDetailsActivity extends XmppActivity
} else {
this.binding.mucInfoMam.setText(R.string.server_info_unavailable);
}
- if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (self.ranks(Affiliation.OWNER)) {
this.binding.changeConferenceButton.setVisibility(View.VISIBLE);
} else {
this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
@@ -751,9 +805,9 @@ public class ConferenceDetailsActivity extends XmppActivity
Collections.sort(
users,
(a, b) -> {
- if (b.getAffiliation().outranks(a.getAffiliation())) {
+ if (b.outranks(a.getAffiliation())) {
return 1;
- } else if (a.getAffiliation().outranks(b.getAffiliation())) {
+ } else if (a.outranks(b.getAffiliation())) {
return -1;
} else {
if (a.getAvatar() != null && b.getAvatar() == null) {
@@ -765,10 +819,14 @@ public class ConferenceDetailsActivity extends XmppActivity
}
}
});
- this.mUserPreviewAdapter.submitList(
- MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
+ this.binding.users.post(
+ () -> {
+ final var list =
+ MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users));
+ this.mUserPreviewAdapter.submitList(list);
+ });
this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
- this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE);
+ this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().ranks(Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE);
this.binding.showUsers.setText(
getResources().getQuantityString(R.plurals.view_users, users.size(), users.size()));
this.binding.usersWrapper.setVisibility(
@@ -825,13 +883,32 @@ public class ConferenceDetailsActivity extends XmppActivity
if (advanced) {
return String.format(
"%s (%s)",
- context.getString(user.getAffiliation().getResId()),
- context.getString(user.getRole().getResId()));
+ context.getString(affiliationToStringRes(user.getAffiliation())),
+ context.getString(roleToStringRes(user.getRole())));
} else {
- return context.getString(user.getAffiliation().getResId());
+ return context.getString(affiliationToStringRes(user.getAffiliation()));
}
}
+ public static @StringRes int affiliationToStringRes(final Affiliation affiliation) {
+ return switch (affiliation) {
+ case OWNER -> R.string.owner;
+ case ADMIN -> R.string.admin;
+ case MEMBER -> R.string.member;
+ case NONE -> R.string.no_affiliation;
+ case OUTCAST -> R.string.outcast;
+ };
+ }
+
+ public static @StringRes int roleToStringRes(final Role role) {
+ return switch (role) {
+ case MODERATOR -> R.string.moderator;
+ case VISITOR -> R.string.visitor;
+ case PARTICIPANT -> R.string.participant;
+ case NONE -> R.string.no_role;
+ };
+ }
+
private String getStatus(User user) {
return getStatus(this, user, mAdvancedMode);
}
@@ -846,31 +923,6 @@ public class ConferenceDetailsActivity extends XmppActivity
displayToast(getString(resId, jid.asBareJid().toString()));
}
- @Override
- public void onRoomDestroySucceeded() {
- finish();
- }
-
- @Override
- public void onRoomDestroyFailed() {
- final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous();
- displayToast(
- getString(
- groupChat
- ? R.string.could_not_destroy_room
- : R.string.could_not_destroy_channel));
- }
-
- @Override
- public void onPushSucceeded() {
- displayToast(getString(R.string.modified_conference_options));
- }
-
- @Override
- public void onPushFailed() {
- displayToast(getString(R.string.could_not_modify_conference_options));
- }
-
private void displayToast(final String msg) {
runOnUiThread(
() -> {
@@ -16,6 +16,7 @@ import android.preference.PreferenceManager;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
+import android.provider.Settings;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;
@@ -99,6 +100,8 @@ import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Collection;
import java.util.Collections;
@@ -140,11 +143,10 @@ public class ContactDetailsActivity extends OmemoActivity
}
} else {
contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
- xmppConnectionService.sendPresencePacket(
- contact.getAccount(),
- xmppConnectionService
- .getPresenceGenerator()
- .stopPresenceUpdatesTo(contact));
+ final var connection = contact.getAccount().getXmppConnection();
+ connection
+ .getManager(PresenceManager.class)
+ .unsubscribed(contact.getJid().asBareJid());
}
}
};
@@ -153,18 +155,15 @@ public class ContactDetailsActivity extends OmemoActivity
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ final var connection = contact.getAccount().getXmppConnection();
if (isChecked) {
- xmppConnectionService.sendPresencePacket(
- contact.getAccount(),
- xmppConnectionService
- .getPresenceGenerator()
- .requestPresenceUpdatesFrom(contact));
+ connection
+ .getManager(PresenceManager.class)
+ .subscribe(contact.getJid().asBareJid());
} else {
- xmppConnectionService.sendPresencePacket(
- contact.getAccount(),
- xmppConnectionService
- .getPresenceGenerator()
- .stopPresenceUpdatesFrom(contact));
+ connection
+ .getManager(PresenceManager.class)
+ .unsubscribe(contact.getJid().asBareJid());
}
}
};
@@ -178,16 +177,14 @@ public class ContactDetailsActivity extends OmemoActivity
private void checkContactPermissionAndShowAddDialog() {
if (hasContactsPermission()) {
showAddToPhoneBookDialog();
- } else if (QuickConversationsService.isContactListIntegration(this)
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ } else if (QuickConversationsService.isContactListIntegration(this)) {
requestPermissions(
new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
}
private boolean hasContactsPermission() {
- if (QuickConversationsService.isContactListIntegration(this)
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (QuickConversationsService.isContactListIntegration(this)) {
return checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED;
} else {
@@ -208,7 +205,7 @@ public class ContactDetailsActivity extends OmemoActivity
value = jid.toString();
}
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
- builder.setTitle(getString(R.string.action_add_phone_book));
+ builder.setTitle(getString(R.string.save_to_contact));
builder.setMessage(getString(R.string.add_phone_book_text, value));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(
@@ -327,14 +324,35 @@ public class ContactDetailsActivity extends OmemoActivity
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// TODO check for Camera / Scan permission
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (grantResults.length > 0)
+ if (grantResults.length == 0) {
+ return;
+ }
+ if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
- showAddToPhoneBookDialog();
- xmppConnectionService.loadPhoneContacts();
- xmppConnectionService.startContactObserver();
- }
+ showAddToPhoneBookDialog();
+ xmppConnectionService.loadPhoneContacts();
+ xmppConnectionService.startContactObserver();
+ } else {
+ showRedirectToAppSettings();
}
+ }
+ }
+
+ private void showRedirectToAppSettings() {
+ final var dialogBuilder = new MaterialAlertDialogBuilder(this);
+ dialogBuilder.setTitle(R.string.save_to_contact);
+ dialogBuilder.setMessage(
+ getString(R.string.no_contacts_permission, getString(R.string.app_name)));
+ dialogBuilder.setPositiveButton(
+ R.string.continue_btn,
+ (d, w) -> {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ Uri uri = Uri.fromParts("package", getPackageName(), null);
+ intent.setData(uri);
+ startActivity(intent);
+ });
+ dialogBuilder.setNegativeButton(R.string.cancel, null);
+ dialogBuilder.create().show();
}
protected void saveEdits() {
@@ -343,7 +361,8 @@ public class ContactDetailsActivity extends OmemoActivity
EditText text = edit.getActionView().findViewById(R.id.search_field);
contact.setServerName(text.getText().toString());
contact.setGroups(binding.editTags.getObjects().stream().map(tag -> tag.getName()).collect(Collectors.toList()));
- ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
+ final var connection = contact.getAccount().getXmppConnection();
+ connection.getManager(RosterManager.class).addRosterItem(contact, null);
populateView();
edit.collapseActionView();
}
@@ -574,29 +593,41 @@ public class ContactDetailsActivity extends OmemoActivity
}
if (contact.isBlocked() && !this.showDynamicTags) {
- binding.detailsLastseen.setVisibility(View.VISIBLE);
- binding.detailsLastseen.setText(R.string.contact_blocked);
+ binding.detailsLastSeen.setVisibility(View.VISIBLE);
+ binding.detailsLastSeen.setText(R.string.contact_blocked);
} else {
if (showLastSeen
&& contact.getLastseen() > 0
&& contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
- binding.detailsLastseen.setVisibility(View.VISIBLE);
- binding.detailsLastseen.setText(
+ binding.detailsLastSeen.setVisibility(View.VISIBLE);
+ binding.detailsLastSeen.setText(
UIHelper.lastseen(
getApplicationContext(),
contact.isActive(),
contact.getLastseen()));
} else {
- binding.detailsLastseen.setVisibility(View.GONE);
+ binding.detailsLastSeen.setVisibility(View.GONE);
}
}
- binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
+ binding.detailsContactXmppAddress.setText(
+ IrregularUnicodeDetector.style(this, contact.getJid()));
final String account = contact.getAccount().getJid().asBareJid().toString();
+ binding.detailsAccount.setOnClickListener(this::onDetailsAccountClicked);
binding.detailsAccount.setText(getString(R.string.using_account, account));
- AvatarWorkerTask.loadAvatar(
- contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size);
- binding.detailsContactBadge.setOnClickListener(this::onBadgeClick);
+ AvatarWorkerTask.loadAvatar(contact, binding.detailsAvatar, R.dimen.publish_avatar_size);
+ binding.detailsAvatar.setOnClickListener(this::onAvatarClicked);
+ if (QuickConversationsService.isContactListIntegration(this)) {
+ if (contact.getSystemAccount() == null) {
+ binding.addAddressBook.setText(R.string.save_to_contact);
+ } else {
+ binding.addAddressBook.setText(R.string.show_in_contacts);
+ }
+ binding.addAddressBook.setVisibility(View.VISIBLE);
+ binding.addAddressBook.setOnClickListener(this::onAddToAddressBookClick);
+ } else {
+ binding.addAddressBook.setVisibility(View.GONE);
+ }
binding.detailsContactKeys.removeAllViews();
boolean hasKeys = false;
@@ -749,7 +780,30 @@ public class ContactDetailsActivity extends OmemoActivity
}
}
- private void onBadgeClick(final View view) {
+ private void onDetailsAccountClicked(final View view) {
+ final var contact = this.contact;
+ if (contact == null) {
+ return;
+ }
+ switchToAccount(contact.getAccount());
+ }
+
+ private void onAvatarClicked(final View view) {
+ final var contact = this.contact;
+ if (contact == null) {
+ return;
+ }
+ final var avatar = contact.getAvatar();
+ if (avatar == null) {
+ return;
+ }
+ final var intent = new Intent(this, ViewProfilePictureActivity.class);
+ intent.setData(Uri.fromParts("avatar", avatar, null));
+ intent.putExtra(ViewProfilePictureActivity.EXTRA_DISPLAY_NAME, contact.getDisplayName());
+ startActivity(intent);
+ }
+
+ private void onAddToAddressBookClick(final View view) {
if (QuickConversationsService.isContactListIntegration(this)) {
final Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
@@ -206,14 +206,20 @@ 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.jingle.RtpEndUserState;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.jetbrains.annotations.NotNull;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Arrays;
@@ -222,6 +228,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -536,7 +543,7 @@ public class ConversationFragment extends XmppFragment
public void onClick(View v) {
final Contact contact = conversation == null ? null : conversation.getContact();
if (contact != null) {
- activity.xmppConnectionService.createContact(contact, true);
+ activity.xmppConnectionService.createContact(contact);
activity.switchToContactDetails(contact);
}
}
@@ -548,11 +555,10 @@ public class ConversationFragment extends XmppFragment
public void onClick(View v) {
final Contact contact = conversation == null ? null : conversation.getContact();
if (contact != null) {
- activity.xmppConnectionService.sendPresencePacket(
- contact.getAccount(),
- activity.xmppConnectionService
- .getPresenceGenerator()
- .sendPresenceUpdatesTo(contact));
+ final var connection = contact.getAccount().getXmppConnection();
+ connection
+ .getManager(PresenceManager.class)
+ .subscribed(contact.getJid().asBareJid());
hideSnackbar();
}
}
@@ -1438,10 +1444,9 @@ public class ConversationFragment extends XmppFragment
}
menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false);
+ final var connection = this.conversation.getAccount().getXmppConnection();
menuInviteContact.setVisible(
- service != null
- && service.findConferenceServer(conversation.getAccount()) != null);
- menuArchiveChat.setTitle(R.string.action_archive_chat);
+ !connection.getManager(MultiUserChatManager.class).getServices().isEmpty());
}
if (conversation.isMuted()) {
menuMute.setVisible(false);
@@ -1572,14 +1577,14 @@ public class ConversationFragment extends XmppFragment
if (!activity.xmppConnectionService.getBooleanPreference("message_autocomplete", R.bool.message_autocomplete)) return;
final var allUsers = conversation.getMucOptions().getUsers();
- if (!conversation.getMucOptions().getUsersByRole(MucOptions.Role.MODERATOR).isEmpty()) {
+ if (!conversation.getMucOptions().getUsersByRole(Role.MODERATOR).isEmpty()) {
final var u = new MucOptions.User(conversation.getMucOptions(), null, "\0role:moderator", "Notify active moderators", new HashSet<>());
- u.setRole("participant");
+ u.setRole(Role.PARTICIPANT);
allUsers.add(u);
}
- if (!allUsers.isEmpty() && conversation.getMucOptions().getSelf() != null && conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ if (!allUsers.isEmpty() && conversation.getMucOptions().getSelf() != null && conversation.getMucOptions().getSelf().ranks(Affiliation.MEMBER)) {
final var u = new MucOptions.User(conversation.getMucOptions(), null, "\0attention", "Notify active participants", new HashSet<>());
- u.setRole("participant");
+ u.setRole(Role.PARTICIPANT);
allUsers.add(u);
}
final String needle = query.toString().toLowerCase(Locale.getDefault());
@@ -1636,7 +1641,7 @@ public class ConversationFragment extends XmppFragment
}
var insert = user.getNick();
if ("\0role:moderator".equals(user.getOccupantId())) {
- insert = conversation.getMucOptions().getUsersByRole(MucOptions.Role.MODERATOR).stream().map(MucOptions.User::getNick).collect(Collectors.joining(", "));
+ insert = conversation.getMucOptions().getUsersByRole(Role.MODERATOR).stream().map(MucOptions.User::getNick).collect(Collectors.joining(", "));
}
editable.replace(Math.max(0, range[0]), Math.min(editable.length(), range[1]), prefix + insert + suffix);
return true;
@@ -1903,9 +1908,9 @@ public class ConversationFragment extends XmppFragment
&& m.getErrorMessage() != null
&& !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage());
final Conversational conversational = m.getConversation();
+ final var connection = conversational.getAccount().getXmppConnection();
if (m.getStatus() == Message.STATUS_RECEIVED
&& conversational instanceof Conversation c) {
- final XmppConnection connection = c.getAccount().getXmppConnection();
if (c.isWithStranger()
&& m.getServerMsgId() != null
&& !c.isBlocked()
@@ -1916,7 +1921,7 @@ public class ConversationFragment extends XmppFragment
}
if (conversational instanceof Conversation c) {
addReaction.setVisible(
- !showError
+ m.getStatus() != Message.STATUS_SEND_FAILED
&& !m.isDeleted()
&& !m.isPrivateMessage()
&& (c.getMode() == Conversational.MODE_SINGLE
@@ -1967,7 +1972,7 @@ public class ConversationFragment extends XmppFragment
correctMessage.setVisible(true);
retractMessage.setVisible(true);
}
- if (conversation.getMode() == Conversation.MODE_MULTI && m.getServerMsgId() != null && m.getModerated() == null && conversation.getMucOptions().getSelf().getRole().ranks(MucOptions.Role.MODERATOR) && conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) {
+ if (conversation.getMode() == Conversation.MODE_MULTI && m.getServerMsgId() != null && m.getModerated() == null && conversation.getMucOptions().getSelf().ranks(Role.MODERATOR) && conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) {
moderateMessage.setVisible(true);
}
if ((m.isFileOrImage() && !deleted && !receiving)
@@ -1978,12 +1983,18 @@ public class ConversationFragment extends XmppFragment
}
if (m.getStatus() == Message.STATUS_SEND_FAILED) {
sendAgain.setVisible(true);
+ final var httpUploadAvailable =
+ connection != null
+ && Objects.nonNull(
+ connection
+ .getManager(HttpUploadManager.class)
+ .getService());
final var fileNotUploaded = m.isFileOrImage() && !m.hasFileOnRemoteHost();
final var isPeerOnline =
conversational.getMode() == Conversation.MODE_SINGLE
&& (conversational instanceof Conversation c)
&& !c.getContact().getPresences().isEmpty();
- retryAsP2P.setVisible(fileNotUploaded && isPeerOnline);
+ retryAsP2P.setVisible(fileNotUploaded && isPeerOnline && httpUploadAvailable);
}
if (m.hasFileOnRemoteHost()
|| m.isGeoUri()
@@ -2256,8 +2267,8 @@ public class ConversationFragment extends XmppFragment
.setTitle(R.string.block_media)
.setMessage("Do you really want to block this avatar?")
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
- activity.xmppConnectionService.blockMedia(activity.xmppConnectionService.getFileBackend().getAvatarFile(conversation.getContact().getAvatarFilename()));
- activity.xmppConnectionService.getFileBackend().getAvatarFile(conversation.getContact().getAvatarFilename()).delete();
+ activity.xmppConnectionService.blockMedia(activity.xmppConnectionService.getFileBackend().getAvatarFile(conversation.getContact().getAvatar()));
+ activity.xmppConnectionService.getFileBackend().getAvatarFile(conversation.getContact().getAvatar()).delete();
activity.avatarService().clear(conversation);
conversation.getContact().setAvatar(null);
activity.xmppConnectionService.updateConversationUi();
@@ -3149,8 +3160,8 @@ public class ConversationFragment extends XmppFragment
&& xmppConnection != null
&& conversation.getMode() == Conversational.MODE_SINGLE
&& (!xmppConnection
- .getFeatures()
- .httpUpload(message.getFileParams().getSize())
+ .getManager(HttpUploadManager.class)
+ .isAvailableForSize(message.getFileParams().getSize())
|| forceP2P)) {
activity.selectPresence(
conversation,
@@ -3551,7 +3562,7 @@ public class ConversationFragment extends XmppFragment
if (commandAdapter == null) return;
final CommandAdapter.MucConfig mucConfig =
- conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) ?
+ conversation.getMucOptions().getSelf().ranks(Affiliation.OWNER) ?
new CommandAdapter.MucConfig() :
null;
@@ -3859,7 +3870,7 @@ public class ConversationFragment extends XmppFragment
activity.xmppConnectionService.archiveConversation(conversation);
return true;
case R.id.add_bookmark:
- activity.xmppConnectionService.saveConversationAsBookmark(conversation, "");
+ conversation.getAccount().getXmppConnection().getManager(BookmarkManager.class).save(conversation, "");
updateSnackBar(conversation);
return true;
case R.id.block_contact:
@@ -4113,9 +4124,14 @@ public class ConversationFragment extends XmppFragment
mSendingPgpMessage.set(false);
}
- public long getMaxHttpUploadSize(Conversation conversation) {
- final XmppConnection connection = conversation.getAccount().getXmppConnection();
- return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
+ public Long getMaxHttpUploadSize(final Conversation conversation) {
+
+ final var connection = conversation.getAccount().getXmppConnection();
+ final var httpUploadService = connection.getManager(HttpUploadManager.class).getService();
+ if (httpUploadService == null) {
+ return -1L;
+ }
+ return httpUploadService.getMaxFileSize();
}
private boolean canWrite() {
@@ -26,6 +26,7 @@ import eu.siacs.conversations.ui.util.DelayedHintHelper;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -158,7 +159,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
}
final Editable nameText = binding.groupChatName.getText();
final String name = nameText == null ? "" : nameText.toString().trim();
- final String domain = connection.getMucServer();
+ final var domain = connection.getManager(MultiUserChatManager.class).getService();
if (domain == null) {
return "";
}
@@ -270,9 +271,8 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
private void refreshKnownHosts() {
Activity activity = getActivity();
- if (activity instanceof XmppActivity) {
- Collection<String> hosts =
- ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
+ if (activity instanceof XmppActivity xmppActivity) {
+ Collection<String> hosts = xmppActivity.xmppConnectionService.getKnownConferenceHosts();
this.knownHostsAdapter.refresh(hosts);
}
}
@@ -58,6 +58,9 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
import de.gultsch.common.Linkify;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
@@ -95,13 +98,16 @@ import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
-import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
+import im.conversations.android.xmpp.model.data.Data;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Arrays;
import java.util.List;
@@ -142,22 +148,19 @@ public class EditAccountActivity extends OmemoActivity
deleteAccountAndReturnIfNecessary();
finish();
};
- private final UiCallback<Avatar> mAvatarFetchCallback =
- new UiCallback<Avatar>() {
+ private final FutureCallback<Void> mAvatarFetchCallback =
+ new FutureCallback<>() {
@Override
- public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
- finishInitialSetup(avatar);
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, "found pre-existing avatar");
+ finishInitialSetup(true);
}
@Override
- public void success(final Avatar avatar) {
- finishInitialSetup(avatar);
- }
-
- @Override
- public void error(final int errorCode, final Avatar avatar) {
- finishInitialSetup(avatar);
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "failed to fetch avatar", t);
+ finishInitialSetup(false);
}
};
private final OnClickListener mAvatarClickListener =
@@ -485,7 +488,8 @@ public class EditAccountActivity extends OmemoActivity
} else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
if (!mFetchingAvatar) {
mFetchingAvatar = true;
- xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
+ final var future = xmppConnectionService.checkForAvatar(mAccount);
+ Futures.addCallback(future, mAvatarFetchCallback, MoreExecutors.directExecutor());
}
}
if (mAccount != null) {
@@ -552,7 +556,7 @@ public class EditAccountActivity extends OmemoActivity
refreshUi();
}
- protected void finishInitialSetup(final Avatar avatar) {
+ protected void finishInitialSetup(final boolean avatar) {
runOnUiThread(
() -> {
SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this);
@@ -561,7 +565,7 @@ public class EditAccountActivity extends OmemoActivity
final boolean wasFirstAccount =
xmppConnectionService != null
&& xmppConnectionService.getAccounts().size() == 1;
- if (avatar != null || (connection != null && !connection.getFeatures().pep())) {
+ if (avatar || (connection != null && !connection.getFeatures().pep())) {
intent =
new Intent(
getApplicationContext(), StartConversationActivity.class);
@@ -860,10 +864,10 @@ public class EditAccountActivity extends OmemoActivity
showBlocklist.setVisible(false);
}
- if (!mAccount.getXmppConnection().getFeatures().register()) {
- changePassword.setVisible(false);
- deleteAccount.setVisible(false);
- }
+ final var registration =
+ mAccount.getXmppConnection().getManager(RegistrationManager.class).hasFeature();
+ changePassword.setVisible(registration);
+ deleteAccount.setVisible(registration);
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
changePresence.setVisible(!mInitMode);
} else {
@@ -1430,13 +1434,15 @@ public class EditAccountActivity extends OmemoActivity
} else {
this.binding.serverInfoPep.setText(R.string.server_info_unavailable);
}
- if (features.httpUpload(0)) {
- final long maxFileSize = features.getMaxHttpUploadSize();
- if (maxFileSize > 0) {
+ final var httpUploadManager = connection.getManager(HttpUploadManager.class);
+ final var uploadService = httpUploadManager.getService();
+ if (uploadService != null) {
+ final Long maxFileSize = uploadService.getMaxFileSize();
+ if (maxFileSize == null) {
+ this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
+ } else {
this.binding.serverInfoHttpUpload.setText(
UIHelper.filesizeToString(maxFileSize));
- } else {
- this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
}
} else {
this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
@@ -1664,7 +1670,7 @@ public class EditAccountActivity extends OmemoActivity
mAccount.setPgpSignId(0);
mAccount.unsetPgpSignature();
xmppConnectionService.databaseBackend.updateAccount(mAccount);
- xmppConnectionService.sendPresence(mAccount);
+ mAccount.getXmppConnection().getManager(PresenceManager.class).available();
refreshUiReal();
});
builder.create().show();
@@ -1751,8 +1757,7 @@ public class EditAccountActivity extends OmemoActivity
}
@Override
- public void onCaptchaRequested(
- final Account account, final String id, final Data data, final Bitmap captcha) {
+ public void onCaptchaRequested(final Account account, final Data data, final Bitmap captcha) {
runOnUiThread(
() -> {
if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
@@ -1774,34 +1779,15 @@ public class EditAccountActivity extends OmemoActivity
builder.setPositiveButton(
getString(R.string.ok),
- (dialog, which) -> {
- String rc = input.getText().toString();
- data.put("username", account.getUsername());
- data.put("password", account.getPassword());
- data.put("ocr", rc);
- data.submit();
-
- if (xmppConnectionServiceBound) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, id, data);
- }
- });
+ (dialog, which) ->
+ account.getXmppConnection()
+ .register(data, input.getText().toString()));
builder.setNegativeButton(
getString(R.string.cancel),
- (dialog, which) -> {
- if (xmppConnectionService != null) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, null, null);
- }
- });
+ (dialog, which) -> account.getXmppConnection().cancelRegistration());
builder.setOnCancelListener(
- dialog -> {
- if (xmppConnectionService != null) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, null, null);
- }
- });
+ dialog -> account.getXmppConnection().cancelRegistration());
mCaptchaDialog = builder.create();
mCaptchaDialog.show();
input.requestFocus();
@@ -33,6 +33,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.adapter.UserAdapter;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.muc.Affiliation;
public class MucUsersActivity extends XmppActivity implements XmppConnectionService.OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, MenuItem.OnActionExpandListener, TextWatcher {
@@ -60,7 +61,7 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
private void loadAndSubmitUsers() {
if (mConversation != null) {
- allUsers = mConversation.getMucOptions().getUsers(true, mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.ADMIN));
+ allUsers = mConversation.getMucOptions().getUsers(true, mConversation.getMucOptions().getSelf().ranks(Affiliation.ADMIN));
submitFilteredList(mSearchEditText != null ? mSearchEditText.getText().toString() : null);
}
}
@@ -17,11 +17,15 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.canhub.cropper.CropImageContract;
import com.canhub.cropper.CropImageContractOptions;
import com.canhub.cropper.CropImageOptions;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@@ -30,14 +34,15 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.xmpp.manager.AvatarManager;
+import im.conversations.android.xmpp.NodeConfiguration;
public class PublishProfilePictureActivity extends XmppActivity
implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication {
- public static final int REQUEST_CHOOSE_PICTURE = 0x1337;
-
private ActivityPublishProfilePictureBinding binding;
private Uri avatarUri;
+ private NodeConfiguration.AccessModel accessModel;
private Uri defaultUri;
private Account account;
private boolean support = false;
@@ -117,7 +122,7 @@ public class PublishProfilePictureActivity extends XmppActivity
}
publishing = true;
togglePublishButton(false, R.string.publishing);
- xmppConnectionService.publishAvatarAsync(account, uri, open, this);
+ xmppConnectionService.publishAvatar(account, uri, open, this);
});
this.binding.cancelButton.setOnClickListener(
v -> {
@@ -138,6 +143,10 @@ public class PublishProfilePictureActivity extends XmppActivity
this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext());
if (savedInstanceState != null) {
this.avatarUri = savedInstanceState.getParcelable("uri");
+ final var accessModel = savedInstanceState.getString("access-model");
+ if (accessModel != null) {
+ this.accessModel = NodeConfiguration.AccessModel.valueOf(accessModel);
+ }
}
}
@@ -180,6 +189,9 @@ public class PublishProfilePictureActivity extends XmppActivity
if (this.avatarUri != null) {
outState.putParcelable("uri", this.avatarUri);
}
+ if (this.accessModel != null) {
+ outState.putString("access-model", this.accessModel.toString());
+ }
super.onSaveInstanceState(outState);
}
@@ -194,8 +206,8 @@ public class PublishProfilePictureActivity extends XmppActivity
cropImageOptions.fixAspectRatio = true;
cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG;
cropImageOptions.imageSourceIncludeCamera = false;
- cropImageOptions.minCropResultHeight = Config.AVATAR_SIZE;
- cropImageOptions.minCropResultWidth = Config.AVATAR_SIZE;
+ cropImageOptions.minCropResultHeight = Config.AVATAR_THUMBNAIL_SIZE;
+ cropImageOptions.minCropResultWidth = Config.AVATAR_THUMBNAIL_SIZE;
return cropImageOptions;
}
@@ -211,18 +223,57 @@ public class PublishProfilePictureActivity extends XmppActivity
@Override
protected void onBackendConnected() {
- this.account = extractAccount(getIntent());
- if (this.account != null) {
- reloadAvatar();
+ final var account = extractAccount(getIntent());
+ this.account = account;
+ if (account != null) {
+ loadCurrentAccessModel(account);
+ reloadAvatar(account);
}
}
+ private void loadCurrentAccessModel(final Account account) {
+ binding.contactOnly.setVisibility(View.INVISIBLE);
+ final var currentPepAccessModel = getPepAccessModelOrCached(account);
+ Futures.addCallback(
+ currentPepAccessModel,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final NodeConfiguration.AccessModel result) {
+ accessModel = result; // cache for after rotation
+ Log.d(Config.LOGTAG, "current access model: " + result);
+ binding.contactOnly.setChecked(
+ result == NodeConfiguration.AccessModel.PRESENCE);
+ binding.contactOnly.jumpDrawablesToCurrentState();
+ binding.contactOnly.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not fetch access model", t);
+ binding.contactOnly.setChecked(false);
+ binding.contactOnly.setVisibility(View.VISIBLE);
+ }
+ },
+ ContextCompat.getMainExecutor(getApplication()));
+ }
+
+ private ListenableFuture<NodeConfiguration.AccessModel> getPepAccessModelOrCached(
+ final Account account) {
+ final var cached = this.accessModel;
+ if (cached != null) {
+ return Futures.immediateFuture(cached);
+ }
+ return account.getXmppConnection().getManager(AvatarManager.class).getPepAccessModel();
+ }
+
private void reloadAvatar() {
- this.support =
- this.account.getXmppConnection() != null
- && this.account.getXmppConnection().getFeatures().pep();
+ reloadAvatar(this.account);
+ }
+
+ private void reloadAvatar(final Account account) {
+ this.support = account.getXmppConnection().getFeatures().pep();
if (this.avatarUri == null) {
- if (this.account.getAvatar() != null || this.defaultUri == null) {
+ if (account.getAvatar() != null || this.defaultUri == null) {
loadImageIntoPreview(null);
} else {
this.avatarUri = this.defaultUri;
@@ -318,6 +369,7 @@ public class PublishProfilePictureActivity extends XmppActivity
final boolean status = enabled && !publishing;
this.binding.publishButton.setText(publishing ? R.string.publishing : res);
this.binding.publishButton.setEnabled(status);
+ this.binding.contactOnly.setEnabled(status);
}
public void refreshUiReal() {
@@ -18,7 +18,6 @@ import android.view.WindowManager;
import android.widget.Toast;
import androidx.databinding.DataBindingUtil;
import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableSet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityRecordingBinding;
@@ -29,7 +28,6 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -119,33 +117,9 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe
}
}
- protected SharedPreferences getPreferences() {
- return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- }
-
- private static final Set<String> AAC_SENSITIVE_DEVICES =
- new ImmutableSet.Builder<String>()
- .add("FP4") // Fairphone 4
- // https://codeberg.org/monocles/monocles_chat/issues/133
- .add("ONEPLUS A6000") // OnePlus 6
- // https://github.com/iNPUTmice/Conversations/issues/4329
- .add("ONEPLUS A6003") // OnePlus 6
- // https://github.com/iNPUTmice/Conversations/issues/4329
- .add("ONEPLUS A6010") // OnePlus 6T
- // https://codeberg.org/monocles/monocles_chat/issues/133
- .add("ONEPLUS A6013") // OnePlus 6T
- // https://codeberg.org/monocles/monocles_chat/issues/133
- .add("Pixel 4a") // Pixel 4a
- // https://github.com/iNPUTmice/Conversations/issues/4223
- .add("WP12 Pro") // Oukitel WP 12 Pro
- // https://github.com/iNPUTmice/Conversations/issues/4223
- .add("Volla Phone X") // Volla Phone X
- // https://github.com/iNPUTmice/Conversations/issues/4223
- .build();
-
private boolean startRecording() {
mRecorder = new MediaRecorder();
- final String userChosenCodec = getPreferences().getString("voice_message_codec", "");
+ final String userChosenCodec = PreferenceManager.getDefaultSharedPreferences(this).getString("voice_message_codec", "");
stopwatch = Stopwatch.createUnstarted();
try {
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
@@ -166,18 +140,11 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe
} else if ("mpeg4".equals(userChosenCodec) || !Config.USE_OPUS_VOICE_MESSAGES) {
outputFormat = MediaRecorder.OutputFormat.MPEG_4;
mRecorder.setOutputFormat(outputFormat);
- if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)
- && Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
- // Changing these three settings for AAC sensitive devices for Android<=13 might
- // lead to sporadically truncated (cut-off) voice messages.
- mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
- mRecorder.setAudioSamplingRate(24_000);
- mRecorder.setAudioEncodingBitRate(28_000);
- } else {
- mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
- mRecorder.setAudioSamplingRate(44_100);
- mRecorder.setAudioEncodingBitRate(64_000);
- }
+ // Changing these three settings for AAC sensitive devices for Android<=13 might
+ // lead to sporadically truncated (cut-off) voice messages.
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
+ mRecorder.setAudioSamplingRate(24_000);
+ mRecorder.setAudioEncodingBitRate(28_000);
} else {
outputFormat = MediaRecorder.OutputFormat.THREE_GPP;
mRecorder.setOutputFormat(outputFormat);
@@ -715,7 +715,7 @@ public class StartConversationActivity extends XmppActivity
} else {
if (save) {
final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
- xmppConnectionService.createContact(contact, true, preAuth);
+ xmppConnectionService.createContact(contact, preAuth);
if (invite != null && invite.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
}
@@ -955,7 +955,7 @@ public class StartConversationActivity extends XmppActivity
Jid groupJid = Jid.ofLocalAndDomain(jids.stream().map(jid -> jid.getLocal()).sorted().collect(Collectors.joining(",")), "cheogram.com");
Contact group = account.getRoster().getContact(groupJid);
if (name != null && !name.equals("")) group.setServerName(name);
- xmppConnectionService.createContact(group, true);
+ xmppConnectionService.createContact(group);
switchToConversation(group);
}).create().show();
} else {
@@ -0,0 +1,47 @@
+package eu.siacs.conversations.ui;
+
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.databinding.DataBindingUtil;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityViewProfilePictureBinding;
+import eu.siacs.conversations.persistance.FileBackend;
+
+public class ViewProfilePictureActivity extends ActionBarActivity {
+
+ public static final String EXTRA_DISPLAY_NAME = "eu.siacs.conversations.extra.DISPLAY_NAME";
+
+ private ActivityViewProfilePictureBinding binding;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_view_profile_picture);
+ Activities.setStatusAndNavigationBarColors(this, binding.getRoot(), false, false);
+
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final var intent = getIntent();
+ if (intent == null) {
+ return;
+ }
+ final var uri = intent.getData();
+ if (uri == null) {
+ return;
+ }
+ final var avatar = uri.getSchemeSpecificPart();
+ if (avatar == null) {
+ return;
+ }
+ final var displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME);
+ final var file = FileBackend.getAvatarFile(this, avatar);
+ this.binding.imageView.setImageURI(Uri.fromFile(file));
+ setTitle(displayName);
+ }
+}
@@ -86,6 +86,9 @@ import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.RejectedExecutionException;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
@@ -116,6 +119,8 @@ import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -466,92 +471,96 @@ public abstract class XmppActivity extends ActionBarActivity {
protected void deleteAccount(final Account account, final Runnable postDelete) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
- final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
builder.setView(dialogView);
builder.setTitle(R.string.mgmt_account_delete);
builder.setPositiveButton(getString(R.string.delete), null);
builder.setNegativeButton(getString(R.string.cancel), null);
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(
- dialogInterface -> {
- final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
- button.setOnClickListener(
- v -> {
- final boolean unregister = deleteFromServer.isChecked();
- if (unregister) {
- if (account.isOnlineAndConnected()) {
- deleteFromServer.setEnabled(false);
- button.setText(R.string.please_wait);
- button.setEnabled(false);
- xmppConnectionService.unregisterAccount(
- account,
- result -> {
- runOnUiThread(
- () -> {
- if (result) {
- dialog.dismiss();
- if (postDelete != null) {
- postDelete.run();
- }
- if (xmppConnectionService
- .getAccounts()
- .size()
- == 0
- && Config
- .MAGIC_CREATE_DOMAIN
- != null) {
- final Intent intent =
- SignupUtils
- .getSignUpIntent(
- this);
- intent.setFlags(
- Intent
- .FLAG_ACTIVITY_NEW_TASK
- | Intent
- .FLAG_ACTIVITY_CLEAR_TASK);
- startActivity(intent);
- }
- } else {
- deleteFromServer.setEnabled(
- true);
- button.setText(R.string.delete);
- button.setEnabled(true);
- Toast.makeText(
- this,
- R.string
- .could_not_delete_account_from_server,
- Toast
- .LENGTH_LONG)
- .show();
- }
- });
- });
- } else {
- Toast.makeText(
- this,
- R.string.not_connected_try_again,
- Toast.LENGTH_LONG)
- .show();
- }
- } else {
- xmppConnectionService.deleteAccount(account);
- dialog.dismiss();
- if (xmppConnectionService.getAccounts().size() == 0
- && Config.MAGIC_CREATE_DOMAIN != null) {
- final Intent intent = SignupUtils.getSignUpIntent(this);
- intent.setFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- startActivity(intent);
- } else if (postDelete != null) {
- postDelete.run();
- }
- }
- });
- });
+ dialogInterface -> onShowDeleteDialog(dialogInterface, account, postDelete));
dialog.show();
}
+ private void onShowDeleteDialog(
+ final DialogInterface dialogInterface,
+ final Account account,
+ final Runnable postDelete) {
+ final AlertDialog alertDialog;
+ if (dialogInterface instanceof AlertDialog dialog) {
+ alertDialog = dialog;
+ } else {
+ throw new IllegalStateException("DialogInterface was not of type AlertDialog");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ button.setOnClickListener(
+ v -> onDeleteDialogButtonClicked(alertDialog, account, postDelete));
+ }
+
+ private void onDeleteDialogButtonClicked(
+ final AlertDialog dialog, final Account account, final Runnable postDelete) {
+ final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server);
+ if (deleteFromServer == null) {
+ throw new IllegalStateException("AlertDialog did not have button");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ final boolean unregister = deleteFromServer.isChecked();
+ if (unregister) {
+ if (account.isOnlineAndConnected()) {
+ final var connection = account.getXmppConnection();
+ deleteFromServer.setEnabled(false);
+ button.setText(R.string.please_wait);
+ button.setEnabled(false);
+ final var future = connection.getManager(RegistrationManager.class).unregister();
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ runOnUiThread(
+ () -> onAccountDeletedSuccess(account, dialog, postDelete));
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not unregister account", t);
+ runOnUiThread(() -> onAccountDeletionFailure(dialog, postDelete));
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ onAccountDeletedSuccess(account, dialog, postDelete);
+ }
+ }
+
+ private void onAccountDeletedSuccess(
+ final Account account, final AlertDialog dialog, final Runnable postDelete) {
+ xmppConnectionService.deleteAccount(account);
+ dialog.dismiss();
+ if (xmppConnectionService.getAccounts().isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) {
+ final Intent intent = SignupUtils.getSignUpIntent(this);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ } else if (postDelete != null) {
+ postDelete.run();
+ }
+ }
+
+ private void onAccountDeletionFailure(final AlertDialog dialog, final Runnable postDelete) {
+ final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server);
+ if (deleteFromServer == null) {
+ throw new IllegalStateException("AlertDialog did not have button");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ deleteFromServer.setEnabled(true);
+ button.setText(R.string.delete);
+ button.setEnabled(true);
+ Toast.makeText(this, R.string.could_not_delete_account_from_server, Toast.LENGTH_LONG)
+ .show();
+ }
+
protected abstract void onBackendConnected();
protected void registerListeners() {
@@ -932,7 +941,9 @@ public abstract class XmppActivity extends ActionBarActivity {
public void success(String signature) {
account.setPgpSignature(signature);
xmppConnectionService.databaseBackend.updateAccount(account);
- xmppConnectionService.sendPresence(account);
+ account.getXmppConnection()
+ .getManager(PresenceManager.class)
+ .available();
if (conversation != null) {
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
xmppConnectionService.updateConversation(conversation);
@@ -1007,7 +1018,7 @@ public abstract class XmppActivity extends ActionBarActivity {
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
contact.copySystemTagsToGroups();
- xmppConnectionService.createContact(contact, true);
+ xmppConnectionService.createContact(contact);
});
builder.create().show();
}
@@ -1020,13 +1031,10 @@ public abstract class XmppActivity extends ActionBarActivity {
builder.setPositiveButton(
R.string.request_now,
(dialog, which) -> {
- if (xmppConnectionServiceBound) {
- xmppConnectionService.sendPresencePacket(
- contact.getAccount(),
- xmppConnectionService
- .getPresenceGenerator()
- .requestPresenceUpdatesFrom(contact));
- }
+ final var connection = contact.getAccount().getXmppConnection();
+ connection
+ .getManager(PresenceManager.class)
+ .subscribe(contact.getJid().asBareJid());
});
builder.create().show();
}
@@ -10,7 +10,6 @@ import android.widget.TextView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
@@ -28,6 +27,7 @@ import java.util.List;
import org.openintents.openpgp.util.OpenPgpUtils;
+import com.google.common.base.Strings;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.databinding.ItemContactBinding;
@@ -40,30 +40,36 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.muc.Role;
+import org.openintents.openpgp.util.OpenPgpUtils;
-public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
-
- static final DiffUtil.ItemCallback<MucOptions.User> DIFF = new DiffUtil.ItemCallback<MucOptions.User>() {
- @Override
- public boolean areItemsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
- final Jid fullA = a.getFullJid();
- final Jid fullB = b.getFullJid();
- final Jid realA = a.getRealJid();
- final Jid realB = b.getRealJid();
- if (fullA != null && fullB != null) {
- return fullA.equals(fullB);
- } else if (realA != null && realB != null) {
- return realA.equals(realB);
- } else {
- return false;
- }
- }
+public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder>
+ implements View.OnCreateContextMenuListener {
+
+ static final DiffUtil.ItemCallback<MucOptions.User> DIFF =
+ new DiffUtil.ItemCallback<MucOptions.User>() {
+ @Override
+ public boolean areItemsTheSame(
+ @NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+ final Jid fullA = a.getFullJid();
+ final Jid fullB = b.getFullJid();
+ final Jid realA = a.getRealJid();
+ final Jid realB = b.getRealJid();
+ if (fullA != null && fullB != null) {
+ return fullA.equals(fullB);
+ } else if (realA != null && realB != null) {
+ return realA.equals(realB);
+ } else {
+ return false;
+ }
+ }
- @Override
- public boolean areContentsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
- return a.equals(b);
- }
- };
+ @Override
+ public boolean areContentsTheSame(
+ @NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+ return a.equals(b);
+ }
+ };
private final boolean advancedMode;
private MucOptions.User selectedUser = null;
@@ -75,36 +81,49 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
- return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_contact, viewGroup, false));
+ return new ViewHolder(
+ DataBindingUtil.inflate(
+ LayoutInflater.from(viewGroup.getContext()),
+ R.layout.item_contact,
+ viewGroup,
+ false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
final MucOptions.User user = getItem(position);
AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar);
- viewHolder.binding.getRoot().setOnClickListener(v -> {
- final XmppActivity activity = XmppActivity.find(v);
- if (activity == null) {
- return;
- }
- final var contact = user.getContact();
- if (user.getRole() == MucOptions.Role.NONE && contact != null) {
- Toast.makeText(
- activity,
- activity.getString(
- R.string.user_has_left_conference,
- contact.getDisplayName()),
- Toast.LENGTH_SHORT)
- .show();
- }
- activity.highlightInMuc(user.getConversation(), user.getName());
- });
+ viewHolder
+ .binding
+ .getRoot()
+ .setOnClickListener(
+ v -> {
+ final XmppActivity activity = XmppActivity.find(v);
+ if (activity == null) {
+ return;
+ }
+ final var contact = user.getContact();
+ if (user.getRole() == Role.NONE && contact != null) {
+ Toast.makeText(
+ activity,
+ activity.getString(
+ R.string.user_has_left_conference,
+ contact.getDisplayName()),
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+ activity.highlightInMuc(user.getConversation(), user.getName());
+ });
viewHolder.binding.getRoot().setTag(user);
viewHolder.binding.getRoot().setOnCreateContextMenuListener(this);
- viewHolder.binding.getRoot().setOnLongClickListener(v -> {
- selectedUser = user;
- return false;
- });
+ viewHolder
+ .binding
+ .getRoot()
+ .setOnLongClickListener(
+ v -> {
+ selectedUser = user;
+ return false;
+ });
final String name = user.getNick();
final Contact contact = user.getContact();
viewHolder.binding.contactJid.setVisibility(View.GONE);
@@ -113,30 +132,51 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
final String displayName = contact.getDisplayName();
viewHolder.binding.contactDisplayName.setText(displayName);
if (name != null && !name.equals(displayName)) {
- viewHolder.binding.contactJid.setVisibility(View.VISIBLE);
- viewHolder.binding.contactJid.setText(name);
+ viewHolder.binding.contactJid.setText(
+ String.format(
+ "%s \u2022 %s",
+ name,
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(),
+ user,
+ advancedMode)));
+ } else {
+ viewHolder.binding.contactJid.setText(
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(), user, advancedMode));
}
} else {
viewHolder.binding.contactDisplayName.setText(Strings.nullToEmpty(name));
- viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+ viewHolder.binding.contactJid.setText(
+ ConferenceDetailsActivity.getStatus(
+ viewHolder.binding.getRoot().getContext(), user, advancedMode));
}
if (advancedMode && user.getPgpKeyId() != 0) {
viewHolder.binding.key.setVisibility(View.VISIBLE);
- viewHolder.binding.key.setOnClickListener(v -> {
- final XmppActivity activity = XmppActivity.find(v);
- final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
- final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine();
- if (pgpEngine != null) {
- PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
- if (intent != null) {
- try {
- activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
- } catch (IntentSender.SendIntentException ignored) {
-
+ viewHolder.binding.key.setOnClickListener(
+ v -> {
+ final XmppActivity activity = XmppActivity.find(v);
+ final XmppConnectionService service =
+ activity == null ? null : activity.xmppConnectionService;
+ final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine();
+ if (pgpEngine != null) {
+ PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
+ if (intent != null) {
+ try {
+ activity.startIntentSenderForResult(
+ intent.getIntentSender(),
+ 0,
+ null,
+ 0,
+ 0,
+ 0,
+ Compatibility.pgpStartIntentSenderOptions());
+ } catch (IntentSender.SendIntentException ignored) {
+
+ }
+ }
}
- }
- }
- });
+ });
viewHolder.binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
} else {
viewHolder.binding.key.setVisibility(View.GONE);
@@ -180,8 +220,9 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
}
@Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- MucDetailsContextMenuHelper.onCreateContextMenu(menu,v);
+ public void onCreateContextMenu(
+ ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ MucDetailsContextMenuHelper.onCreateContextMenu(menu, v);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
@@ -5,18 +5,17 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
-
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemUserPreviewBinding;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+import im.conversations.android.xmpp.model.muc.Role;
public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreviewAdapter.ViewHolder>
implements View.OnCreateContextMenuListener {
@@ -52,7 +51,7 @@ public class UserPreviewAdapter extends ListAdapter<MucOptions.User, UserPreview
return;
}
final var contact = user.getContact();
- if (user.getRole() == MucOptions.Role.NONE && contact != null) {
+ if (user.getRole() == Role.NONE && contact != null) {
Toast.makeText(
activity,
activity.getString(
@@ -1,12 +1,11 @@
package eu.siacs.conversations.ui.util;
import android.content.Context;
-import android.os.Bundle;
-
import androidx.annotation.StringRes;
-
+import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.MucOptions;
+import java.util.Map;
public class MucConfiguration {
@@ -116,22 +115,23 @@ public class MucConfiguration {
return builder.toString();
}
- public Bundle toBundle(boolean[] values) {
- Bundle bundle = new Bundle();
+ public Map<String, Object> toBundle(boolean[] values) {
+ final var builder = new ImmutableMap.Builder<String, Object>();
for (int i = 0; i < values.length; ++i) {
final Option option = options[i];
- bundle.putString(option.name, option.values[values[i] ? 0 : 1]);
+ builder.put(option.name, option.values[values[i] ? 0 : 1]);
}
- return bundle;
+ builder.put("muc#roomconfig_persistentroom", true);
+ return builder.buildOrThrow();
}
private static class Option {
public final String name;
- public final String[] values;
+ public final Object[] values;
private Option(String name) {
this.name = name;
- this.values = new String[] {"1", "0"};
+ this.values = new Boolean[] {true, false};
}
private Option(String name, String on, String off) {
@@ -26,7 +26,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.ConferenceDetailsActivity;
import eu.siacs.conversations.ui.ConversationFragment;
@@ -36,6 +35,8 @@ import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xml.Element;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
public final class MucDetailsContextMenuHelper {
private static final int ACTION_BAN = 0;
@@ -49,7 +50,7 @@ public final class MucDetailsContextMenuHelper {
public static void onCreateContextMenu(ContextMenu menu, View v) {
final XmppActivity activity = XmppActivity.find(v);
final Object tag = v.getTag();
- if (tag instanceof User user && activity != null) {
+ if (tag instanceof MucOptions.User user && activity != null) {
activity.getMenuInflater().inflate(R.menu.muc_details_context, menu);
String name;
final Contact contact = user.getContact();
@@ -66,40 +67,40 @@ public final class MucDetailsContextMenuHelper {
}
}
- public static Pair<CharSequence[], Integer[]> getPermissionsChoices(Activity activity, Conversation conversation, User user) {
+ public static Pair<CharSequence[], Integer[]> getPermissionsChoices(Activity activity, Conversation conversation, MucOptions.User user) {
ArrayList<CharSequence> items = new ArrayList<>();
ArrayList<Integer> actions = new ArrayList<>();
- final User self = conversation.getMucOptions().getSelf();
+ final MucOptions.User self = conversation.getMucOptions().getSelf();
final MucOptions mucOptions = conversation.getMucOptions();
final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous();
- if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) || self.getAffiliation() == MucOptions.Affiliation.OWNER) {
- if (!Config.DISABLE_BAN && user.getAffiliation() != MucOptions.Affiliation.OUTCAST) {
+ if ((self.ranks(Affiliation.ADMIN) && self.outranks(user.getAffiliation())) || self.getAffiliation() == Affiliation.OWNER) {
+ if (!Config.DISABLE_BAN && user.getAffiliation() != Affiliation.OUTCAST) {
items.add(activity.getString(isGroupChat ? R.string.ban_from_conference : R.string.ban_from_channel));
actions.add(ACTION_BAN);
} else if (!Config.DISABLE_BAN) {
items.add(isGroupChat ? "Unban from group chat" : "Unban from channel");
actions.add(ACTION_REMOVE_MEMBERSHIP);
}
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ if (!user.ranks(Affiliation.MEMBER)) {
items.add(activity.getString(R.string.grant_membership));
actions.add(ACTION_GRANT_MEMBERSHIP);
- } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) {
+ } else if (user.getAffiliation() == Affiliation.MEMBER) {
items.add(activity.getString(R.string.remove_membership));
actions.add(ACTION_REMOVE_MEMBERSHIP);
}
}
- if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) {
+ if (self.ranks(Affiliation.OWNER)) {
+ if (!user.ranks(Affiliation.ADMIN)) {
items.add(activity.getString(R.string.grant_admin_privileges));
actions.add(ACTION_GRANT_ADMIN);
- } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
+ } else if (user.getAffiliation() == Affiliation.ADMIN) {
items.add(activity.getString(R.string.remove_admin_privileges));
actions.add(ACTION_REMOVE_ADMIN);
}
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (!user.ranks(Affiliation.OWNER)) {
items.add(activity.getString(R.string.grant_owner_privileges));
actions.add(ACTION_GRANT_OWNER);
- } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER){
+ } else if (user.getAffiliation() == Affiliation.OWNER){
items.add(activity.getString(R.string.remove_owner_privileges));
actions.add(ACTION_REMOVE_OWNER);
}
@@ -108,7 +109,7 @@ public final class MucDetailsContextMenuHelper {
}
public static void configureMucDetailsContextMenu(
- XmppActivity activity, Menu menu, Conversation conversation, User user) {
+ XmppActivity activity, Menu menu, Conversation conversation, MucOptions.User user) {
final MucOptions mucOptions = conversation.getMucOptions();
final boolean advancedMode =
PreferenceManager.getDefaultSharedPreferences(activity)
@@ -143,56 +144,56 @@ public final class MucDetailsContextMenuHelper {
MenuItem invite = menu.findItem(R.id.invite);
startConversation.setVisible(true);
final Contact contact = user.getContact();
- final User self = conversation.getMucOptions().getSelf();
+ final MucOptions.User self = conversation.getMucOptions().getSelf();
if ((contact != null && contact.showInRoster())
|| mucOptions.isPrivateAndNonAnonymous()) {
showContactDetails.setVisible(contact == null || !contact.isSelf());
}
if ((activity instanceof ConferenceDetailsActivity
|| activity instanceof MucUsersActivity)
- && user.getRole() == MucOptions.Role.NONE) {
+ && user.getRole() == Role.NONE) {
invite.setVisible(true);
}
boolean managePermissionsVisible = false;
- if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) || self.getAffiliation() == MucOptions.Affiliation.OWNER) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ if ((self.ranks(Affiliation.ADMIN) && self.outranks(user.getAffiliation())) || self.getAffiliation() == Affiliation.OWNER) {
+ if (!user.ranks(Affiliation.MEMBER)) {
managePermissionsVisible = true;
- } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) {
+ } else if (user.getAffiliation() == Affiliation.MEMBER) {
managePermissionsVisible = true;
}
if (!Config.DISABLE_BAN) {
managePermissionsVisible = true;
}
}
- if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ if (self.ranks(Affiliation.OWNER)) {
+ if (!user.ranks(Affiliation.OWNER)) {
managePermissionsVisible = true;
- } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER){
+ } else if (user.getAffiliation() == Affiliation.OWNER){
managePermissionsVisible = true;
}
- if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) {
+ if (!user.ranks(Affiliation.ADMIN)) {
managePermissionsVisible = true;
- } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) {
+ } else if (user.getAffiliation() == Affiliation.ADMIN) {
managePermissionsVisible = true;
}
}
managePermissions.setVisible(managePermissionsVisible);
- sendPrivateMessage.setVisible(showMucPm && user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
- shareContactDetails.setVisible(user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
+ sendPrivateMessage.setVisible(showMucPm && user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.ranks(Role.VISITOR));
+ shareContactDetails.setVisible(user.isOnline() && !isGroupChat && mucOptions.allowPm() && user.ranks(Role.VISITOR));
} else {
sendPrivateMessage.setVisible(showMucPm && user != null && user.isOnline());
- sendPrivateMessage.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
+ sendPrivateMessage.setEnabled(user != null && mucOptions.allowPm() && user.ranks(Role.VISITOR));
shareContactDetails.setVisible(user != null && user.isOnline());
- shareContactDetails.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR));
+ shareContactDetails.setEnabled(user != null && mucOptions.allowPm() && user.ranks(Role.VISITOR));
}
}
- public static boolean onContextItemSelected(MenuItem item, User user, XmppActivity activity) {
+ public static boolean onContextItemSelected(MenuItem item, MucOptions.User user, XmppActivity activity) {
return onContextItemSelected(item, user, activity, null);
}
- public static void maybeModerateRecent(XmppActivity activity, Conversation conversation, User user) {
- if (!conversation.getMucOptions().getSelf().getRole().ranks(MucOptions.Role.MODERATOR) || !conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) return;
+ public static void maybeModerateRecent(XmppActivity activity, Conversation conversation, MucOptions.User user) {
+ if (!conversation.getMucOptions().getSelf().ranks(Role.MODERATOR) || !conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) return;
DialogQuickeditBinding binding = DataBindingUtil.inflate(activity.getLayoutInflater(), R.layout.dialog_quickedit, null, false);
binding.inputEditText.setText("Spam");
@@ -209,7 +210,7 @@ public final class MucDetailsContextMenuHelper {
}
public static boolean onContextItemSelected(
- MenuItem item, User user, XmppActivity activity, final String fingerprint) {
+ MenuItem item, MucOptions.User user, XmppActivity activity, final String fingerprint) {
final Conversation conversation = user.getConversation();
final XmppConnectionService.OnAffiliationChanged onAffiliationChanged =
activity instanceof XmppConnectionService.OnAffiliationChanged
@@ -270,25 +271,25 @@ public final class MucDetailsContextMenuHelper {
.setPositiveButton(R.string.action_complete, (dialog, whichButton) -> {
switch (selected[0] >= 0 ? choices.second[selected[0]] : -1) {
case ACTION_BAN:
- activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OUTCAST, onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
- activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE);
+ activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, Affiliation.OUTCAST, onAffiliationChanged);
+ if (user.getRole() != Role.NONE) {
+ activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), Role.NONE);
}
maybeModerateRecent(activity, conversation, user);
break;
case ACTION_GRANT_MEMBERSHIP:
case ACTION_REMOVE_ADMIN:
case ACTION_REMOVE_OWNER:
- activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged);
+ activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, Affiliation.MEMBER, onAffiliationChanged);
break;
case ACTION_GRANT_ADMIN:
- activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged);
+ activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, Affiliation.ADMIN, onAffiliationChanged);
break;
case ACTION_GRANT_OWNER:
- activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OWNER, onAffiliationChanged);
+ activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, Affiliation.OWNER, onAffiliationChanged);
break;
case ACTION_REMOVE_MEMBERSHIP:
- activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged);
+ activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, Affiliation.NONE, onAffiliationChanged);
break;
}
})
@@ -320,7 +321,8 @@ public final class MucDetailsContextMenuHelper {
activity.xmppConnectionService.sendMessage(message);
return true;
case R.id.invite:
- if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
+ // TODO use direct invites for public conferences
+ if (user.ranks(Affiliation.MEMBER)) {
activity.xmppConnectionService.directInvite(conversation, jid.asBareJid());
} else {
activity.xmppConnectionService.invite(conversation, jid);
@@ -332,19 +334,16 @@ public final class MucDetailsContextMenuHelper {
}
private static void removeFromRoom(
- final User user,
+ final MucOptions.User user,
XmppActivity activity,
XmppConnectionService.OnAffiliationChanged onAffiliationChanged) {
final Conversation conversation = user.getConversation();
if (conversation.getMucOptions().membersOnly()) {
activity.xmppConnectionService.changeAffiliationInConference(
- conversation,
- user.getRealJid(),
- MucOptions.Affiliation.NONE,
- onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
+ conversation, user.getRealJid(), Affiliation.NONE, onAffiliationChanged);
+ if (user.getRole() != Role.NONE) {
activity.xmppConnectionService.changeRoleInConference(
- conversation, user.getName(), MucOptions.Role.NONE);
+ conversation, user.getName(), Role.NONE);
}
} else {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
@@ -369,18 +368,18 @@ public final class MucDetailsContextMenuHelper {
activity.xmppConnectionService.changeAffiliationInConference(
conversation,
user.getRealJid(),
- MucOptions.Affiliation.OUTCAST,
+ Affiliation.OUTCAST,
onAffiliationChanged);
- if (user.getRole() != MucOptions.Role.NONE) {
+ if (user.getRole() != Role.NONE) {
activity.xmppConnectionService.changeRoleInConference(
- conversation, user.getName(), MucOptions.Role.NONE);
+ conversation, user.getName(), Role.NONE);
}
});
builder.create().show();
}
}
- private static void startConversation(User user, XmppActivity activity) {
+ private static void startConversation(MucOptions.User user, XmppActivity activity) {
if (user.getRealJid() != null) {
Conversation newConversation =
activity.xmppConnectionService.findOrCreateConversation(
@@ -37,6 +37,10 @@ public class Compatibility {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
}
+ public static boolean thirtyFour() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+ }
+
public static void startService(final Context context, final Intent intent) {
try {
if (Compatibility.twentySix()) {
@@ -2,17 +2,16 @@ package eu.siacs.conversations.utils;
import android.os.Parcel;
import android.os.Parcelable;
-
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-
-import java.util.Collections;
-import java.util.List;
-
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
public class EasyOnboardingInvite implements Parcelable {
@@ -44,39 +43,42 @@ public class EasyOnboardingInvite implements Parcelable {
return 0;
}
- public static final Creator<EasyOnboardingInvite> CREATOR = new Creator<EasyOnboardingInvite>() {
- @Override
- public EasyOnboardingInvite createFromParcel(Parcel in) {
- return new EasyOnboardingInvite(in);
- }
+ public static final Creator<EasyOnboardingInvite> CREATOR =
+ new Creator<EasyOnboardingInvite>() {
+ @Override
+ public EasyOnboardingInvite createFromParcel(Parcel in) {
+ return new EasyOnboardingInvite(in);
+ }
- @Override
- public EasyOnboardingInvite[] newArray(int size) {
- return new EasyOnboardingInvite[size];
- }
- };
+ @Override
+ public EasyOnboardingInvite[] newArray(int size) {
+ return new EasyOnboardingInvite[size];
+ }
+ };
public static boolean anyHasSupport(final XmppConnectionService service) {
if (QuickConversationsService.isQuicksy()) {
return false;
}
- return getSupportingAccounts(service).size() > 0;
-
+ return !getSupportingAccounts(service).isEmpty();
}
public static List<Account> getSupportingAccounts(final XmppConnectionService service) {
- final ImmutableList.Builder<Account> supportingAccountsBuilder = new ImmutableList.Builder<>();
- final List<Account> accounts = service == null ? Collections.emptyList() : service.getAccounts();
- for(Account account : accounts) {
- final XmppConnection xmppConnection = account.getXmppConnection();
- if (xmppConnection != null && xmppConnection.getFeatures().easyOnboardingInvites()) {
+ final ImmutableList.Builder<Account> supportingAccountsBuilder =
+ new ImmutableList.Builder<>();
+ final List<Account> accounts =
+ service == null ? Collections.emptyList() : service.getAccounts();
+ for (final var account : accounts) {
+ final var connection = account.getXmppConnection();
+ final var discoManager = connection.getManager(DiscoManager.class);
+ if (Objects.nonNull(
+ discoManager.getAddressForCommand(Namespace.EASY_ONBOARDING_INVITE))) {
supportingAccountsBuilder.add(account);
}
}
return supportingAccountsBuilder.build();
}
-
public String getShareableLink() {
return Strings.isNullOrEmpty(landingUrl) ? uri : landingUrl;
}
@@ -91,6 +93,7 @@ public class EasyOnboardingInvite implements Parcelable {
public interface OnInviteRequested {
void inviteRequested(EasyOnboardingInvite invite);
+
void inviteRequestFailed(String message);
}
}
@@ -6,12 +6,9 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
-import android.os.Build;
import android.provider.ContactsContract.Profile;
import android.provider.Settings;
-
import com.google.common.base.Strings;
-
import eu.siacs.conversations.services.QuickConversationsService;
public class PhoneHelper {
@@ -23,9 +20,8 @@ public class PhoneHelper {
public static Uri getProfilePictureUri(final Context context) {
if (!QuickConversationsService.isContactListIntegration(context)
- || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
- != PackageManager.PERMISSION_GRANTED)) {
+ || context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
return null;
}
final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
@@ -42,24 +38,4 @@ public class PhoneHelper {
}
return null;
}
-
- public static boolean isEmulator() {
- return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
- || Build.FINGERPRINT.startsWith("generic")
- || Build.FINGERPRINT.startsWith("unknown")
- || Build.HARDWARE.contains("goldfish")
- || Build.HARDWARE.contains("ranchu")
- || Build.MODEL.contains("google_sdk")
- || Build.MODEL.contains("Emulator")
- || Build.MODEL.contains("Android SDK built for x86")
- || Build.MANUFACTURER.contains("Genymotion")
- || Build.PRODUCT.contains("sdk_google")
- || Build.PRODUCT.contains("google_sdk")
- || Build.PRODUCT.contains("sdk")
- || Build.PRODUCT.contains("sdk_x86")
- || Build.PRODUCT.contains("sdk_gphone64_arm64")
- || Build.PRODUCT.contains("vbox86p")
- || Build.PRODUCT.contains("emulator")
- || Build.PRODUCT.contains("simulator");
- }
}
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.utils;
-
-import java.util.HashMap;
-
-import eu.siacs.conversations.entities.Account;
-
-public class ReplacingTaskManager {
-
- private final HashMap<Account, ReplacingSerialSingleThreadExecutor> executors = new HashMap<>();
-
- public void execute(final Account account, Runnable runnable) {
- ReplacingSerialSingleThreadExecutor executor;
- synchronized (this.executors) {
- executor = this.executors.get(account);
- if (executor == null) {
- executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName());
- this.executors.put(account, executor);
- }
- executor.execute(runnable);
- }
- }
-
- public void clear(Account account) {
- synchronized (this.executors) {
- this.executors.remove(account);
- }
- }
-}
@@ -19,8 +19,13 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
-
import java.io.IOException;
+
+import de.gultsch.common.FutureMerger;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.Conversations;
+import eu.siacs.conversations.xmpp.Jid;
+
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -283,7 +288,7 @@ public class Resolver {
final var startTls = resolveSrvAsFuture(domain, false);
final var directTls = resolveSrvAsFuture(domain, true);
- final var combined = merge(ImmutableList.of(startTls, directTls));
+ final var combined = FutureMerger.successfulAsList(ImmutableList.of(startTls, directTls));
final var combinedWithFallback =
Futures.transformAsync(
@@ -374,7 +379,7 @@ public class Resolver {
futuresBuilder.add(ipv6s);
}
final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
- return merge(futures);
+ return FutureMerger.successfulAsList(futures);
}
private static ListenableFuture<List<Result>> merge(
@@ -450,13 +455,13 @@ public class Resolver {
Lists.transform(
ImmutableList.copyOf(result.getAnswersOrEmptySet()),
cname -> resolveNoSrvAsFuture(cname.target, false));
- return merge(test);
+ return FutureMerger.successfulAsList(test);
},
MoreExecutors.directExecutor());
futuresBuilder.add(cNameRecordResults);
}
final ImmutableList<ListenableFuture<List<Result>>> futures = futuresBuilder.build();
- final var noSrvFallbacks = merge(futures);
+ final var noSrvFallbacks = FutureMerger.successfulAsList(futures);
return Futures.transform(
noSrvFallbacks,
results -> {
@@ -30,7 +30,10 @@ import java.util.Locale;
import de.gultsch.common.Linkify;
import com.google.android.material.color.MaterialColors;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -327,6 +330,54 @@ public class UIHelper {
}
}
+ private static CharSequence getBodyOmitQuotesAndBlocks(final String body) {
+ final var parts = Splitter.on('\n').trimResults().omitEmptyStrings().splitToList(body);
+ final var filtered =
+ Collections2.filter(
+ parts,
+ line ->
+ !QuoteHelper.isPositionQuoteCharacter(line, 0)
+ && !line.equals("```"));
+ if (filtered.isEmpty()) {
+ return body;
+ }
+ return Joiner.on(' ').join(filtered);
+ }
+
+ private static CharSequence getStyledBodyOneLine(final String body, final int textColor) {
+ final var styledBody = new SpannableStringBuilder(body);
+ StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor, false);
+ final var builder = new SpannableStringBuilder();
+ for (final var l : CharSequenceUtils.split(styledBody, '\n')) {
+ if (l.length() == 0) {
+ continue;
+ }
+ if (l.toString().equals("```")) {
+ continue;
+ }
+ if (QuoteHelper.isPositionQuoteCharacter(l, 0)) {
+ continue;
+ }
+ final var trimmed = CharSequenceUtils.trim(l);
+ if (trimmed.length() == 0) {
+ continue;
+ }
+ char last = trimmed.charAt(trimmed.length() - 1);
+ if (builder.length() != 0) {
+ builder.append(' ');
+ }
+ builder.append(trimmed);
+ if (!PUNCTIONATION.contains(last)) {
+ break;
+ }
+ }
+ if (builder.length() == 0) {
+ return body.trim();
+ } else {
+ return builder;
+ }
+ }
+
public static boolean isLastLineQuote(String body) {
if (body.endsWith("\n")) {
return false;
@@ -1,6 +1,7 @@
package eu.siacs.conversations.xml;
import androidx.annotation.NonNull;
+import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -132,6 +133,16 @@ public class Element implements Node {
return findChild(name) != null;
}
+ public Element setAttribute(final String name, final Enum<?> e) {
+ if (e == null) {
+ this.attributes.remove(name);
+ } else {
+ this.attributes.put(
+ name, CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString()));
+ }
+ return this;
+ }
+
public boolean hasChild(final String name, final String xmlns) {
return findChild(name, xmlns) != null;
}
@@ -3,6 +3,7 @@ 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 BOB = "urn:xmpp:bob";
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";
@@ -14,10 +15,12 @@ public final class Namespace {
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 CAPTCHA = "urn:xmpp:captcha";
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 DIRECT_MUC_INVITATIONS = "jabber:x:conference";
public static final String DELAY = "urn:xmpp:delay";
public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0";
public static final String STREAMS = "http://etherx.jabber.org/streams";
@@ -33,6 +36,7 @@ public final class Namespace {
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0";
+ public static final String HTTP_UPLOAD_PURPOSE = "urn:xmpp:http:upload:purpose:0";
public static final String STANZA_IDS = "urn:xmpp:sid:0";
public static final String IDLE = "urn:xmpp:idle:1";
public static final String DATA = "jabber:x:data";
@@ -41,10 +45,15 @@ public final class Namespace {
public static final String SASL_2 = "urn:xmpp:sasl:2";
public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0";
public static final String FAST = "urn:xmpp:fast:0";
+ public static final String TIME = "urn:xmpp:time";
public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls";
public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
public static final String PUBSUB_EVENT = PUBSUB + "#event";
public static final String MUC = "http://jabber.org/protocol/muc";
+ public static final String MUC_ADMIN = MUC + "#admin";
+ public static final String MUC_OWNER = MUC + "#owner";
+ public static final String MUC_USER = MUC + "#user";
+ public static final String MUC_ROOM_INFO = MUC + "#roominfo";
public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max";
public static final String PUBSUB_ERROR = PUBSUB + "#errors";
@@ -91,10 +100,9 @@ public final class Namespace {
public static final String PING = "urn:xmpp:ping";
public static final String PUSH = "urn:xmpp:push:0";
public static final String COMMANDS = "http://jabber.org/protocol/commands";
- public static final String MUC_USER = "http://jabber.org/protocol/muc#user";
public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1";
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
- public static final String INVITE = "urn:xmpp:invite";
+ public static final String PRE_AUTHENTICATED_IN_BAND_REGISTRATION = "urn:xmpp:ibr-token:0";
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 =
@@ -107,9 +115,9 @@ public final class Namespace {
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
public static final String HASHES = "urn:xmpp:hashes:2";
+ public static final String MEDIA_ELEMENT = "urn:xmpp:media-element";
public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
-
public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
public static final String PRIVATE_XML_STORAGE = "jabber:iq:private";
@@ -0,0 +1,43 @@
+package im.conversations.android.xmpp;
+
+import com.google.common.base.Strings;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class IqErrorException extends Exception {
+
+ private final Iq response;
+
+ public IqErrorException(Iq response) {
+ super(getErrorText(response));
+ this.response = response;
+ }
+
+ public Error getError() {
+ return this.response.getError();
+ }
+
+ public Condition getErrorCondition() {
+ final var error = getError();
+ if (error == null) {
+ return null;
+ }
+ return error.getCondition();
+ }
+
+ private static String getErrorText(final Iq response) {
+ final var error = response.getError();
+ final var text = error == null ? null : error.getText();
+ final var textContent = text == null ? null : text.getContent();
+ if (Strings.isNullOrEmpty(textContent)) {
+ final var condition = error == null ? null : error.getExtension(Condition.class);
+ return condition == null ? null : condition.getName();
+ }
+ return textContent;
+ }
+
+ public Iq getResponse() {
+ return this.response;
+ }
+}
@@ -1,33 +0,0 @@
-package eu.siacs.conversations.xmpp;
-
-import im.conversations.android.xmpp.model.stanza.Iq;
-
-public class IqErrorResponseException extends Exception {
-
- private final Iq response;
-
- public IqErrorResponseException(final Iq response) {
- super(message(response));
- this.response = response;
- }
-
- public Iq getResponse() {
- return this.response;
- }
-
- public static String message(final Iq iq) {
- final var error = iq.getError();
- if (error == null) {
- return "missing error element in response";
- }
- final var text = error.getTextAsString();
- if (text != null) {
- return text;
- }
- final var condition = error.getCondition();
- if (condition != null) {
- return condition.getName();
- }
- return "no condition attached to error";
- }
-}
@@ -15,7 +15,7 @@ public abstract class Jid implements Comparable<Jid>, Serializable, CharSequence
private static final Pattern HOSTNAME_PATTERN =
Pattern.compile(
- "^(?=.{1,253}$)(?=.{1,253}$)(?!-)(?!.*--)(?!.*-$)[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$");
+ "^(?=.{1,253}$)(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?(?:\\.(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?)*\\.?$");
public static Jid of(
final CharSequence local, final CharSequence domain, final CharSequence resource) {
@@ -1,14 +1,33 @@
package eu.siacs.conversations.xmpp;
-import android.content.Context;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.AvatarManager;
+import eu.siacs.conversations.xmpp.manager.AxolotlManager;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.LegacyBookmarkManager;
+import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.NativeBookmarkManager;
+import eu.siacs.conversations.xmpp.manager.NickManager;
+import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
+import eu.siacs.conversations.xmpp.manager.PepManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
+import eu.siacs.conversations.xmpp.manager.PubSubManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.StreamHostManager;
+import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
+import eu.siacs.conversations.xmpp.manager.VCardManager;
public class Managers {
@@ -19,10 +38,32 @@ public class Managers {
public static ClassToInstanceMap<AbstractManager> get(
final XmppConnectionService context, final XmppConnection connection) {
return new ImmutableClassToInstanceMap.Builder<AbstractManager>()
+ .put(AvatarManager.class, new AvatarManager(context, connection))
+ .put(AxolotlManager.class, new AxolotlManager(context, connection))
+ .put(BlockingManager.class, new BlockingManager(context, connection))
+ .put(BookmarkManager.class, new BookmarkManager(context, connection))
.put(CarbonsManager.class, new CarbonsManager(context, connection))
.put(DiscoManager.class, new DiscoManager(context, connection))
+ .put(EntityTimeManager.class, new EntityTimeManager(context, connection))
+ .put(HttpUploadManager.class, new HttpUploadManager(context, connection))
+ .put(LegacyBookmarkManager.class, new LegacyBookmarkManager(context, connection))
+ .put(
+ MessageDisplayedSynchronizationManager.class,
+ new MessageDisplayedSynchronizationManager(context, connection))
+ .put(MultiUserChatManager.class, new MultiUserChatManager(context, connection))
+ .put(NativeBookmarkManager.class, new NativeBookmarkManager(context, connection))
+ .put(NickManager.class, new NickManager(context, connection))
+ .put(OfflineMessagesManager.class, new OfflineMessagesManager(context, connection))
+ .put(PepManager.class, new PepManager(context, connection))
.put(PingManager.class, new PingManager(context, connection))
.put(PresenceManager.class, new PresenceManager(context, connection))
+ .put(PrivateStorageManager.class, new PrivateStorageManager(context, connection))
+ .put(PubSubManager.class, new PubSubManager(context, connection))
+ .put(RegistrationManager.class, new RegistrationManager(context, connection))
+ .put(RosterManager.class, new RosterManager(context, connection))
+ .put(StreamHostManager.class, new StreamHostManager(context, connection))
+ .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection))
+ .put(VCardManager.class, new VCardManager(context, connection))
.build();
}
}
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp;
+
+import im.conversations.android.xmpp.model.pubsub.error.PubSubError;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class PreconditionNotMetException extends PubSubErrorException {
+
+ public PreconditionNotMetException(final Iq response) {
+ super(response);
+ if (this.pubSubError instanceof PubSubError.PreconditionNotMet) {
+ return;
+ }
+ throw new AssertionError(
+ "This exception should only be constructed for PreconditionNotMet errors");
+ }
+}
@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp;
+
+import im.conversations.android.xmpp.model.pubsub.error.PubSubError;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class PubSubErrorException extends IqErrorException {
+
+ protected final PubSubError pubSubError;
+
+ public PubSubErrorException(Iq response) {
+ super(response);
+ final var error = response.getError();
+ final var pubSubError = error == null ? null : error.getExtension(PubSubError.class);
+ if (pubSubError == null) {
+ throw new AssertionError("This exception should only be constructed for PubSubErrors");
+ }
+ this.pubSubError = pubSubError;
+ }
+}
@@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.SystemClock;
import android.security.KeyChain;
@@ -20,7 +18,9 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ClassToInstanceMap;
+import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
@@ -81,6 +81,8 @@ import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
+import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.sasl.ChannelBinding;
@@ -91,8 +93,6 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramMechanism;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
-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;
@@ -105,7 +105,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SSLSockets;
import eu.siacs.conversations.utils.SocksSocketFactory;
@@ -117,13 +116,16 @@ import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter;
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.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.IqErrorException;
import im.conversations.android.xmpp.model.AuthenticationFailure;
import im.conversations.android.xmpp.model.AuthenticationRequest;
import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
@@ -134,7 +136,6 @@ import im.conversations.android.xmpp.model.bind2.Bound;
import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
import im.conversations.android.xmpp.model.csi.Active;
import im.conversations.android.xmpp.model.csi.Inactive;
-import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.error.Condition;
import im.conversations.android.xmpp.model.fast.Fast;
import im.conversations.android.xmpp.model.fast.RequestToken;
@@ -162,10 +163,10 @@ import im.conversations.android.xmpp.model.stanza.Stanza;
import im.conversations.android.xmpp.model.streams.StreamError;
import im.conversations.android.xmpp.model.tls.Proceed;
import im.conversations.android.xmpp.model.tls.StartTls;
+import im.conversations.android.xmpp.processor.AccountStateProcessor;
import im.conversations.android.xmpp.processor.BindProcessor;
-import java.io.ByteArrayInputStream;
+import im.conversations.android.xmpp.processor.MessageAcknowledgedProcessor;
import java.io.IOException;
-import java.io.InputStream;
import java.net.ConnectException;
import java.net.IDN;
import java.net.InetAddress;
@@ -178,22 +179,19 @@ import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import javax.net.ssl.KeyManager;
@@ -209,7 +207,6 @@ public class XmppConnection implements Runnable {
protected final Account account;
private final Features features = new Features(this);
- private final HashMap<String, Jid> commands = new HashMap<>();
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 =
@@ -246,9 +243,11 @@ public class XmppConnection implements Runnable {
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 final Runnable bindListener;
- private OnMessageAcknowledged acknowledgedListener = null;
+ private final Consumer<Account.State> accountStateProcessor;
+ private final BiFunction<Jid, String, Boolean> messageAcknowledgedProcessor;
+ private AxolotlService axolotlService;
+ private final PgpDecryptionService pgpDecryptionService;
+ private final Runnable bindProcessor;
private final PendingItem<String> pendingResumeId = new PendingItem<>();
private LoginInfo loginInfo;
private HashedToken.Mechanism hashTokenRequest;
@@ -272,8 +271,12 @@ public class XmppConnection implements Runnable {
// TODO requires roster and blocking not to be handled by this
this.unregisteredIqListener = new IqParser(service, this);
this.messageListener = new MessageParser(service, this);
- this.bindListener = new BindProcessor(service, this);
+ this.bindProcessor = new BindProcessor(service, this);
+ this.accountStateProcessor = new AccountStateProcessor(service, this);
+ this.messageAcknowledgedProcessor = new MessageAcknowledgedProcessor(service, this);
this.managers = Managers.get(service, this);
+ this.setAxolotlService(new AxolotlService(account, service));
+ this.pgpDecryptionService = new PgpDecryptionService(service);
}
private static void fixResource(final Context context, final Account account) {
@@ -306,9 +309,13 @@ public class XmppConnection implements Runnable {
return currentResolverResult.isAuthenticated();
}
- private void changeStatus(final Account.State nextStatus) {
+ private void changeState(final Account.State nextStatus) {
+ this.changeState(nextStatus, true);
+ }
+
+ private void changeState(final Account.State nextStatus, final boolean skipOnInterrupt) {
synchronized (this) {
- if (Thread.currentThread().isInterrupted()) {
+ if (skipOnInterrupt && Thread.currentThread().isInterrupted()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -333,15 +340,15 @@ public class XmppConnection implements Runnable {
return;
}
}
- if (statusListener != null) {
- statusListener.onStatusChanged(account);
- }
+ this.accountStateProcessor.accept(nextStatus);
}
- public Jid getJidForCommand(final String node) {
- synchronized (this.commands) {
- return this.commands.get(node);
- }
+ private void changeStateTerminal(final Account.State state) {
+ // interrupt needs to be called before status change; otherwise we interrupt the newly
+ // created thread
+ this.interrupt();
+ this.forceCloseSocket();
+ this.changeState(state, false);
}
public void prepareNewConnection() {
@@ -349,7 +356,7 @@ public class XmppConnection implements Runnable {
this.lastPingSent = SystemClock.elapsedRealtime();
this.lastDiscoStarted = Long.MAX_VALUE;
this.mWaitingForSmCatchup.set(false);
- this.changeStatus(Account.State.CONNECTING);
+ this.changeState(Account.State.CONNECTING);
}
public boolean isWaitingForSmCatchup() {
@@ -380,7 +387,7 @@ public class XmppConnection implements Runnable {
try {
Socket localSocket;
shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
- this.changeStatus(Account.State.CONNECTING);
+ this.changeState(Account.State.CONNECTING);
final boolean useTorSetting = appSettings.isUseTor();
final boolean extended = appSettings.isExtendedConnectionOptions();
final boolean useTor = useTorSetting || account.isOnion();
@@ -582,27 +589,27 @@ public class XmppConnection implements Runnable {
}
processStream();
} catch (final SecurityException e) {
- this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
+ this.changeState(Account.State.MISSING_INTERNET_PERMISSION);
} catch (final StateChangingException e) {
- this.changeStatus(e.state);
+ this.changeState(e.state);
} catch (final UnknownHostException
| ConnectException
| SocksSocketFactory.HostNotFoundException e) {
- this.changeStatus(Account.State.SERVER_NOT_FOUND);
+ this.changeState(Account.State.SERVER_NOT_FOUND);
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
- this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
+ this.changeState(Account.State.TOR_NOT_AVAILABLE);
} catch (final IOException | XmlPullParserException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
- this.changeStatus(Account.State.OFFLINE);
+ this.changeState(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} finally {
- if (!Thread.currentThread().isInterrupted()) {
- forceCloseSocket();
- } else {
+ if (Thread.currentThread().isInterrupted()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": not force closing socket because thread was interrupted");
+ } else {
+ forceCloseSocket();
}
}
}
@@ -1238,7 +1245,7 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": awaiting disco results after resume");
- changeStatus(Account.State.CONNECTING);
+ changeState(Account.State.CONNECTING);
} else {
changeStatusToOnline();
}
@@ -1248,7 +1255,7 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": online with resource " + account.getResource());
- changeStatus(Account.State.ONLINE);
+ changeState(Account.State.ONLINE);
}
private void processFailed(final Failed failed, final boolean sendBindRequest) {
@@ -1301,12 +1308,11 @@ public class XmppConnection implements Runnable {
}
final Stanza stanza = mStanzaQueue.valueAt(i);
if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet
- && acknowledgedListener != null) {
+ && messageAcknowledgedProcessor != null) {
final String id = packet.getId();
final Jid to = packet.getTo();
if (id != null && to != null) {
- acknowledgedMessages |=
- acknowledgedListener.onMessageAcknowledged(account, to, id);
+ acknowledgedMessages |= messageAcknowledgedProcessor.apply(to, id);
}
}
mStanzaQueue.removeAt(i);
@@ -1603,12 +1609,12 @@ public class XmppConnection implements Runnable {
mXmppConnectionService.databaseBackend.updateAccount(account);
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
- if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
- && account.isOptionSet(Account.OPTION_REGISTER)) {
- register();
- } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
- && account.isOptionSet(Account.OPTION_REGISTER)) {
- throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
+ if (account.isOptionSet(Account.OPTION_REGISTER)) {
+ if (this.streamFeatures.register()) {
+ this.register();
+ } else {
+ throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
+ }
} else if (streamFeatures.hasStreamFeature(Authentication.class)
&& shouldAuthenticate
&& this.loginInfo == null) {
@@ -1867,7 +1873,7 @@ public class XmppConnection implements Runnable {
account, appSettings.getInstallationId())));
userAgent.setSoftware(
String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME));
- if (!PhoneHelper.isEmulator()) {
+ if (new Device(mXmppConnectionService).isPhysicalDevice()) {
userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
}
// do not include bind if 'inlineStreamManagement' is missing and we have a streamId
@@ -1911,167 +1917,95 @@ 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 Iq preAuthRequest = new Iq(Iq.Type.SET);
- preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
- sendUnmodifiedIqPacket(
- preAuthRequest,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- sendRegistryRequest();
+ final String preAuthToken =
+ Strings.emptyToNull(account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
+ final ListenableFuture<RegistrationManager.Registration> registrationFuture;
+ if (preAuthToken != null && streamFeatures.preAuthenticatedInBandRegistration()) {
+ registrationFuture =
+ getManager(RegistrationManager.class).getRegistration(preAuthToken);
+ } else {
+ registrationFuture = getManager(RegistrationManager.class).getRegistration();
+ }
+ // TODO should we store this future and cancel it during disconnect or something
+ Futures.addCallback(
+ registrationFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final RegistrationManager.Registration registration) {
+ if (registration instanceof RegistrationManager.SimpleRegistration) {
+ final var future = getManager(RegistrationManager.class).register();
+ awaitRegistrationResponse(future);
+ } else if (registration
+ instanceof RegistrationManager.ExtendedRegistration er) {
+ mXmppConnectionService.displayCaptchaRequest(
+ account, er.getData(), er.getCaptcha());
+ } else if (registration
+ instanceof
+ RegistrationManager.RedirectRegistration redirectRegistration) {
+ XmppConnection.this.redirectionUrl = redirectRegistration.getURL();
+ changeStateTerminal(Account.State.REGISTRATION_WEB);
} else {
- final String error = response.getErrorCondition();
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid()
- + ": failed to pre auth. "
- + error);
- throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
+ "got registration: " + registration.getClass().getName());
+ changeStateTerminal(Account.State.REGISTRATION_NOT_SUPPORTED);
}
- },
- true);
- } else {
- sendRegistryRequest();
- }
- }
-
- private void sendRegistryRequest() {
- final Iq register = new Iq(Iq.Type.GET);
- register.query(Namespace.REGISTER);
- register.setTo(account.getDomain());
- sendUnmodifiedIqPacket(
- register,
- (packet) -> {
- if (packet.getType() == Iq.Type.TIMEOUT) {
- return;
- }
- if (packet.getType() == Iq.Type.ERROR) {
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
}
- final Element query = packet.query(Namespace.REGISTER);
- if (query.hasChild("username") && (query.hasChild("password"))) {
- final Iq register1 = new Iq(Iq.Type.SET);
- final Element username =
- new Element("username").setContent(account.getUsername());
- final Element password =
- new Element("password").setContent(account.getPassword());
- register1.query(Namespace.REGISTER).addChild(username);
- register1.query().addChild(password);
- register1.setFrom(account.getJid().asBareJid());
- sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true);
- } else if (query.hasChild("x", Namespace.DATA)) {
- final Data data = Data.parse(query.findChild("x", Namespace.DATA));
- final Element blob = query.findChild("data", "urn:xmpp:bob");
- final String id = packet.getId();
- InputStream is;
- if (blob != null) {
- try {
- final String base64Blob = blob.getContent();
- final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
- is = new ByteArrayInputStream(strBlob);
- } catch (Exception e) {
- is = null;
- }
- } else {
- final boolean useTor = this.appSettings.isUseTor() || account.isOnion();
- try {
- final String url = data.getValue("url");
- final String fallbackUrl = data.getValue("captcha-fallback-url");
- if (url != null) {
- is = HttpConnectionManager.open(url, useTor);
- } else if (fallbackUrl != null) {
- is = HttpConnectionManager.open(fallbackUrl, useTor);
- } else {
- is = null;
- }
- } catch (final IOException e) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": unable to fetch captcha",
- e);
- is = null;
- }
- }
- if (is != null) {
- Bitmap captcha = BitmapFactory.decodeStream(is);
- try {
- if (mXmppConnectionService.displayCaptchaRequest(
- account, id, data, captcha)) {
- return;
- }
- } catch (Exception e) {
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
- }
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ return;
}
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
- } else if (query.hasChild("instructions")
- || query.hasChild("x", Namespace.OOB)) {
- final String instructions = query.findChildContent("instructions");
- final Element oob = query.findChild("x", Namespace.OOB);
- final String url = oob == null ? null : oob.findChildContent("url");
- if (url != null) {
- setAccountCreationFailed(url);
- } else if (instructions != null) {
- final Matcher matcher = Patterns.URI_HTTP.matcher(instructions);
- if (matcher.find()) {
- setAccountCreationFailed(
- instructions.substring(matcher.start(), matcher.end()));
- }
+ if (t instanceof RegistrationManager.InvalidTokenException) {
+ changeStateTerminal(Account.State.REGISTRATION_INVALID_TOKEN);
+ } else {
+ changeStateTerminal(Account.State.REGISTRATION_FAILED);
}
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
}
},
- true);
+ MoreExecutors.directExecutor());
}
- public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
- final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
- this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
+ public void register(
+ final im.conversations.android.xmpp.model.data.Data data, final String ocr) {
+ final var future = getManager(RegistrationManager.class).register(data, ocr);
+ awaitRegistrationResponse(future);
}
- private void processRegistrationResponse(final Iq response) {
- if (response.getType() == Iq.Type.RESULT) {
- account.setOption(Account.OPTION_REGISTER, false);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": successfully registered new account on server");
- throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
- } else {
- final Account.State state = getRegistrationFailedState(response);
- throw new StateChangingError(state);
- }
- }
-
- @NonNull
- private static Account.State getRegistrationFailedState(final Iq response) {
- final List<String> PASSWORD_TOO_WEAK_MESSAGES =
- Arrays.asList("The password is too weak", "Please use a longer password.");
- final var error = response.getError();
- final var condition = error == null ? null : error.getCondition();
- final Account.State state;
- if (condition instanceof Condition.Conflict) {
- state = Account.State.REGISTRATION_CONFLICT;
- } else if (condition instanceof Condition.ResourceConstraint) {
- state = Account.State.REGISTRATION_PLEASE_WAIT;
- } else if (condition instanceof Condition.NotAcceptable
- && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
- state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
- } else {
- state = Account.State.REGISTRATION_FAILED;
- }
- return state;
+ private void awaitRegistrationResponse(final ListenableFuture<Void> registration) {
+ Futures.addCallback(
+ registration,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ account.setOption(Account.OPTION_REGISTER, false);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": successfully registered new account on server");
+ changeStateTerminal(Account.State.REGISTRATION_SUCCESSFUL);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ return;
+ }
+ if (t
+ instanceof
+ RegistrationManager.RegistrationFailedException exception) {
+ changeStateTerminal(exception.asAccountState());
+ } else {
+ changeStateTerminal(Account.State.REGISTRATION_FAILED);
+ }
+ }
+ },
+ MoreExecutors.directExecutor());
}
- private void setAccountCreationFailed(final String url) {
- final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
- if (httpUrl != null && httpUrl.isHttps()) {
- this.redirectionUrl = httpUrl;
- throw new StateChangingError(Account.State.REGISTRATION_WEB);
- }
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
+ public void cancelRegistration() {
+ this.changeStateTerminal(Account.State.REGISTRATION_FAILED);
}
public HttpUrl getRedirectionUrl() {
@@ -2088,9 +2022,6 @@ public class XmppConnection implements Runnable {
}
this.redirectionUrl = null;
getManager(DiscoManager.class).clear();
- synchronized (this.commands) {
- this.commands.clear();
- }
this.loginInfo = null;
}
@@ -2388,31 +2319,6 @@ public class XmppConnection implements Runnable {
});
}
- private void discoverCommands() {
- final var future =
- getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
- Futures.addCallback(
- future,
- new FutureCallback<>() {
- @Override
- public void onSuccess(Map<String, Jid> result) {
- synchronized (XmppConnection.this.commands) {
- XmppConnection.this.commands.clear();
- XmppConnection.this.commands.putAll(result);
- }
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": could not fetch commands",
- throwable);
- }
- },
- MoreExecutors.directExecutor());
- }
-
public boolean isMamPreferenceAlways() {
return isMamPreferenceAlways;
}
@@ -2427,14 +2333,14 @@ public class XmppConnection implements Runnable {
private void finalizeBind() {
this.offlineMessagesRetrieved = false;
- this.bindListener.run();
+ this.bindProcessor.run();
this.changeStatusToOnline();
}
private void enableAdvancedStreamFeatures() {
- if (getFeatures().blocking() && !features.blockListRequested) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
- this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
+ final var blockingManager = getManager(BlockingManager.class);
+ if (blockingManager.hasFeature()) {
+ blockingManager.request();
}
for (final OnAdvancedStreamFeaturesLoaded listener :
advancedStreamFeaturesLoadedListeners) {
@@ -2444,8 +2350,9 @@ public class XmppConnection implements Runnable {
if (carbonsManager.hasFeature() && !carbonsManager.isEnabled()) {
carbonsManager.enable();
}
- if (getFeatures().commands()) {
- discoverCommands();
+ final var discoManager = getManager(DiscoManager.class);
+ if (discoManager.hasServerCommands()) {
+ discoManager.fetchServerCommands();
}
}
@@ -2585,17 +2492,22 @@ public class XmppConnection implements Runnable {
}
public ListenableFuture<Iq> sendIqPacket(final Iq request) {
+ return sendIqPacket(request, false);
+ }
+
+ public ListenableFuture<Iq> sendIqPacket(final Iq request, final boolean allowUnbound) {
final SettableFuture<Iq> settable = SettableFuture.create();
- this.sendIqPacket(
+ this.sendUnmodifiedIqPacket(
request,
response -> {
final var type = response.getType();
switch (type) {
case RESULT -> settable.set(response);
case TIMEOUT -> settable.setException(new TimeoutException());
- default -> settable.setException(new IqErrorResponseException(response));
+ default -> settable.setException(new IqErrorException(response));
}
- });
+ },
+ allowUnbound);
return settable;
}
@@ -2686,6 +2598,7 @@ public class XmppConnection implements Runnable {
return;
}
synchronized (this.mStanzaQueue) {
+ // TODO should we fail IQs for unbound streams?
if (force || isBound) {
tagWriter.writeStanzaAsync(packet);
} else {
@@ -2739,14 +2652,6 @@ public class XmppConnection implements Runnable {
this.jingleListener = listener;
}
- public void setOnStatusChangedListener(final OnStatusChanged listener) {
- this.statusListener = listener;
- }
-
- public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) {
- this.acknowledgedListener = listener;
- }
-
public void addOnAdvancedStreamFeaturesAvailableListener(
final OnAdvancedStreamFeaturesLoaded listener) {
this.advancedStreamFeaturesLoadedListeners.add(listener);
@@ -2821,61 +2726,10 @@ public class XmppConnection implements Runnable {
return this.managers.getInstance(clazz);
}
- private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
- final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
- for (final Entry<Jid, InfoQuery> cursor :
- getManager(DiscoManager.class).getServerItems().entrySet()) {
- if (cursor.getValue().getFeatureStrings().contains(feature)) {
- items.add(cursor);
- }
- }
- return items;
- }
-
- public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
- return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
- }
-
- public Jid findDiscoItemByFeature(final String feature) {
- final var items = findDiscoItemsByFeature(feature);
- if (items.isEmpty()) {
- return null;
- }
- return Iterables.getFirst(items, null).getKey();
- }
-
- public boolean r() {
- if (getFeatures().sm()) {
- this.tagWriter.writeStanzaAsync(new Request());
- return true;
- } else {
- return false;
- }
- }
-
- public List<String> getMucServersWithholdAccount() {
- final List<String> servers = getMucServers();
- servers.remove(account.getDomain().toString());
- return servers;
- }
-
- public List<String> getMucServers() {
- List<String> servers = new ArrayList<>();
- for (final Entry<Jid, InfoQuery> entry :
- getManager(DiscoManager.class).getServerItems().entrySet()) {
- final var value = entry.getValue();
- if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
- && value.hasIdentityWithCategoryAndType("conference", "text")
- && !value.getFeatureStrings().contains("jabber:iq:gateway")
- && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
- servers.add(entry.getKey().toString());
- }
- }
- return servers;
- }
-
- public String getMucServer() {
- return Iterables.getFirst(getMucServers(), null);
+ public Set<Jid> getMucServersWithholdAccount() {
+ final var services = getManager(MultiUserChatManager.class).getServices();
+ return ImmutableSet.copyOf(
+ Collections2.filter(services, s -> !s.equals(account.getDomain())));
}
public int getTimeToNextAttempt(final boolean aggressive) {
@@ -2939,10 +2793,6 @@ public class XmppConnection implements Runnable {
this.mInteractive = interactive;
}
- private IqGenerator getIqGenerator() {
- return mXmppConnectionService.getIqGenerator();
- }
-
public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
if (trackOfflineMessageRetrieval) {
getManager(PingManager.class)
@@ -2964,21 +2814,10 @@ public class XmppConnection implements Runnable {
return this.offlineMessagesRetrieved;
}
- public void fetchRoster() {
- final Iq iqPacket = new Iq(Iq.Type.GET);
- final var version = account.getRosterVersion();
- if (Strings.isNullOrEmpty(account.getRosterVersion())) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": fetching roster version " + version);
- }
- iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
- sendIqPacket(iqPacket, unregisteredIqListener);
- }
-
public void triggerConnectionTimeout() {
+
+ // TODO not triggering timeout while waiting for captcha input
+
final var duration = getConnectionDuration();
Log.d(
Config.LOGTAG,
@@ -2987,11 +2826,7 @@ public class XmppConnection implements Runnable {
// last connection time gets reset so time to next attempt is calculated correctly
this.lastConnectionStarted = SystemClock.elapsedRealtime();
- // interrupt needs to be called before status change; otherwise we interrupt the newly
- // created thread
- this.interrupt();
- this.forceCloseSocket();
- this.changeStatus(Account.State.CONNECTION_TIMEOUT);
+ this.changeStateTerminal(Account.State.CONNECTION_TIMEOUT);
}
public Account getAccount() {
@@ -3002,6 +2837,43 @@ public class XmppConnection implements Runnable {
return this.features;
}
+ public boolean fromServer(final Stanza stanza) {
+ final var account = getAccount().getJid();
+ final Jid from = stanza.getFrom();
+ return from == null
+ || from.equals(account.getDomain())
+ || from.equals(account.asBareJid())
+ || from.equals(account);
+ }
+
+ public boolean fromAccount(final Stanza stanza) {
+ final var account = getAccount().getJid();
+ final Jid from = stanza.getFrom();
+ return from == null || from.asBareJid().equals(account.asBareJid());
+ }
+
+ public AxolotlService getAxolotlService() {
+ return this.axolotlService;
+ }
+
+ public PgpDecryptionService getPgpDecryptionService() {
+ return this.pgpDecryptionService;
+ }
+
+ public void setAxolotlService(AxolotlService axolotlService) {
+ final var current = this.axolotlService;
+ if (current != null) {
+ this.advancedStreamFeaturesLoadedListeners.remove(current);
+ }
+ this.axolotlService = axolotlService;
+ this.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
+ }
+
+ public void setStatusAndTriggerProcessor(final Account.State state) {
+ this.account.setStatus(state);
+ this.accountStateProcessor.accept(state);
+ }
+
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@@ -3164,43 +3036,14 @@ public class XmppConnection implements Runnable {
return infoQuery != null && infoQuery.getFeatureStrings().contains(feature);
}
- public boolean commands() {
- return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
- }
-
- public boolean easyOnboardingInvites() {
- synchronized (commands) {
- return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
- }
- }
-
- public boolean bookmarksConversion() {
- return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION)
- && pepPublishOptions();
- }
-
public boolean blocking() {
- return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
+ return connection.getManager(BlockingManager.class).hasFeature();
}
public boolean spamReporting() {
return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
}
- public boolean flexibleOfflineMessageRetrieval() {
- return hasDiscoFeature(
- account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
- }
-
- public boolean register() {
- return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
- }
-
- public boolean invite() {
- return connection.streamFeatures != null
- && connection.streamFeatures.hasChild("register", Namespace.INVITE);
- }
-
public boolean sm() {
return streamId != null
|| (connection.streamFeatures != null
@@ -3212,6 +3055,7 @@ public class XmppConnection implements Runnable {
&& connection.streamFeatures.clientStateIndication();
}
+ // TODO move to manager
public boolean pep() {
final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep");
@@ -3289,56 +3133,6 @@ public class XmppConnection implements Runnable {
return HttpUrl.parse(address);
}
- public boolean httpUpload(long fileSize) {
- if (Config.DISABLE_HTTP_UPLOAD) {
- return false;
- }
- final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
- if (result == null) {
- return false;
- }
- final long maxSize;
- try {
- maxSize =
- Long.parseLong(
- result.getValue()
- .getServiceDiscoveryExtension(
- Namespace.HTTP_UPLOAD, "max-file-size"));
- } catch (final Exception e) {
- return true;
- }
- if (fileSize <= maxSize) {
- return true;
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": http upload is not available for files with"
- + " size "
- + fileSize
- + " (max is "
- + maxSize
- + ")");
- return false;
- }
- }
-
- public long getMaxHttpUploadSize() {
- final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
- if (result == null) {
- return -1;
- }
- try {
- return Long.parseLong(
- result.getValue()
- .getServiceDiscoveryExtension(
- Namespace.HTTP_UPLOAD, "max-file-size"));
- } catch (final Exception e) {
- return -1;
- // ignored
- }
- }
-
public boolean stanzaIds() {
return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS);
}
@@ -15,6 +15,7 @@ import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableSet;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -899,6 +900,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
public void deliverIbbPacket(final Account account, final Iq packet) {
+ // TODO use extensions
final String sid;
final Element payload;
final InbandBytestreamsTransport.PacketType packetType;
@@ -181,7 +181,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
}
@Override
- public void onFailure(@NonNull Throwable throwable) {}
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "could not prepare transport info", throwable);
+ }
},
MoreExecutors.directExecutor());
}
@@ -1301,7 +1303,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
this.file = file;
this.transportSecurity = transportSecurity;
this.transportTerminationLatch = transportTerminationLatch;
- this.total = transportSecurity == null ? total : (total + 16);
+ this.total =
+ transportSecurity == null ? total : (total + GCM_AUTHENTICATION_TAG_LENGTH);
this.updateRunnable = updateRunnable;
}
@@ -1443,7 +1446,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
if (this.transportSecurity == null) {
return fileOutputStream;
} else {
- final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ final var cipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
cipher.init(
false,
new AEADParameters(
@@ -14,7 +14,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
-import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -29,6 +28,9 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils;
import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.manager.StreamHostManager;
+import im.conversations.android.xmpp.model.socks5.Activate;
+import im.conversations.android.xmpp.model.socks5.Query;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.io.IOException;
import java.io.InputStream;
@@ -248,10 +250,9 @@ public class SocksByteStreamsTransport implements Transport {
final SettableFuture<String> iqFuture = SettableFuture.create();
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);
- final Element activate = query.addChild("activate");
- activate.setContent(id.with.toString());
+ final var query = proxyActivation.addExtension(new Query());
+ query.setSid(this.streamId);
+ query.addExtension(new Activate(id.with));
xmppConnection.sendIqPacket(
proxyActivation,
(response) -> {
@@ -275,7 +276,8 @@ public class SocksByteStreamsTransport implements Transport {
}
private ListenableFuture<Connection> getOurProxyConnection(final String ourDestination) {
- final var proxyFuture = getProxyCandidate();
+ final var proxyFuture =
+ xmppConnection.getManager(StreamHostManager.class).getProxyCandidate(initiator);
return Futures.transformAsync(
proxyFuture,
proxy -> {
@@ -301,62 +303,6 @@ public class SocksByteStreamsTransport implements Transport {
MoreExecutors.directExecutor());
}
- private ListenableFuture<Candidate> getProxyCandidate() {
- if (Config.DISABLE_PROXY_LOOKUP) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("Proxy look up is disabled"));
- }
- final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS);
- if (streamer == null) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("No proxy/streamer found"));
- }
- final Iq iqRequest = new Iq(Iq.Type.GET);
- iqRequest.setTo(streamer);
- iqRequest.query(Namespace.BYTE_STREAMS);
- final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
- xmppConnection.sendIqPacket(
- iqRequest,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final Element query = response.findChild("query", Namespace.BYTE_STREAMS);
- final Element streamHost =
- query == null
- ? null
- : query.findChild("streamhost", Namespace.BYTE_STREAMS);
- final String host =
- streamHost == null ? null : streamHost.getAttribute("host");
- final Integer port =
- Ints.tryParse(
- Strings.nullToEmpty(
- streamHost == null
- ? null
- : streamHost.getAttribute("port")));
- if (Strings.isNullOrEmpty(host) || port == null) {
- candidateFuture.setException(
- new IOException("Proxy response is missing attributes"));
- return;
- }
- candidateFuture.set(
- new Candidate(
- UUID.randomUUID().toString(),
- host,
- streamer,
- port,
- 655360 + (initiator ? 0 : 15),
- CandidateType.PROXY));
-
- } else if (response.getType() == Iq.Type.TIMEOUT) {
- candidateFuture.setException(new TimeoutException());
- } else {
- candidateFuture.setException(
- new IOException(
- "received iq error in response to proxy discovery"));
- }
- });
- return candidateFuture;
- }
-
@Override
public OutputStream getOutputStream() throws IOException {
final var connection = this.connection;
@@ -0,0 +1,61 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class AbstractBookmarkManager extends AbstractManager {
+
+ protected final XmppConnectionService service;
+
+ protected AbstractBookmarkManager(
+ final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ // TODO rename to setBookmarks?
+ protected void processBookmarksInitial(final Map<Jid, Bookmark> bookmarks, final boolean pep) {
+ final var account = getAccount();
+ // TODO we can internalize this getBookmarkedJid
+ final Set<Jid> previousBookmarks = account.getBookmarkedJids();
+ for (final Bookmark bookmark : bookmarks.values()) {
+ previousBookmarks.remove(bookmark.getJid().asBareJid());
+ getManager(BookmarkManager.class).processModifiedBookmark(bookmark, pep);
+ }
+ if (pep) {
+ this.processDeletedBookmarks(previousBookmarks);
+ }
+ account.setBookmarks(bookmarks);
+ }
+
+ protected void processDeletedBookmarks(final Collection<Jid> bookmarks) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": "
+ + bookmarks.size()
+ + " bookmarks have been removed");
+ for (final Jid bookmark : bookmarks) {
+ processDeletedBookmark(bookmark);
+ }
+ }
+
+ protected void processDeletedBookmark(final Jid jid) {
+ final Conversation conversation = service.find(getAccount(), jid);
+ if (conversation == null) {
+ return;
+ }
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
+ this.service.archiveConversation(conversation, false);
+ }
+}
@@ -0,0 +1,941 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.heifwriter.AvifWriter;
+import androidx.heifwriter.HeifWriter;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
+import com.google.common.hash.Hashing;
+import com.google.common.hash.HashingOutputStream;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.Device;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.avatar.Data;
+import im.conversations.android.xmpp.model.avatar.Info;
+import im.conversations.android.xmpp.model.avatar.Metadata;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.upload.purpose.Profile;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class AvatarManager extends AbstractManager {
+
+ private static final Object RENAME_LOCK = new Object();
+
+ private static final List<String> SUPPORTED_CONTENT_TYPES;
+
+ private static final Ordering<Info> AVATAR_ORDERING =
+ new Ordering<>() {
+ @Override
+ public int compare(Info left, Info right) {
+ return ComparisonChain.start()
+ .compare(
+ right.getWidth() * right.getHeight(),
+ left.getWidth() * left.getHeight())
+ .compare(
+ ImageFormat.formatPriority(right.getType()),
+ ImageFormat.formatPriority(left.getType()))
+ .result();
+ }
+ };
+
+ static {
+ final ImmutableList.Builder<ImageFormat> builder = new ImmutableList.Builder<>();
+ builder.add(ImageFormat.JPEG, ImageFormat.PNG, ImageFormat.WEBP, ImageFormat.SVG);
+ if (Compatibility.twentyEight()) {
+ builder.add(ImageFormat.HEIF);
+ }
+ if (Compatibility.thirtyFour()) {
+ builder.add(ImageFormat.AVIF);
+ }
+ final var supportedFormats = builder.build();
+ SUPPORTED_CONTENT_TYPES =
+ ImmutableList.copyOf(
+ Collections2.transform(supportedFormats, ImageFormat::toContentType));
+ }
+
+ private static final Executor AVATAR_COMPRESSION_EXECUTOR =
+ MoreExecutors.newSequentialExecutor(Executors.newSingleThreadScheduledExecutor());
+
+ private final XmppConnectionService service;
+
+ public AvatarManager(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public ListenableFuture<NodeConfiguration.AccessModel> getPepAccessModel() {
+ final var nodeConfiguration =
+ getManager(PepManager.class).getNodeConfiguration(Namespace.AVATAR_DATA);
+ return Futures.transform(
+ nodeConfiguration,
+ data -> {
+ final var accessModel = data.getValue(NodeConfiguration.ACCESS_MODEL);
+ if (Strings.isNullOrEmpty(accessModel)) {
+ throw new IllegalStateException(
+ "Access model missing from node configuration");
+ }
+ return NodeConfiguration.AccessModel.valueOf(
+ CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, accessModel));
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<byte[]> fetch(final Jid address, final String itemId) {
+ final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class);
+ return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Info> fetchAndStoreWithFallback(
+ final Jid address, final Info picked, final Info fallback) {
+ Preconditions.checkArgument(fallback.getUrl() == null, "fallback avatar must be in-band");
+ final var url = picked.getUrl();
+ if (url != null) {
+ final var httpDownloadFuture = fetchAndStoreHttp(url, picked);
+ return Futures.catchingAsync(
+ httpDownloadFuture,
+ Exception.class,
+ ex -> {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not download avatar for "
+ + address
+ + " from "
+ + url,
+ ex);
+ return fetchAndStoreInBand(address, fallback);
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ return fetchAndStoreInBand(address, picked);
+ }
+ }
+
+ private ListenableFuture<Info> fetchAndStoreInBand(final Jid address, final Info avatar) {
+ final var future = fetch(address, avatar.getId());
+ return Futures.transformAsync(
+ future,
+ data -> {
+ final var actualHash = Hashing.sha1().hashBytes(data).toString();
+ if (!actualHash.equals(avatar.getId())) {
+ throw new IllegalStateException(
+ String.format("In-band avatar hash of %s did not match", address));
+ }
+
+ final var file = FileBackend.getAvatarFile(context, avatar.getId());
+ if (file.exists()) {
+ return Futures.immediateFuture(avatar);
+ }
+ return Futures.transform(
+ write(file, data), v -> avatar, MoreExecutors.directExecutor());
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> write(final File destination, byte[] bytes) {
+ return Futures.submit(
+ () -> {
+ final var randomFile =
+ new File(context.getCacheDir(), UUID.randomUUID().toString());
+ Files.write(bytes, randomFile);
+ if (moveAvatarIntoCache(randomFile, destination)) {
+ return null;
+ }
+ throw new IllegalStateException(
+ String.format(
+ "Could not move file to %s", destination.getAbsolutePath()));
+ },
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ private ListenableFuture<Info> fetchAndStoreHttp(final HttpUrl url, final Info avatar) {
+ final SettableFuture<Info> settableFuture = SettableFuture.create();
+ final OkHttpClient client =
+ service.getHttpConnectionManager().buildHttpClient(url, getAccount(), 30, false);
+ final var request = new Request.Builder().url(url).get().build();
+ client.newCall(request)
+ .enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ settableFuture.setException(e);
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ try {
+ write(avatar, response);
+ } catch (final Exception e) {
+ settableFuture.setException(e);
+ return;
+ }
+ settableFuture.set(avatar);
+ } else {
+ settableFuture.setException(
+ new IOException("HTTP call was not successful"));
+ }
+ }
+ });
+ return settableFuture;
+ }
+
+ private void write(final Info avatar, Response response) throws IOException {
+ final var body = response.body();
+ if (body == null) {
+ throw new IOException("Body was null");
+ }
+ final long bytes = avatar.getBytes();
+ final long actualBytes;
+ final var inputStream = ByteStreams.limit(body.byteStream(), avatar.getBytes());
+ final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString());
+ final String actualHash;
+ try (final var fileOutputStream = new FileOutputStream(randomFile);
+ var hashingOutputStream =
+ new HashingOutputStream(Hashing.sha1(), fileOutputStream)) {
+ actualBytes = ByteStreams.copy(inputStream, hashingOutputStream);
+ actualHash = hashingOutputStream.hash().toString();
+ }
+ if (actualBytes != bytes) {
+ throw new IllegalStateException("File size did not meet expected size");
+ }
+ if (!actualHash.equals(avatar.getId())) {
+ throw new IllegalStateException("File hash did not match");
+ }
+ final var avatarFile = FileBackend.getAvatarFile(context, avatar.getId());
+ if (moveAvatarIntoCache(randomFile, avatarFile)) {
+ return;
+ }
+ throw new IOException("Could not move avatar to avatar location");
+ }
+
+ private void setAvatarInfo(final Jid address, @NonNull final Info info) {
+ setAvatar(address, info.getId());
+ }
+
+ private void setAvatar(final Jid from, @Nullable final String id) {
+ Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + id);
+ if (from.isBareJid()) {
+ setAvatarContact(from, id);
+ } else {
+ setAvatarMucUser(from, id);
+ }
+ }
+
+ private void setAvatarContact(final Jid from, @Nullable final String id) {
+ final var account = getAccount();
+ if (account.getJid().asBareJid().equals(from)) {
+ if (account.setAvatar(id)) {
+ getDatabase().updateAccount(account);
+ service.notifyAccountAvatarHasChanged(account);
+ }
+ service.getAvatarService().clear(account);
+ service.updateConversationUi();
+ service.updateAccountUi();
+ } else {
+ final Contact contact = account.getRoster().getContact(from);
+ if (contact.setAvatar(id)) {
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
+ service.getAvatarService().clear(contact);
+
+ final var conversation = service.find(account, from);
+ if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
+ service.getAvatarService().clear(conversation.getMucOptions());
+ }
+
+ service.updateConversationUi();
+ service.updateRosterUi(XmppConnectionService.UpdateRosterReason.AVATAR);
+ }
+ }
+ }
+
+ private void setAvatarMucUser(final Jid from, final String id) {
+ final var account = getAccount();
+ final Conversation conversation = service.find(account, from.asBareJid());
+ if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) {
+ return;
+ }
+ final var user = conversation.getMucOptions().findUserByFullJid(from);
+ if (user == null) {
+ return;
+ }
+ if (user.setAvatar(id)) {
+ service.getAvatarService().clear(user);
+ service.updateConversationUi();
+ service.updateMucRosterUi();
+ }
+ }
+
+ public void handleItems(final Jid from, final Items items) {
+ final var account = getAccount();
+ // TODO support retract
+ final var entry = items.getFirstItemWithId(Metadata.class);
+ if (entry == null) {
+ return;
+ }
+ final var avatar = getPreferredFallback(entry);
+ if (avatar == null) {
+ return;
+ }
+
+ Log.d(Config.LOGTAG, "picked avatar from " + from + ": " + avatar.preferred);
+
+ final var cache = FileBackend.getAvatarFile(context, avatar.preferred.getId());
+
+ if (cache.exists()) {
+ setAvatarInfo(from, avatar.preferred);
+ } else if (service.isDataSaverDisabled()) {
+ final var contact = getManager(RosterManager.class).getContactFromContactList(from);
+ final ListenableFuture<Info> future;
+ if (contact != null && contact.showInContactList()) {
+ future = this.fetchAndStoreWithFallback(from, avatar.preferred, avatar.fallback);
+ } else {
+ future = fetchAndStoreInBand(from, avatar.fallback);
+ }
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Info result) {
+ setAvatarInfo(from, result);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": successfully fetched pep avatar for "
+ + from);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not fetch avatar", t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ }
+
+ public void handleVCardUpdate(final Jid address, final VCardUpdate vCardUpdate) {
+ final var hash = vCardUpdate.getHash();
+ if (hash == null) {
+ return;
+ }
+ handleVCardUpdate(address, hash);
+ }
+
+ public void handleVCardUpdate(final Jid address, final String hash) {
+ Preconditions.checkArgument(VCardUpdate.isValidSHA1(hash));
+ final var avatarFile = FileBackend.getAvatarFile(context, hash);
+ if (avatarFile.exists()) {
+ setAvatar(address, hash);
+ } else if (service.isDataSaverDisabled()) {
+ final var future = this.fetchAndStoreVCard(address, hash);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, "successfully fetch vCard avatar for " + address);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not fetch avatar for " + address, t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+ }
+
+ private PreferredFallback getPreferredFallback(final Map.Entry<String, Metadata> entry) {
+ final var mainItemId = entry.getKey();
+ final var infos = entry.getValue().getInfos();
+
+ final var inBandAvatar = Iterables.find(infos, i -> mainItemId.equals(i.getId()), null);
+
+ if (inBandAvatar == null || inBandAvatar.getUrl() != null) {
+ return null;
+ }
+
+ final var optionalAutoAcceptSize = new AppSettings(context).getAutoAcceptFileSize();
+ if (optionalAutoAcceptSize.isEmpty()) {
+ return new PreferredFallback(inBandAvatar);
+ } else {
+
+ final var supported =
+ Collections2.filter(
+ infos,
+ i ->
+ Objects.nonNull(i.getId())
+ && i.getBytes() > 0
+ && i.getHeight() > 0
+ && i.getWidth() > 0
+ && SUPPORTED_CONTENT_TYPES.contains(i.getType()));
+
+ final var autoAcceptSize = optionalAutoAcceptSize.get();
+
+ final var supportedBelowLimit =
+ Collections2.filter(supported, i -> i.getBytes() <= autoAcceptSize);
+
+ if (supportedBelowLimit.isEmpty()) {
+ return new PreferredFallback(inBandAvatar);
+ } else {
+ final var preferred =
+ Iterables.getFirst(AVATAR_ORDERING.sortedCopy(supportedBelowLimit), null);
+ return new PreferredFallback(preferred, inBandAvatar);
+ }
+ }
+ }
+
+ public void handleDelete(final Jid from) {
+ Preconditions.checkArgument(
+ from.isBareJid(), "node deletion can only be triggered from bare JIDs");
+ setAvatar(from, null);
+ }
+
+ private Info resizeAndStoreAvatar(
+ final Uri image, final int size, final ImageFormat format, final Integer charLimit)
+ throws Exception {
+ final var centerSquare = context.getFileBackend().cropCenterSquare(image, size);
+ final var info = resizeAndStoreAvatar(centerSquare, format, charLimit);
+ centerSquare.recycle();
+ return info;
+ }
+
+ private Info resizeAndStoreAvatar(
+ final Bitmap centerSquare, final ImageFormat format, final Integer charLimit)
+ throws Exception {
+ if (charLimit == null || format == ImageFormat.PNG) {
+ return resizeAndStoreAvatar(centerSquare, format, 90);
+ } else {
+ Info avatar = null;
+ for (int quality = 90; quality >= 50; quality = quality - 2) {
+ if (avatar != null) {
+ FileBackend.getAvatarFile(context, avatar.getId()).delete();
+ }
+ Log.d(Config.LOGTAG, "trying to save thumbnail with quality " + quality);
+ avatar = resizeAndStoreAvatar(centerSquare, format, quality);
+ if (avatar.getBytes() <= charLimit) {
+ return avatar;
+ }
+ }
+ return avatar;
+ }
+ }
+
+ private Info resizeAndStoreAvatar(final Bitmap image, ImageFormat format, final int quality)
+ throws Exception {
+ return switch (format) {
+ case PNG -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.PNG, quality);
+ case JPEG -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.JPEG, quality);
+ case WEBP -> resizeAndStoreAvatar(image, Bitmap.CompressFormat.WEBP, quality);
+ case HEIF -> resizeAndStoreAvatarAsHeif(image, quality);
+ case AVIF -> resizeAndStoreAvatarAsAvif(image, quality);
+ case SVG -> throw new RuntimeException("SVG cannot be a Bitmap?");
+ };
+ }
+
+ private Info resizeAndStoreAvatar(
+ final Bitmap image, final Bitmap.CompressFormat format, final int quality)
+ throws IOException {
+ final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString());
+ final var fileOutputStream = new FileOutputStream(randomFile);
+ final var hashingOutputStream = new HashingOutputStream(Hashing.sha1(), fileOutputStream);
+ image.compress(format, quality, hashingOutputStream);
+ hashingOutputStream.close();
+ final var sha1 = hashingOutputStream.hash().toString();
+ final var avatarFile = FileBackend.getAvatarFile(context, sha1);
+ if (moveAvatarIntoCache(randomFile, avatarFile)) {
+ return new Info(
+ sha1,
+ avatarFile.length(),
+ ImageFormat.of(format).toContentType(),
+ image.getWidth(),
+ image.getHeight());
+ }
+ throw new IllegalStateException(
+ String.format("Could not move file to %s", avatarFile.getAbsolutePath()));
+ }
+
+ private Info resizeAndStoreAvatarAsHeif(final Bitmap image, final int quality)
+ throws Exception {
+ final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString());
+ try (final var fileOutputStream = new FileOutputStream(randomFile);
+ final var heifWriter =
+ new HeifWriter.Builder(
+ fileOutputStream.getFD(),
+ image.getWidth(),
+ image.getHeight(),
+ HeifWriter.INPUT_MODE_BITMAP)
+ .setMaxImages(1)
+ .setQuality(quality)
+ .build()) {
+
+ heifWriter.start();
+ heifWriter.addBitmap(image);
+ heifWriter.stop(3_000);
+ }
+ final var width = image.getWidth();
+ final var height = image.getHeight();
+ checkDecoding(randomFile, ImageFormat.HEIF, width, height);
+ return storeAsAvatar(randomFile, ImageFormat.HEIF, width, height);
+ }
+
+ private Info resizeAndStoreAvatarAsAvif(final Bitmap image, final int quality)
+ throws Exception {
+ final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString());
+ try (final var fileOutputStream = new FileOutputStream(randomFile);
+ final var avifWriter =
+ new AvifWriter.Builder(
+ fileOutputStream.getFD(),
+ image.getWidth(),
+ image.getHeight(),
+ AvifWriter.INPUT_MODE_BITMAP)
+ .setMaxImages(1)
+ .setQuality(quality)
+ .build()) {
+ avifWriter.start();
+ avifWriter.addBitmap(image);
+ avifWriter.stop(3_000);
+ }
+ final var width = image.getWidth();
+ final var height = image.getHeight();
+ checkDecoding(randomFile, ImageFormat.AVIF, width, height);
+ return storeAsAvatar(randomFile, ImageFormat.AVIF, width, height);
+ }
+
+ private void checkDecoding(
+ final File randomFile, final ImageFormat format, final int width, final int height) {
+ var readCheck = BitmapFactory.decodeFile(randomFile.getAbsolutePath());
+ if (readCheck == null) {
+ throw new ImageCompressionException(
+ String.format("%s image was null after trying to decode", format));
+ }
+ if (readCheck.getWidth() != width || readCheck.getHeight() != height) {
+ readCheck.recycle();
+ throw new ImageCompressionException(String.format("%s had wrong image bounds", format));
+ }
+ readCheck.recycle();
+ }
+
+ private Info storeAsAvatar(
+ final File randomFile, final ImageFormat type, final int width, final int height)
+ throws IOException {
+ final var sha1 = Files.asByteSource(randomFile).hash(Hashing.sha1()).toString();
+ final var avatarFile = FileBackend.getAvatarFile(context, sha1);
+ if (moveAvatarIntoCache(randomFile, avatarFile)) {
+ return new Info(sha1, avatarFile.length(), type.toContentType(), width, height);
+ }
+ throw new IllegalStateException(
+ String.format("Could not move file to %s", avatarFile.getAbsolutePath()));
+ }
+
+ private ListenableFuture<Collection<Info>> uploadAvatar(final Uri image) {
+ return Futures.transformAsync(
+ hasAlphaChannel(image),
+ hasAlphaChannel -> uploadAvatar(image, hasAlphaChannel),
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Collection<Info>> uploadAvatar(
+ final Uri image, final boolean hasAlphaChannel) {
+ final var avatarFutures = new ImmutableList.Builder<ListenableFuture<Info>>();
+
+ final ListenableFuture<Info> avatarThumbnailFuture;
+ if (hasAlphaChannel) {
+ avatarThumbnailFuture =
+ resizeAndStoreAvatarAsync(
+ image, Config.AVATAR_THUMBNAIL_SIZE / 2, ImageFormat.PNG);
+ } else {
+ avatarThumbnailFuture =
+ resizeAndStoreAvatarAsync(
+ image,
+ Config.AVATAR_THUMBNAIL_SIZE,
+ ImageFormat.JPEG,
+ Config.AVATAR_THUMBNAIL_CHAR_LIMIT);
+ }
+
+ final var uploadManager = getManager(HttpUploadManager.class);
+
+ final var uploadService = uploadManager.getService();
+ if (uploadService == null || !uploadService.supportsPurpose(Profile.class)) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid() + ": 'profile' upload purpose not supported");
+ return Futures.transform(
+ avatarThumbnailFuture, ImmutableList::of, MoreExecutors.directExecutor());
+ }
+
+ final ListenableFuture<Info> avatarFuture;
+ if (hasAlphaChannel) {
+ avatarFuture =
+ resizeAndStoreAvatarAsync(image, Config.AVATAR_FULL_SIZE / 2, ImageFormat.PNG);
+ } else {
+ final int autoAcceptFileSize =
+ context.getResources().getInteger(R.integer.auto_accept_filesize);
+ avatarFuture =
+ resizeAndStoreAvatarAsync(
+ image, Config.AVATAR_FULL_SIZE, ImageFormat.JPEG, autoAcceptFileSize);
+
+ final var device = new Device(context);
+
+ if (Compatibility.twentyEight() && device.isPhysicalDevice()) {
+ final var avatarHeifFuture =
+ resizeAndStoreAvatarAsync(
+ image,
+ Config.AVATAR_FULL_SIZE,
+ ImageFormat.HEIF,
+ autoAcceptFileSize);
+ final var avatarHeifWithUrlFuture =
+ Futures.transformAsync(
+ avatarHeifFuture, this::upload, MoreExecutors.directExecutor());
+ avatarFutures.add(avatarHeifWithUrlFuture);
+ }
+ if (Compatibility.thirtyFour() && device.isPhysicalDevice()) {
+ final var avatarAvifFuture =
+ resizeAndStoreAvatarAsync(
+ image,
+ Config.AVATAR_FULL_SIZE,
+ ImageFormat.AVIF,
+ autoAcceptFileSize);
+ final var avatarAvifWithUrlFuture =
+ Futures.transformAsync(
+ avatarAvifFuture, this::upload, MoreExecutors.directExecutor());
+ final var caughtAvifWithUrlFuture =
+ Futures.catching(
+ avatarAvifWithUrlFuture,
+ Exception.class,
+ ex -> {
+ Log.d(Config.LOGTAG, "ignoring AVIF compression failure", ex);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ avatarFutures.add(caughtAvifWithUrlFuture);
+ }
+ }
+ avatarFutures.add(avatarThumbnailFuture);
+ final var avatarWithUrlFuture =
+ Futures.transformAsync(avatarFuture, this::upload, MoreExecutors.directExecutor());
+ avatarFutures.add(avatarWithUrlFuture);
+
+ final var all = Futures.allAsList(avatarFutures.build());
+ return Futures.transform(
+ all,
+ input -> Collections2.filter(input, Objects::nonNull),
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Boolean> hasAlphaChannel(final Uri image) {
+ return Futures.submit(
+ () -> {
+ final var cropped =
+ context.getFileBackend().cropCenterSquare(image, Config.AVATAR_FULL_SIZE);
+ final var hasAlphaChannel = FileBackend.hasAlpha(cropped);
+ cropped.recycle();
+ return hasAlphaChannel;
+ },
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ private ListenableFuture<Info> upload(final Info avatar) {
+ final var file = FileBackend.getAvatarFile(context, avatar.getId());
+ final var urlFuture =
+ getManager(HttpUploadManager.class).upload(file, avatar.getType(), new Profile());
+ return Futures.transform(
+ urlFuture,
+ url -> {
+ avatar.setUrl(url);
+ return avatar;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Info> resizeAndStoreAvatarAsync(
+ final Uri image, final int size, final ImageFormat format) {
+ return resizeAndStoreAvatarAsync(image, size, format, null);
+ }
+
+ private ListenableFuture<Info> resizeAndStoreAvatarAsync(
+ final Uri image, final int size, final ImageFormat format, final Integer charLimit) {
+ return Futures.submit(
+ () -> resizeAndStoreAvatar(image, size, format, charLimit),
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ private ListenableFuture<Void> publish(final Collection<Info> avatars, final boolean open) {
+ final Info mainAvatarInfo;
+ final byte[] mainAvatar;
+ try {
+ mainAvatarInfo = Iterables.find(avatars, a -> Objects.isNull(a.getUrl()));
+ mainAvatar =
+ Files.asByteSource(FileBackend.getAvatarFile(context, mainAvatarInfo.getId()))
+ .read();
+ } catch (final IOException | NoSuchElementException e) {
+ return Futures.immediateFailedFuture(e);
+ }
+ final NodeConfiguration configuration =
+ open ? NodeConfiguration.OPEN : NodeConfiguration.PRESENCE;
+ final var avatarData = new Data();
+ avatarData.setContent(mainAvatar);
+ final var future =
+ getManager(PepManager.class)
+ .publish(avatarData, mainAvatarInfo.getId(), configuration);
+ return Futures.transformAsync(
+ future,
+ v -> {
+ final var id = mainAvatarInfo.getId();
+ final var metadata = new Metadata();
+ metadata.addExtensions(avatars);
+ return getManager(PepManager.class).publish(metadata, id, configuration);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> publishVCard(final Jid address, final Uri image) {
+
+ ListenableFuture<Info> avatarThumbnailFuture =
+ Futures.transformAsync(
+ hasAlphaChannel(image),
+ hasAlphaChannel -> {
+ if (hasAlphaChannel) {
+ return resizeAndStoreAvatarAsync(
+ image, Config.AVATAR_THUMBNAIL_SIZE / 2, ImageFormat.PNG);
+ } else {
+ return resizeAndStoreAvatarAsync(
+ image,
+ Config.AVATAR_THUMBNAIL_SIZE,
+ ImageFormat.JPEG,
+ Config.AVATAR_THUMBNAIL_CHAR_LIMIT);
+ }
+ },
+ MoreExecutors.directExecutor());
+ return Futures.transformAsync(
+ avatarThumbnailFuture,
+ info -> {
+ final var avatar =
+ Files.asByteSource(FileBackend.getAvatarFile(context, info.getId()))
+ .read();
+ return getManager(VCardManager.class)
+ .publishPhoto(address, info.getType(), avatar);
+ },
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ public ListenableFuture<Void> uploadAndPublish(final Uri image, final boolean open) {
+ final var infoFuture = uploadAvatar(image);
+ return Futures.transformAsync(
+ infoFuture, avatars -> publish(avatars, open), MoreExecutors.directExecutor());
+ }
+
+ public boolean hasPepToVCardConversion() {
+ return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION);
+ }
+
+ public ListenableFuture<Void> delete() {
+ final var pepManager = getManager(PepManager.class);
+ final var deleteMetaDataFuture = pepManager.delete(Namespace.AVATAR_METADATA);
+ final var deleteDataFuture = pepManager.delete(Namespace.AVATAR_DATA);
+ return Futures.transform(
+ Futures.allAsList(deleteDataFuture, deleteMetaDataFuture),
+ vs -> null,
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> fetchAndStore(final Jid address) {
+ final var metaDataFuture =
+ getManager(PubSubManager.class).fetchItems(address, Metadata.class);
+ return Futures.transformAsync(
+ metaDataFuture,
+ metaData -> {
+ final var entry = Iterables.getFirst(metaData.entrySet(), null);
+ if (entry == null) {
+ throw new IllegalStateException("Metadata item not found");
+ }
+ final var avatar = getPreferredFallback(entry);
+
+ if (avatar == null) {
+ throw new IllegalStateException("No avatar found");
+ }
+
+ final var cache = FileBackend.getAvatarFile(context, avatar.preferred.getId());
+
+ if (cache.exists()) {
+ Log.d(
+ Config.LOGTAG,
+ "fetchAndStore. file existed " + cache.getAbsolutePath());
+ setAvatarInfo(address, avatar.preferred);
+ return Futures.immediateVoidFuture();
+ } else {
+ final var future =
+ this.fetchAndStoreWithFallback(
+ address, avatar.preferred, avatar.fallback);
+ return Futures.transform(
+ future,
+ info -> {
+ setAvatarInfo(address, info);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private static boolean moveAvatarIntoCache(final File randomFile, final File destination) {
+ synchronized (RENAME_LOCK) {
+ if (destination.exists()) {
+ return true;
+ }
+ final var directory = destination.getParentFile();
+ if (directory != null && directory.mkdirs()) {
+ Log.d(
+ Config.LOGTAG,
+ "create avatar cache directory: " + directory.getAbsolutePath());
+ }
+ return randomFile.renameTo(destination);
+ }
+ }
+
+ public ListenableFuture<Void> fetchAndStoreVCard(final Jid address, final String expectedHash) {
+ final var future =
+ connection.getManager(VCardManager.class).retrievePhotoCacheException(address);
+ return Futures.transformAsync(
+ future,
+ photo -> {
+ final var actualHash = Hashing.sha1().hashBytes(photo).toString();
+ if (!actualHash.equals(expectedHash)) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException(
+ String.format(
+ "Hash in vCard update for %s did not match",
+ address)));
+ }
+ final var avatarFile = FileBackend.getAvatarFile(context, actualHash);
+ if (avatarFile.exists()) {
+ setAvatar(address, actualHash);
+ return Futures.immediateVoidFuture();
+ }
+ final var writeFuture = write(avatarFile, photo);
+ return Futures.transform(
+ writeFuture,
+ v -> {
+ setAvatar(address, actualHash);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ },
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ private static final class ImageCompressionException extends IllegalStateException {
+ ImageCompressionException(final String message) {
+ super(message);
+ }
+ }
+
+ public enum ImageFormat {
+ PNG,
+ JPEG,
+ WEBP,
+ HEIF,
+ AVIF,
+ SVG;
+
+ public String toContentType() {
+ return switch (this) {
+ case WEBP -> "image/webp";
+ case PNG -> "image/png";
+ case JPEG -> "image/jpeg";
+ case AVIF -> "image/avif";
+ case HEIF -> "image/heif";
+ case SVG -> "image/svg+xml";
+ };
+ }
+
+ public static int formatPriority(final String type) {
+ final var format = ofContentType(type);
+ return format == null ? Integer.MIN_VALUE : format.ordinal();
+ }
+
+ private static ImageFormat ofContentType(final String type) {
+ return switch (type) {
+ case "image/png" -> PNG;
+ case "image/jpeg" -> JPEG;
+ case "image/webp" -> WEBP;
+ case "image/heif" -> HEIF;
+ case "image/avif" -> AVIF;
+ case "image/svg+xml" -> SVG;
+ default -> null;
+ };
+ }
+
+ public static ImageFormat of(final Bitmap.CompressFormat compressFormat) {
+ return switch (compressFormat) {
+ case PNG -> PNG;
+ case WEBP -> WEBP;
+ case JPEG -> JPEG;
+ default -> throw new AssertionError("Not implemented");
+ };
+ }
+ }
+
+ private static final class PreferredFallback {
+ private final Info preferred;
+ private final Info fallback;
+
+ private PreferredFallback(final Info fallback) {
+ this(fallback, fallback);
+ }
+
+ private PreferredFallback(Info preferred, Info fallback) {
+ this.preferred = preferred;
+ this.fallback = fallback;
+ }
+ }
+}
@@ -0,0 +1,39 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.axolotl.DeviceList;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import java.util.HashSet;
+import java.util.Set;
+
+public class AxolotlManager extends AbstractManager {
+
+ public AxolotlManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void handleItems(final Jid from, final Items items) {
+ final var account = getAccount();
+ final var deviceList = items.getFirstItem(DeviceList.class);
+ if (deviceList == null) {
+ return;
+ }
+ final Set<Integer> deviceIds = deviceList.getDeviceIds();
+ Log.d(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Received PEP device list "
+ + deviceIds
+ + " update from "
+ + from
+ + ", processing... ");
+ final AxolotlService axolotlService = account.getAxolotlService();
+ axolotlService.registerDevices(from, new HashSet<>(deviceIds));
+ }
+}
@@ -0,0 +1,229 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Blockable;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.blocking.Block;
+import im.conversations.android.xmpp.model.blocking.Blocklist;
+import im.conversations.android.xmpp.model.blocking.Item;
+import im.conversations.android.xmpp.model.blocking.Unblock;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.reporting.Report;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.unique.StanzaId;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class BlockingManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ private final HashSet<Jid> blocklist = new HashSet<>();
+
+ public BlockingManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ // TODO find a way to get rid of XmppConnectionService and use context instead
+ this.service = service;
+ }
+
+ public void request() {
+ final var future = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Blocklist()));
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ final var blocklist = result.getExtension(Blocklist.class);
+ if (blocklist == null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": invalid blocklist response");
+ return;
+ }
+ final var addresses = itemsAsAddresses(blocklist.getItems());
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": discovered blocklist with "
+ + addresses.size()
+ + " items");
+ setBlocklist(addresses);
+ removeBlockedConversations(addresses);
+ service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.w(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not retrieve blocklist",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void pushBlock(final Iq request) {
+ if (connection.fromServer(request)) {
+ final var block = request.getExtension(Block.class);
+ final var addresses = itemsAsAddresses(block.getItems());
+ synchronized (this.blocklist) {
+ this.blocklist.addAll(addresses);
+ }
+ this.removeBlockedConversations(addresses);
+ this.service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ this.connection.sendResultFor(request);
+ } else {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ public void pushUnblock(final Iq request) {
+ if (connection.fromServer(request)) {
+ final var unblock = request.getExtension(Unblock.class);
+ final var address = itemsAsAddresses(unblock.getItems());
+ synchronized (this.blocklist) {
+ this.blocklist.removeAll(address);
+ }
+ this.service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ this.connection.sendResultFor(request);
+ } else {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ private void removeBlockedConversations(final Collection<Jid> addresses) {
+ var removed = false;
+ for (final Jid address : addresses) {
+ removed |= service.removeBlockedConversations(getAccount(), address);
+ }
+ if (removed) {
+ service.updateConversationUi();
+ }
+ }
+
+ public ImmutableSet<Jid> getBlocklist() {
+ synchronized (this.blocklist) {
+ return ImmutableSet.copyOf(this.blocklist);
+ }
+ }
+
+ private void setBlocklist(final Collection<Jid> addresses) {
+ synchronized (this.blocklist) {
+ this.blocklist.clear();
+ this.blocklist.addAll(addresses);
+ }
+ }
+
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class).hasServerFeature(Namespace.BLOCKING);
+ }
+
+ private static Set<Jid> itemsAsAddresses(final Collection<Item> items) {
+ final var builder = new ImmutableSet.Builder<Jid>();
+ for (final var item : items) {
+ final var jid = Jid.Invalid.getNullForInvalid(item.getJid());
+ if (jid == null) {
+ continue;
+ }
+ builder.add(jid);
+ }
+ return builder.build();
+ }
+
+ public boolean block(
+ @NonNull final Blockable blockable,
+ final boolean reportSpam,
+ @Nullable final String serverMsgId) {
+ final var address = blockable.getBlockedJid();
+ final var iq = new Iq(Iq.Type.SET);
+ final var block = iq.addExtension(new Block());
+ final var item = block.addExtension(new Item());
+ item.setJid(address);
+ if (reportSpam) {
+ final var report = item.addExtension(new Report());
+ report.setReason(Namespace.REPORTING_REASON_SPAM);
+ if (serverMsgId != null) {
+ // XEP has a 'by' attribute that is the same as reported jid but that doesn't make
+ // sense this the 'by' attribute in the stanza-id refers to the arriving entity
+ // (usually the account or the MUC)
+ report.addExtension(new StanzaId(serverMsgId));
+ }
+ }
+ final var future = this.connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Iq result) {
+ synchronized (blocklist) {
+ blocklist.add(address);
+ }
+ service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": could not block " + address,
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ if (address.isFullJid()) {
+ return false;
+ } else if (service.removeBlockedConversations(getAccount(), address)) {
+ service.updateConversationUi();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void unblock(@NonNull final Blockable blockable) {
+ final var address = blockable.getBlockedJid();
+ final var iq = new Iq(Iq.Type.SET);
+ final var unblock = iq.addExtension(new Unblock());
+ final var item = unblock.addExtension(new Item());
+ item.setJid(address);
+ final var future = this.connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Iq>() {
+ @Override
+ public void onSuccess(Iq result) {
+ synchronized (blocklist) {
+ blocklist.remove(address);
+ }
+ service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not unblock "
+ + address,
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,176 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+public class BookmarkManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public BookmarkManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public void request() {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ getManager(NativeBookmarkManager.class).fetch();
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid() + ": not fetching bookmarks. waiting for server to push");
+ } else {
+ getManager(PrivateStorageManager.class).fetchBookmarks();
+ }
+ }
+
+ public void save(final Conversation conversation, final String name) {
+ final Account account = conversation.getAccount();
+ final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
+ final String nick = conversation.getJid().getResource();
+ if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
+ bookmark.setNick(nick);
+ }
+ if (!TextUtils.isEmpty(name)) {
+ bookmark.setBookmarkName(name);
+ }
+ bookmark.setAutojoin(true);
+ this.create(bookmark);
+ bookmark.setConversation(conversation);
+ }
+
+ public void create(final Bookmark bookmark) {
+ final var account = getAccount();
+ account.putBookmark(bookmark);
+ final ListenableFuture<Void> future;
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ future = getManager(NativeBookmarkManager.class).publish(bookmark);
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks());
+ } else {
+ future =
+ getManager(PrivateStorageManager.class)
+ .publishBookmarks(account.getBookmarks());
+ }
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": created bookmark");
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not create bookmark",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void delete(final Bookmark bookmark) {
+ final var account = getAccount();
+ account.removeBookmark(bookmark);
+ final ListenableFuture<Void> future;
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ future = getManager(NativeBookmarkManager.class).retract(bookmark.getJid().asBareJid());
+ } else if (getManager(LegacyBookmarkManager.class).hasConversion()) {
+ future = getManager(LegacyBookmarkManager.class).publish(account.getBookmarks());
+ } else {
+ future =
+ getManager(PrivateStorageManager.class)
+ .publishBookmarks(account.getBookmarks());
+ }
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark");
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not delete bookmark",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
+ final var account = getAccount();
+ final var existingBookmark = conversation.getBookmark();
+ if (existingBookmark == null) {
+ final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
+ bookmark.setAutojoin(true);
+ create(bookmark);
+ } else {
+ if (existingBookmark.autojoin()) {
+ return;
+ }
+ existingBookmark.setAutojoin(true);
+ create(existingBookmark);
+ }
+ }
+
+ public void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
+ final var existing = this.service.find(bookmark);
+ if (existing != null) {
+ if (existing.getMode() != Conversation.MODE_MULTI) {
+ return;
+ }
+ bookmark.setConversation(existing);
+ if (pep && !bookmark.autojoin()) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": archiving conference ("
+ + existing.getJid()
+ + ") after receiving pep");
+ service.archiveConversation(existing, false);
+ } else {
+ final MucOptions mucOptions = existing.getMucOptions();
+ if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
+ final String current = mucOptions.getActualNick();
+ final String proposed = mucOptions.getProposedNickPure();
+ if (current != null && !current.equals(proposed)) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": proposed nick changed after bookmark push "
+ + current
+ + "->"
+ + proposed);
+ getManager(MultiUserChatManager.class).join(existing);
+ }
+ } else {
+ getManager(MultiUserChatManager.class).checkMucRequiresRename(existing);
+ }
+ }
+ } else if (bookmark.autojoin()) {
+ final var fresh =
+ this.service.findOrCreateConversation(
+ getAccount(), bookmark.getFullJid(), true, true, false);
+ bookmark.setConversation(fresh);
+ }
+ }
+}
@@ -7,7 +7,10 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -57,6 +60,8 @@ public class DiscoManager extends AbstractManager {
Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
"http://jabber.org/protocol/muc",
"jabber:x:conference",
+ "http://jabber.org/protocol/xhtml-im",
+ "urn:xmpp:bob",
Namespace.OOB,
Namespace.ENTITY_CAPABILITIES,
Namespace.ENTITY_CAPABILITIES_2,
@@ -73,7 +78,7 @@ public class DiscoManager extends AbstractManager {
Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION);
private final List<String> PRIVACY_SENSITIVE =
Collections.singletonList(
- "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
+ Namespace.TIME // XEP-0202: Entity Time leaks time zone
);
private final List<String> VOIP_NAMESPACES =
Arrays.asList(
@@ -91,6 +96,7 @@ public class DiscoManager extends AbstractManager {
private final Map<Jid, InfoQuery> entityInformation = new HashMap<>();
private final Map<Jid, ImmutableSet<Jid>> discoItems = new HashMap<>();
+ private final Map<String, Jid> commands = new HashMap<>();
public DiscoManager(XmppConnectionService context, XmppConnection connection) {
super(context, connection);
@@ -313,8 +319,7 @@ public class DiscoManager extends AbstractManager {
final var appSettings = new AppSettings(context);
final var account = connection.getAccount();
final ImmutableList.Builder<String> features = ImmutableList.builder();
- features.add("http://jabber.org/protocol/xhtml-im");
- features.add("urn:xmpp:bob");
+ features.addAll(STATIC_FEATURES);
if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
features.add(Namespace.MDS_DISPLAYED + "+notify");
}
@@ -335,7 +340,7 @@ public class DiscoManager extends AbstractManager {
if (appSettings.isBroadcastLastActivity()) {
features.add(Namespace.IDLE);
}
- if (connection.getFeatures().bookmarks2()) {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
features.add(Namespace.BOOKMARKS2 + "+notify");
} else {
features.add(Namespace.BOOKMARKS + "+notify");
@@ -443,6 +448,11 @@ public class DiscoManager extends AbstractManager {
return infoQuery != null && infoQuery.hasFeature(feature);
}
+ public boolean hasAccountFeature(final String feature) {
+ final var infoQuery = this.get(getAccount().getJid().asBareJid());
+ return infoQuery != null && infoQuery.hasFeature(feature);
+ }
+
private void put(final Jid address, final InfoQuery infoQuery) {
synchronized (this.entityInformation) {
this.entityInformation.put(address, infoQuery);
@@ -461,10 +471,19 @@ public class DiscoManager extends AbstractManager {
}
}
+ public Jid getAddressForCommand(final String node) {
+ synchronized (this.commands) {
+ return this.commands.get(node);
+ }
+ }
+
public void clear() {
synchronized (this.entityInformation) {
this.entityInformation.clear();
}
+ synchronized (this.commands) {
+ this.commands.clear();
+ }
}
public void clear(final Jid address) {
@@ -483,6 +502,43 @@ public class DiscoManager extends AbstractManager {
}
}
+ public Map<Jid, InfoQuery> findDiscoItemsByFeature(final String feature) {
+ return Maps.filterValues(getServerItems(), v -> v.hasFeature(feature));
+ }
+
+ public Map.Entry<Jid, InfoQuery> findDiscoItemByFeature(final String feature) {
+ final var items = findDiscoItemsByFeature(feature);
+ return Iterables.getFirst(items.entrySet(), null);
+ }
+
+ public boolean hasServerCommands() {
+ return hasServerFeature(Namespace.COMMANDS);
+ }
+
+ public void fetchServerCommands() {
+ final var future = commands(Entity.discoItem(getAccount().getDomain()));
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Map<String, Jid> result) {
+ synchronized (commands) {
+ commands.clear();
+ commands.putAll(result);
+ }
+ }
+
+ @Override
+ public void onFailure(@androidx.annotation.NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": could not fetch commands",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
public static final class CapsHashMismatchException extends IllegalStateException {
public CapsHashMismatchException(final String message) {
super(message);
@@ -0,0 +1,44 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.generator.AbstractGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.time.Time;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class EntityTimeManager extends AbstractManager {
+
+ public EntityTimeManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void request(final Iq request) {
+ final var appSettings = new AppSettings(this.context);
+ if (appSettings.isUseTor() || getAccount().isOnion()) {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ return;
+ }
+ final var time = new Time();
+ final long now = System.currentTimeMillis();
+ time.setUniversalTime(AbstractGenerator.getTimestamp(now));
+ final TimeZone ourTimezone = TimeZone.getDefault();
+ final long offsetSeconds = ourTimezone.getOffset(now) / 1000;
+ final long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
+ final long offsetHours = offsetSeconds / 3600;
+ final String hours;
+ if (offsetHours < 0) {
+ hours = String.format(Locale.US, "%03d", offsetHours);
+ } else {
+ hours = String.format(Locale.US, "%02d", offsetHours);
+ }
+ String minutes = String.format(Locale.US, "%02d", offsetMinutes);
+ time.setTimeZoneOffset(hours + ":" + minutes);
+ this.connection.sendResultFor(request, time);
+ }
+}
@@ -0,0 +1,262 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Base64;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Longs;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.ExtensionFactory;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.upload.Request;
+import im.conversations.android.xmpp.model.upload.purpose.Purpose;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class HttpUploadManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public HttpUploadManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public ListenableFuture<Slot> request(final DownloadableFile file, final String mime) {
+ return request(file.getName(), mime, file.getExpectedSize(), null);
+ }
+
+ public ListenableFuture<Slot> request(
+ final String filename,
+ final String mime,
+ final long size,
+ @Nullable final Purpose purpose) {
+ final var result =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
+ if (result == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("No HTTP upload host found"));
+ }
+ return requestHttpUpload(result.getKey(), filename, mime, size, purpose);
+ }
+
+ public ListenableFuture<HttpUrl> upload(
+ final File file, final String mime, final Purpose purpose) {
+ final var filename = file.getName();
+ final var size = file.length();
+ final var slotFuture = request(filename, mime, size, purpose);
+ return Futures.transformAsync(
+ slotFuture, slot -> upload(file, mime, slot), MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<HttpUrl> upload(final File file, final String mime, final Slot slot) {
+ final SettableFuture<HttpUrl> future = SettableFuture.create();
+ final OkHttpClient client =
+ service.getHttpConnectionManager()
+ .buildHttpClient(slot.put, getAccount(), 0, false);
+ final var body = RequestBody.create(MediaType.parse(mime), file);
+ final okhttp3.Request request =
+ new okhttp3.Request.Builder().url(slot.put).put(body).headers(slot.headers).build();
+ client.newCall(request)
+ .enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ future.setException(e);
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ future.set(slot.get);
+ } else {
+ future.setException(
+ new IllegalStateException(
+ String.format(
+ "Response code was %s",
+ response.code())));
+ }
+ }
+ });
+ return future;
+ }
+
+ private ListenableFuture<Slot> requestHttpUpload(
+ final Jid host,
+ final String filename,
+ final String mime,
+ final long size,
+ @Nullable final Purpose purpose) {
+ final Iq iq = new Iq(Iq.Type.GET);
+ iq.setTo(host);
+ final var request = iq.addExtension(new Request());
+ request.setFilename(convertFilename(filename));
+ request.setSize(size);
+ request.setContentType(mime);
+ if (purpose != null) {
+ request.addExtension(purpose);
+ }
+ Log.d(Config.LOGTAG, "-->" + iq);
+ final var iqFuture = this.connection.sendIqPacket(iq);
+ return Futures.transform(
+ iqFuture,
+ response -> {
+ final var slot =
+ response.getExtension(
+ im.conversations.android.xmpp.model.upload.Slot.class);
+ if (slot == null) {
+ throw new IllegalStateException("Slot not found in IQ response");
+ }
+ final var getUrl = slot.getGetUrl();
+ final var put = slot.getPut();
+ if (getUrl == null || put == null) {
+ throw new IllegalStateException("Missing get or put in slot response");
+ }
+ final var putUrl = put.getUrl();
+ if (putUrl == null) {
+ throw new IllegalStateException("Missing put url");
+ }
+ final var contentType = mime == null ? "application/octet-stream" : mime;
+ final var headers =
+ new ImmutableMap.Builder<String, String>()
+ .putAll(put.getHeadersAllowList())
+ .put("Content-Type", contentType)
+ .buildKeepingLast();
+ return new Slot(putUrl, getUrl, headers);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public Service getService() {
+ if (Config.ENABLE_HTTP_UPLOAD) {
+ final var entry =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
+ return entry == null ? null : new Service(entry);
+ }
+ return null;
+ }
+
+ private static String convertFilename(final String name) {
+ int pos = name.indexOf('.');
+ if (pos < 0) {
+ return name;
+ }
+ try {
+ UUID uuid = UUID.fromString(name.substring(0, pos));
+ ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ return Base64.encodeToString(
+ bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
+ + name.substring(pos);
+ } catch (final Exception e) {
+ return name;
+ }
+ }
+
+ public boolean isAvailableForSize(final long size) {
+ final var result = getManager(HttpUploadManager.class).getService();
+ if (result == null) {
+ return false;
+ }
+ final Long maxSize = result.getMaxFileSize();
+ if (maxSize == null) {
+ return true;
+ }
+ if (size <= maxSize) {
+ return true;
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": http upload is not available for files with"
+ + " size "
+ + size
+ + " (max is "
+ + maxSize
+ + ")");
+ return false;
+ }
+ }
+
+ public static final class Service {
+ private final Map.Entry<Jid, InfoQuery> addressInfoQuery;
+
+ public Service(final Map.Entry<Jid, InfoQuery> addressInfoQuery) {
+ this.addressInfoQuery = addressInfoQuery;
+ }
+
+ public Jid getAddress() {
+ return this.addressInfoQuery.getKey();
+ }
+
+ public InfoQuery getInfoQuery() {
+ return this.addressInfoQuery.getValue();
+ }
+
+ public boolean supportsPurpose(final Class<? extends Purpose> purpose) {
+ final var id = ExtensionFactory.id(purpose);
+ if (id == null) {
+ throw new IllegalStateException("Purpose has not been annotated as @XmlElement");
+ }
+ final var feature = String.format("%s#%s", id.namespace, id.name);
+ return getInfoQuery().hasFeature(feature);
+ }
+
+ public Long getMaxFileSize() {
+ final var value =
+ getInfoQuery()
+ .getServiceDiscoveryExtension(Namespace.HTTP_UPLOAD, "max-file-size");
+ return value == null ? null : Longs.tryParse(value);
+ }
+ }
+
+ public static class Slot {
+ public final HttpUrl put;
+ public final HttpUrl get;
+ public final Headers headers;
+
+ private Slot(final HttpUrl put, final HttpUrl get, final Headers headers) {
+ this.put = put;
+ this.get = get;
+ this.headers = headers;
+ }
+
+ private Slot(final HttpUrl put, final HttpUrl get, final Map<String, String> headers) {
+ this(put, get, Headers.of(headers));
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("put", put)
+ .add("get", get)
+ .add("headers", headers)
+ .toString();
+ }
+ }
+}
@@ -0,0 +1,59 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import com.google.common.util.concurrent.ListenableFuture;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import java.util.Collection;
+import java.util.Map;
+
+public class LegacyBookmarkManager extends AbstractBookmarkManager {
+
+ public LegacyBookmarkManager(
+ final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ }
+
+ public void handleItems(final Items items) {
+ final var account = this.getAccount();
+ if (this.hasConversion()) {
+ if (getManager(NativeBookmarkManager.class).hasFeature()) {
+ Log.w(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": received storage:bookmark notification even though we"
+ + " opted into bookmarks:1");
+ }
+ final var storage = items.getFirstItem(Storage.class);
+ final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
+ this.processBookmarksInitial(bookmarks, true);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event");
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ignoring bookmark PEP event because bookmark conversion was"
+ + " not detected");
+ }
+ }
+
+ public boolean hasConversion() {
+ return getManager(PepManager.class).hasPublishOptions()
+ && getManager(DiscoManager.class).hasAccountFeature(Namespace.BOOKMARKS_CONVERSION);
+ }
+
+ public ListenableFuture<Void> publish(final Collection<Bookmark> bookmarks) {
+ final var storage = new Storage();
+ for (final var bookmark : bookmarks) {
+ storage.addChild(bookmark);
+ }
+ return getManager(PepManager.class).publishSingleton(storage, NodeConfiguration.WHITELIST);
+ }
+}
@@ -0,0 +1,93 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.mds.Displayed;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.unique.StanzaId;
+import java.util.Map;
+
+public class MessageDisplayedSynchronizationManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public MessageDisplayedSynchronizationManager(
+ final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public void handleItems(final Items items) {
+ for (final var item : items.getItemMap(Displayed.class).entrySet()) {
+ this.processMdsItem(item);
+ }
+ }
+
+ public void processMdsItem(final Map.Entry<String, Displayed> item) {
+ final var account = getAccount();
+ final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
+ if (jid == null) {
+ return;
+ }
+ final var displayed = item.getValue();
+ final var stanzaId = displayed.getStanzaId();
+ final String id = stanzaId == null ? null : stanzaId.getId();
+ final Conversation conversation = this.service.find(account, jid);
+ if (id != null && conversation != null) {
+ conversation.setDisplayState(id);
+ this.service.markReadUpToStanzaId(conversation, id);
+ }
+ }
+
+ public void fetch() {
+ final var future = getManager(PepManager.class).fetchItems(Displayed.class);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Map<String, Displayed> result) {
+ for (final var entry : result.entrySet()) {
+ processMdsItem(entry);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not retrieve MDS items",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public static Displayed displayed(final String id, final Conversation conversation) {
+ final Jid by;
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ by = conversation.getJid().asBareJid();
+ } else {
+ by = conversation.getAccount().getJid().asBareJid();
+ }
+ final var displayed = new Displayed();
+ final var stanzaId = displayed.addExtension(new StanzaId(id));
+ stanzaId.setBy(by);
+ return displayed;
+ }
+
+ public ListenableFuture<Void> publish(final Jid itemId, final Displayed displayed) {
+ return getManager(PepManager.class)
+ .publish(displayed, itemId.toString(), NodeConfiguration.WHITELIST_MAX_ITEMS);
+ }
+}
@@ -0,0 +1,1006 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import de.gultsch.common.FutureMerger;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.StringUtils;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.IqErrorException;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.conference.DirectInvite;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.hints.NoCopy;
+import im.conversations.android.xmpp.model.hints.NoStore;
+import im.conversations.android.xmpp.model.jabber.Subject;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.History;
+import im.conversations.android.xmpp.model.muc.MultiUserChat;
+import im.conversations.android.xmpp.model.muc.Password;
+import im.conversations.android.xmpp.model.muc.Role;
+import im.conversations.android.xmpp.model.muc.admin.Item;
+import im.conversations.android.xmpp.model.muc.admin.MucAdmin;
+import im.conversations.android.xmpp.model.muc.owner.Destroy;
+import im.conversations.android.xmpp.model.muc.owner.MucOwner;
+import im.conversations.android.xmpp.model.muc.user.Invite;
+import im.conversations.android.xmpp.model.muc.user.MucUser;
+import im.conversations.android.xmpp.model.pgp.Signed;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Message;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.TreeSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class MultiUserChatManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ private final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
+ private final Set<Conversation> inProgressConferencePings = new HashSet<>();
+
+ public MultiUserChatManager(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public ListenableFuture<Void> join(final Conversation conversation) {
+ return join(conversation, true);
+ }
+
+ private ListenableFuture<Void> join(
+ final Conversation conversation, final boolean autoPushConfiguration) {
+ final var account = getAccount();
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.add(conversation);
+ }
+ if (Config.MUC_LEAVE_BEFORE_JOIN) {
+ unavailable(conversation);
+ }
+ conversation.resetMucOptions();
+ conversation.getMucOptions().setAutoPushConfiguration(autoPushConfiguration);
+ conversation.setHasMessagesLeftOnServer(false);
+ final var disco = fetchDiscoInfo(conversation);
+
+ final var caughtDisco =
+ Futures.catchingAsync(
+ disco,
+ IqErrorException.class,
+ ex -> {
+ if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException(
+ "conversation got archived before disco returned"));
+ }
+ Log.d(Config.LOGTAG, "error fetching disco#info", ex);
+ final var iqError = ex.getError();
+ if (iqError != null
+ && iqError.getCondition()
+ instanceof Condition.RemoteServerNotFound) {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ }
+ conversation
+ .getMucOptions()
+ .setError(MucOptions.Error.SERVER_NOT_FOUND);
+ service.updateConversationUi();
+ return Futures.immediateFailedFuture(ex);
+ } else {
+ return Futures.immediateFuture(new InfoQuery());
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ return Futures.transform(
+ caughtDisco,
+ v -> {
+ checkConfigurationSendPresenceFetchHistory(conversation);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> joinFollowingInvite(final Conversation conversation) {
+ // TODO this special treatment is probably unnecessary; just always make sure the bookmark
+ // exists
+ return Futures.transform(
+ join(conversation),
+ v -> {
+ // we used to do this only for private groups
+ final Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ if (bookmark.autojoin()) {
+ return null;
+ }
+ bookmark.setAutojoin(true);
+ getManager(BookmarkManager.class).create(bookmark);
+ } else {
+ getManager(BookmarkManager.class).save(conversation, null);
+ }
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void checkConfigurationSendPresenceFetchHistory(final Conversation conversation) {
+
+ Account account = conversation.getAccount();
+ final MucOptions mucOptions = conversation.getMucOptions();
+
+ if (mucOptions.nonanonymous()
+ && !mucOptions.membersOnly()
+ && !conversation.getBooleanAttribute("accept_non_anonymous", false)) {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ }
+ mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
+ service.updateConversationUi();
+ return;
+ }
+
+ final Jid joinJid = mucOptions.getSelf().getFullJid();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid().toString()
+ + ": joining conversation "
+ + joinJid.toString());
+
+ final var x = new MultiUserChat();
+
+ if (mucOptions.getPassword() != null) {
+ x.addExtension(new Password(mucOptions.getPassword()));
+ }
+
+ final var history = x.addExtension(new History());
+
+ if (mucOptions.mamSupport()) {
+ // Use MAM instead of the limited muc history to get history
+ history.setMaxStanzas(0);
+ } else {
+ // Fallback to muc history
+ history.setSince(conversation.getLastMessageTransmitted().getTimestamp());
+ }
+ available(joinJid, mucOptions.nonanonymous(), x);
+ if (!joinJid.equals(conversation.getJid())) {
+ conversation.setContactJid(joinJid);
+ getDatabase().updateConversation(conversation);
+ }
+
+ if (mucOptions.mamSupport()) {
+ this.service.getMessageArchiveService().catchupMUC(conversation);
+ }
+ fetchMembers(conversation);
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.remove(conversation);
+ this.service.sendUnsentMessages(conversation);
+ }
+ }
+
+ public ListenableFuture<Conversation> createPrivateGroupChat(
+ final String name, final Collection<Jid> addresses) {
+ final var service = getService();
+ if (service == null) {
+ return Futures.immediateFailedFuture(new IllegalStateException("No MUC service found"));
+ }
+ final var address = Jid.ofLocalAndDomain(CryptoHelper.pronounceable(), service);
+ final var conversation =
+ this.service.findOrCreateConversation(getAccount(), address, true, false, true);
+ final var join = this.join(conversation, false);
+ final var configured =
+ Futures.transformAsync(
+ join,
+ v -> {
+ final var options =
+ configWithName(defaultGroupChatConfiguration(), name);
+ return pushConfiguration(conversation, options);
+ },
+ MoreExecutors.directExecutor());
+
+ // TODO add catching to 'configured' to archive the chat again
+
+ return Futures.transform(
+ configured,
+ c -> {
+ for (var invitee : addresses) {
+ this.service.invite(conversation, invitee);
+ }
+ final var account = getAccount();
+ for (final var resource :
+ account.getSelfContact().getPresences().toResourceArray()) {
+ Jid other = getAccount().getJid().withResource(resource);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": sending direct invite to "
+ + other);
+ this.service.directInvite(conversation, other);
+ }
+ getManager(BookmarkManager.class).save(conversation, name);
+ return conversation;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Conversation> createPublicChannel(
+ final Jid address, final String name) {
+
+ final var conversation =
+ this.service.findOrCreateConversation(getAccount(), address, true, false, true);
+
+ final var join = this.join(conversation, false);
+ final var configuration =
+ Futures.transformAsync(
+ join,
+ v -> {
+ final var options = configWithName(defaultChannelConfiguration(), name);
+ return pushConfiguration(conversation, options);
+ },
+ MoreExecutors.directExecutor());
+
+ // TODO mostly ignore configuration error
+
+ return Futures.transform(
+ configuration,
+ v -> {
+ getManager(BookmarkManager.class).save(conversation, name);
+ return conversation;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void leave(final Conversation conversation) {
+ final var mucOptions = conversation.getMucOptions();
+ mucOptions.setOffline();
+ getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
+ unavailable(conversation);
+ }
+
+ public void handlePresence(final Presence presence) {}
+
+ public void handleStatusMessage(final Message message) {
+ final var from = Jid.Invalid.getNullForInvalid(message.getFrom());
+ final var mucUser = message.getExtension(MucUser.class);
+ if (from == null || from.isFullJid() || mucUser == null) {
+ return;
+ }
+ final var conversation = this.service.find(getAccount(), from);
+ if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) {
+ return;
+ }
+ for (final var status : mucUser.getStatus()) {
+ handleStatusCode(conversation, status);
+ }
+ final var item = mucUser.getItem();
+ if (item == null) {
+ return;
+ }
+ final var user = itemToUser(conversation, item, null, null, null, null);
+ this.handleAffiliationChange(conversation, user);
+ }
+
+ private void handleAffiliationChange(
+ final Conversation conversation, final MucOptions.User user) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid()
+ + ": changing affiliation for "
+ + user.getRealJid()
+ + " to "
+ + user.getAffiliation()
+ + " in "
+ + conversation.getJid().asBareJid());
+ if (user.realJidMatchesAccount()) {
+ return;
+ }
+ final var mucOptions = conversation.getMucOptions();
+ final boolean isNew = mucOptions.updateUser(user);
+ final var avatarService = this.service.getAvatarService();
+ if (Strings.isNullOrEmpty(mucOptions.getAvatar())) {
+ avatarService.clear(mucOptions);
+ }
+ avatarService.clear(user);
+ this.service.updateMucRosterUi();
+ this.service.updateConversationUi();
+ if (user.ranks(Affiliation.MEMBER)) {
+ fetchDeviceIdsIfNeeded(isNew, user);
+ } else {
+ final var jid = user.getRealJid();
+ final var cryptoTargets = conversation.getAcceptedCryptoTargets();
+ if (cryptoTargets.remove(user.getRealJid())) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": removed "
+ + jid
+ + " from crypto targets of "
+ + conversation.getName());
+ conversation.setAcceptedCryptoTargets(cryptoTargets);
+ getDatabase().updateConversation(conversation);
+ }
+ }
+ }
+
+ private void fetchDeviceIdsIfNeeded(final boolean isNew, final MucOptions.User user) {
+ final var contact = user.getContact();
+ final var mucOptions = user.getMucOptions();
+ final var axolotlService = connection.getAxolotlService();
+ if (isNew
+ && user.getRealJid() != null
+ && mucOptions.isPrivateAndNonAnonymous()
+ && (contact == null || !contact.mutualPresenceSubscription())
+ && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
+ axolotlService.fetchDeviceIds(user.getRealJid());
+ }
+ }
+
+ private void handleStatusCode(final Conversation conversation, final int status) {
+ if ((status >= 170 && status <= 174) || (status >= 102 && status <= 104)) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": fetching disco#info on status code "
+ + status);
+ getManager(MultiUserChatManager.class).fetchDiscoInfo(conversation);
+ }
+ }
+
+ public ListenableFuture<Void> fetchDiscoInfo(final Conversation conversation) {
+ final var address = conversation.getJid().asBareJid();
+ final var future =
+ connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null);
+ return Futures.transform(
+ future,
+ infoQuery -> {
+ setDiscoInfo(conversation, infoQuery);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setDiscoInfo(final Conversation conversation, final InfoQuery result) {
+ final var account = conversation.getAccount();
+ final var address = conversation.getJid().asBareJid();
+ final var avatarHash =
+ result.getServiceDiscoveryExtension(
+ Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash");
+ if (VCardUpdate.isValidSHA1(avatarHash)) {
+ connection.getManager(AvatarManager.class).handleVCardUpdate(address, avatarHash);
+ }
+ final MucOptions mucOptions = conversation.getMucOptions();
+ final Bookmark bookmark = conversation.getBookmark();
+ final boolean sameBefore =
+ StringUtils.equals(
+ bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
+
+ final var hadOccupantId = mucOptions.occupantId();
+ if (mucOptions.updateConfiguration(result)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": muc configuration changed for "
+ + conversation.getJid().asBareJid());
+ getDatabase().updateConversation(conversation);
+ }
+
+ final var hasOccupantId = mucOptions.occupantId();
+
+ if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
+ final var me = mucOptions.getSelf().getFullJid();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": gained support for occupant-id in "
+ + me
+ + ". resending presence");
+ this.available(me, mucOptions.nonanonymous());
+ }
+
+ if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
+ if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ }
+ this.service.updateConversationUi();
+ }
+
+ public void resendPresence(final Conversation conversation) {
+ final MucOptions mucOptions = conversation.getMucOptions();
+ if (mucOptions.online()) {
+ available(mucOptions.getSelf().getFullJid(), mucOptions.nonanonymous());
+ }
+ }
+
+ private void available(
+ final Jid address, final boolean nonAnonymous, final Extension... extensions) {
+ final var presenceManager = getManager(PresenceManager.class);
+ final var account = getAccount();
+ final String pgpSignature = account.getPgpSignature();
+ if (nonAnonymous && pgpSignature != null) {
+ final String message = account.getPresenceStatusMessage();
+ presenceManager.available(
+ address, message, combine(extensions, new Signed(pgpSignature)));
+ } else {
+ presenceManager.available(address, extensions);
+ }
+ }
+
+ public void unavailable(final Conversation conversation) {
+ final var mucOptions = conversation.getMucOptions();
+ getManager(PresenceManager.class).unavailable(mucOptions.getSelf().getFullJid());
+ }
+
+ private static Extension[] combine(final Extension[] extensions, final Extension extension) {
+ return new ImmutableList.Builder<Extension>()
+ .addAll(Arrays.asList(extensions))
+ .add(extension)
+ .build()
+ .toArray(new Extension[0]);
+ }
+
+ public ListenableFuture<Void> pushConfiguration(
+ final Conversation conversation, final Map<String, Object> input) {
+ final var address = conversation.getJid().asBareJid();
+ final var configuration = modifyBestInteroperability(input);
+
+ if (configuration.get("muc#roomconfig_whois") instanceof String whois
+ && whois.equals("anyone")) {
+ conversation.setAttribute("accept_non_anonymous", true);
+ getDatabase().updateConversation(conversation);
+ }
+
+ final var future = fetchConfigurationForm(address);
+ return Futures.transformAsync(
+ future,
+ current -> {
+ final var modified = current.submit(configuration);
+ return submitConfigurationForm(address, modified);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Data> fetchConfigurationForm(final Jid address) {
+ final var iq = new Iq(Iq.Type.GET, new MucOwner());
+ iq.setTo(address);
+ Log.d(Config.LOGTAG, "fetching configuration form: " + iq);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ response -> {
+ final var mucOwner = response.getExtension(MucOwner.class);
+ if (mucOwner == null) {
+ throw new IllegalStateException("Missing MucOwner element in response");
+ }
+ return mucOwner.getConfiguration();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> submitConfigurationForm(final Jid address, final Data data) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var mucOwner = iq.addExtension(new MucOwner());
+ mucOwner.addExtension(data);
+ Log.d(Config.LOGTAG, "pushing configuration form: " + iq);
+ return Futures.transform(
+ this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> fetchMembers(final Conversation conversation) {
+ final var affiliations = new ArrayList<Affiliation>();
+ affiliations.add(Affiliation.OUTCAST);
+ if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of(Affiliation.OWNER, Affiliation.ADMIN, Affiliation.MEMBER));
+ final var futures =
+ Collections2.transform(
+ affiliations,
+ a -> fetchAffiliations(conversation, a));
+ ListenableFuture<List<MucOptions.User>> future = FutureMerger.allAsList(futures);
+ return Futures.transform(
+ future,
+ members -> {
+ setMembers(conversation, members);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setMembers(final Conversation conversation, final List<MucOptions.User> users) {
+ for (final var user : users) {
+ if (user.realJidMatchesAccount()) {
+ continue;
+ }
+ boolean isNew = conversation.getMucOptions().updateUser(user);
+ fetchDeviceIdsIfNeeded(isNew, user);
+ }
+ final var mucOptions = conversation.getMucOptions();
+ final var members = mucOptions.getMembers(true);
+ final var cryptoTargets = conversation.getAcceptedCryptoTargets();
+ boolean changed = false;
+ for (final var iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) {
+ final var jid = iterator.next();
+ if (!members.contains(jid) && !members.contains(jid.getDomain())) {
+ iterator.remove();
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": removed "
+ + jid
+ + " from crypto targets of "
+ + conversation.getName());
+ changed = true;
+ }
+ }
+ if (changed) {
+ conversation.setAcceptedCryptoTargets(cryptoTargets);
+ getDatabase().updateConversation(conversation);
+ }
+ // TODO only when room has no avatar
+ this.service.getAvatarService().clear(mucOptions);
+ this.service.updateMucRosterUi();
+ this.service.updateConversationUi();
+ }
+
+ private ListenableFuture<Collection<MucOptions.User>> fetchAffiliations(
+ final Conversation conversation, final Affiliation affiliation) {
+ final var iq = new Iq(Iq.Type.GET);
+ iq.setTo(conversation.getJid().asBareJid());
+ iq.addExtension(new MucAdmin()).addExtension(new Item()).setAffiliation(affiliation);
+ return Futures.transform(
+ this.connection.sendIqPacket(iq),
+ response -> {
+ final var mucAdmin = response.getExtension(MucAdmin.class);
+ if (mucAdmin == null) {
+ throw new IllegalStateException("No query in response");
+ }
+ return Collections2.transform(
+ mucAdmin.getItems(), i -> itemToUser(conversation, i, null, null, null, null));
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> changeUsername(
+ final Conversation conversation, final String username) {
+
+ // TODO when online send normal available presence
+ // TODO when not online do a normal join
+
+ final Bookmark bookmark = conversation.getBookmark();
+ final MucOptions options = conversation.getMucOptions();
+ final Jid joinJid = options.createJoinJid(username);
+ if (joinJid == null) {
+ return Futures.immediateFailedFuture(new IllegalArgumentException());
+ }
+
+ if (options.online()) {
+ final SettableFuture<Void> renameFuture = SettableFuture.create();
+ options.setOnRenameListener(
+ new MucOptions.OnRenameListener() {
+
+ @Override
+ public void onSuccess() {
+ renameFuture.set(null);
+ }
+
+ @Override
+ public void onFailure() {
+ renameFuture.setException(new IllegalStateException());
+ }
+ });
+
+ available(joinJid, options.nonanonymous());
+
+ if (username.equals(MucOptions.defaultNick(getAccount()))
+ && bookmark != null
+ && bookmark.getNick() != null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": removing nick from bookmark for "
+ + bookmark.getJid());
+ bookmark.setNick(null);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ return renameFuture;
+ } else {
+ conversation.setContactJid(joinJid);
+ getDatabase().updateConversation(conversation);
+ if (bookmark != null) {
+ bookmark.setNick(username);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ join(conversation);
+ return Futures.immediateVoidFuture();
+ }
+ }
+
+ public void checkMucRequiresRename(final Conversation conversation) {
+ final var options = conversation.getMucOptions();
+ if (!options.online()) {
+ return;
+ }
+ final String current = options.getActualNick();
+ final String proposed = options.getProposedNickPure();
+ if (current == null || current.equals(proposed)) {
+ return;
+ }
+ final Jid joinJid = options.createJoinJid(proposed);
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: muc rename required %s (was: %s)",
+ getAccount().getJid().asBareJid(), joinJid, current));
+ available(joinJid, options.nonanonymous());
+ }
+
+ public void setPassword(final Conversation conversation, final String password) {
+ final var bookmark = conversation.getBookmark();
+ conversation.getMucOptions().setPassword(password);
+ if (bookmark != null) {
+ bookmark.setAutojoin(true);
+ getManager(BookmarkManager.class).create(bookmark);
+ }
+ getDatabase().updateConversation(conversation);
+ this.join(conversation);
+ }
+
+ public void pingAndRejoin(final Conversation conversation) {
+ final Account account = getAccount();
+ synchronized (this.inProgressConferenceJoins) {
+ if (this.inProgressConferenceJoins.contains(conversation)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": canceling muc self ping because join is already under way");
+ return;
+ }
+ }
+ synchronized (this.inProgressConferencePings) {
+ if (!this.inProgressConferencePings.add(conversation)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": canceling muc self ping because ping is already under way");
+ return;
+ }
+ }
+ final Jid self = conversation.getMucOptions().getSelf().getFullJid();
+ final var future = getManager(PingManager.class).ping(self);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Iq result) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " came back fine");
+ synchronized (MultiUserChatManager.this.inProgressConferencePings) {
+ MultiUserChatManager.this.inProgressConferencePings.remove(
+ conversation);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ synchronized (MultiUserChatManager.this.inProgressConferencePings) {
+ MultiUserChatManager.this.inProgressConferencePings.remove(
+ conversation);
+ }
+ if (throwable instanceof IqErrorException iqErrorException) {
+ final var condition = iqErrorException.getErrorCondition();
+ if (condition instanceof Condition.ServiceUnavailable
+ || condition instanceof Condition.FeatureNotImplemented
+ || condition instanceof Condition.ItemNotFound) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " came back as ignorable error");
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": ping to "
+ + self
+ + " failed. attempting rejoin");
+ join(conversation);
+ }
+ }
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> destroy(final Jid address) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var mucOwner = iq.addExtension(new MucOwner());
+ mucOwner.addExtension(new Destroy());
+ return Futures.transform(
+ connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> setAffiliation(
+ final Conversation conversation, final Affiliation affiliation, Jid user) {
+ return setAffiliation(conversation, affiliation, Collections.singleton(user));
+ }
+
+ public ListenableFuture<Void> setAffiliation(
+ final Conversation conversation,
+ final Affiliation affiliation,
+ final Collection<Jid> users) {
+ final var address = conversation.getJid().asBareJid();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var admin = iq.addExtension(new MucAdmin());
+ for (final var user : users) {
+ final var item = admin.addExtension(new Item());
+ item.setJid(user);
+ item.setAffiliation(affiliation);
+ }
+ return Futures.transform(
+ this.connection.sendIqPacket(iq),
+ response -> {
+ // TODO figure out what this was meant to do
+ // is this a work around for some servers not sending notifications when
+ // changing the affiliation of people not in the room? this would explain this
+ // firing only when getRole == None
+ final var mucOptions = conversation.getMucOptions();
+ for (final var user : users) {
+ mucOptions.changeAffiliation(user, affiliation);
+ }
+ service.getAvatarService().clear(mucOptions);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> setRole(final Jid address, final Role role, final String user) {
+ return setRole(address, role, Collections.singleton(user));
+ }
+
+ public ListenableFuture<Void> setRole(
+ final Jid address, final Role role, final Collection<String> users) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var admin = iq.addExtension(new MucAdmin());
+ for (final var user : users) {
+ final var item = admin.addExtension(new Item());
+ item.setNick(user);
+ item.setRole(role);
+ }
+ return Futures.transform(
+ this.connection.sendIqPacket(iq), response -> null, MoreExecutors.directExecutor());
+ }
+
+ public void setSubject(final Conversation conversation, final String subject) {
+ final var message = new Message();
+ message.setType(Message.Type.GROUPCHAT);
+ message.setTo(conversation.getJid().asBareJid());
+ message.addExtension(new Subject(subject));
+ connection.sendMessagePacket(message);
+ }
+
+ public void invite(final Conversation conversation, final Jid address) {
+ Log.d(
+ Config.LOGTAG,
+ conversation.getAccount().getJid().asBareJid()
+ + ": inviting "
+ + address
+ + " to "
+ + conversation.getJid().asBareJid());
+ final MucOptions.User user =
+ conversation.getMucOptions().findUserByRealJid(address.asBareJid());
+ if (user == null || user.getAffiliation() == Affiliation.OUTCAST) {
+ this.setAffiliation(conversation, Affiliation.NONE, address);
+ }
+
+ final var packet = new Message();
+ packet.setTo(conversation.getJid().asBareJid());
+ final var x = packet.addExtension(new MucUser());
+ final var invite = x.addExtension(new Invite());
+ invite.setTo(address.asBareJid());
+ connection.sendMessagePacket(packet);
+ }
+
+ public void directInvite(final Conversation conversation, final Jid address) {
+ final var message = new Message();
+ message.setTo(address);
+ final var directInvite = message.addExtension(new DirectInvite());
+ directInvite.setJid(conversation.getJid().asBareJid());
+ final var password = conversation.getMucOptions().getPassword();
+ if (password != null) {
+ directInvite.setPassword(password);
+ }
+ if (address.isFullJid()) {
+ message.addExtension(new NoStore());
+ message.addExtension(new NoCopy());
+ }
+ this.connection.sendMessagePacket(message);
+ }
+
+ public boolean isJoinInProgress(final Conversation conversation) {
+ synchronized (this.inProgressConferenceJoins) {
+ if (conversation.getMode() == Conversational.MODE_MULTI) {
+ final boolean inProgress = this.inProgressConferenceJoins.contains(conversation);
+ if (inProgress) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": holding back message to group. join in progress");
+ }
+ return inProgress;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public void clearInProgress() {
+ synchronized (this.inProgressConferenceJoins) {
+ this.inProgressConferenceJoins.clear();
+ }
+ synchronized (this.inProgressConferencePings) {
+ this.inProgressConferencePings.clear();
+ }
+ }
+
+ public Jid getService() {
+ return Iterables.getFirst(this.getServices(), null);
+ }
+
+ public List<Jid> getServices() {
+ final var builder = new ImmutableList.Builder<Jid>();
+ for (final var entry : getManager(DiscoManager.class).getServerItems().entrySet()) {
+ final var value = entry.getValue();
+ if (value.getFeatureStrings().contains(Namespace.MUC)
+ && value.hasIdentityWithCategoryAndType("conference", "text")
+ && !value.getFeatureStrings().contains("jabber:iq:gateway")
+ && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
+ builder.add(entry.getKey());
+ }
+ }
+ return builder.build();
+ }
+
+ public static MucOptions.User itemToUser(
+ final Conversation conference,
+ im.conversations.android.xmpp.model.muc.Item item,
+ final Jid from,
+ final String occupantId,
+ final String nicknameIn,
+ final Element hatsEl) {
+ final var affiliation = item.getAffiliation();
+ final var role = item.getRole();
+ var nick = item.getNick();
+ try {
+ if (nicknameIn != null && nick != null && !nick.equals(nicknameIn) && gnu.inet.encoding.Punycode.decode(nick).equals(nicknameIn)) {
+ nick = nicknameIn;
+ }
+ } catch (final Exception e) { }
+ Set<MucOptions.Hat> hats = new TreeSet<>();
+ for (final var hat : hatsEl.getChildren()) {
+ if ("hat".equals(hat.getName()) && ("urn:xmpp:hats:0".equals(hat.getNamespace()) || "xmpp:prosody.im/protocol/hats:1".equals(hat.getNamespace()))) {
+ hats.add(new MucOptions.Hat(hat));
+ }
+ }
+ final Jid fullAddress;
+ if (from != null && from.isFullJid()) {
+ fullAddress = from;
+ } else if (Strings.isNullOrEmpty(nick)) {
+ fullAddress = null;
+ } else {
+ fullAddress = ofNick(conference, nick);
+ }
+ final Jid realJid = item.getAttributeAsJid("jid");
+ MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullAddress, occupantId, nick, hats);
+ if (Jid.Invalid.isValid(realJid)) {
+ user.setRealJid(realJid);
+ }
+ user.setAffiliation(affiliation);
+ user.setRole(role);
+ return user;
+ }
+
+ private static Jid ofNick(final Conversation conversation, final String nick) {
+ try {
+ return conversation.getJid().withResource(nick);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ private static Map<String, Object> modifyBestInteroperability(
+ final Map<String, Object> unmodified) {
+ final var builder = new ImmutableMap.Builder<String, Object>();
+ builder.putAll(unmodified);
+
+ if (unmodified.get("muc#roomconfig_moderatedroom") instanceof Boolean moderated) {
+ builder.put("members_by_default", !moderated);
+ }
+ if (unmodified.get("muc#roomconfig_allowpm") instanceof String allowPm) {
+ // ejabberd :-/
+ final boolean allow = "anyone".equals(allowPm);
+ builder.put("allow_private_messages", allow);
+ builder.put("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
+ }
+
+ if (unmodified.get("muc#roomconfig_allowinvites") instanceof Boolean allowInvites) {
+ // TODO check that this actually does something useful?
+ builder.put(
+ "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", allowInvites);
+ }
+
+ return builder.buildOrThrow();
+ }
+
+ private static Map<String, Object> configWithName(
+ final Map<String, Object> unmodified, final String name) {
+ if (Strings.isNullOrEmpty(name)) {
+ return unmodified;
+ }
+ return new ImmutableMap.Builder<String, Object>()
+ .putAll(unmodified)
+ .put("muc#roomconfig_roomname", name)
+ .buildKeepingLast();
+ }
+
+ public static Map<String, Object> defaultGroupChatConfiguration() {
+ return new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_membersonly", true)
+ .put("muc#roomconfig_publicroom", false)
+ .put("muc#roomconfig_whois", "anyone")
+ .put("muc#roomconfig_changesubject", false)
+ .put("muc#roomconfig_allowinvites", false)
+ .put("muc#roomconfig_enablearchiving", true) // prosody
+ .put("mam", true) // ejabberd community
+ .put("muc#roomconfig_mam", true) // ejabberd saas
+ .buildOrThrow();
+ }
+
+ public static Map<String, Object> defaultChannelConfiguration() {
+ return new ImmutableMap.Builder<String, Object>()
+ .put("muc#roomconfig_persistentroom", true)
+ .put("muc#roomconfig_membersonly", false)
+ .put("muc#roomconfig_publicroom", true)
+ .put("muc#roomconfig_whois", "moderators")
+ .put("muc#roomconfig_changesubject", false)
+ .put("muc#roomconfig_enablearchiving", true) // prosody
+ .put("mam", true) // ejabberd community
+ .put("muc#roomconfig_mam", true) // ejabberd saas
+ .buildOrThrow();
+ }
+}
@@ -0,0 +1,178 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
+import im.conversations.android.xmpp.model.bookmark2.Nick;
+import im.conversations.android.xmpp.model.bookmark2.Password;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.pubsub.event.Retract;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public class NativeBookmarkManager extends AbstractBookmarkManager {
+
+ public NativeBookmarkManager(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ }
+
+ public void fetch() {
+ final var future = getManager(PepManager.class).fetchItems(Conference.class);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Map<String, Conference> bookmarks) {
+ final var builder = new ImmutableMap.Builder<Jid, Bookmark>();
+ for (final var entry : bookmarks.entrySet()) {
+ final Bookmark bookmark =
+ itemToBookmark(entry.getKey(), entry.getValue(), getAccount());
+ if (bookmark == null) {
+ continue;
+ }
+ builder.put(bookmark.getJid(), bookmark);
+ }
+ processBookmarksInitial(builder.buildKeepingLast(), true);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.d(Config.LOGTAG, "Could not fetch bookmarks", throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void handleItems(final Items items) {
+ this.handleItems(items.getItemMap(Conference.class));
+ this.handleRetractions(items.getRetractions());
+ }
+
+ private void handleRetractions(final Collection<Retract> retractions) {
+ final var account = getAccount();
+ for (final var retract : retractions) {
+ final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
+ if (id != null) {
+ account.removeBookmark(id);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
+ processDeletedBookmark(id);
+ service.updateConversationUi();
+ }
+ }
+ }
+
+ private void handleItems(final Map<String, Conference> items) {
+ final var account = getAccount();
+ for (final var item : items.entrySet()) {
+ final Bookmark bookmark = itemToBookmark(item.getKey(), item.getValue(), account);
+ if (bookmark == null) {
+ continue;
+ }
+ account.putBookmark(bookmark);
+ getManager(BookmarkManager.class).processModifiedBookmark(bookmark, true);
+ service.updateConversationUi();
+ }
+ }
+
+ public ListenableFuture<Void> publish(final Bookmark bookmark) {
+ final var address = bookmark.getJid();
+ final var name = bookmark.getBookmarkName();
+ final var nick = bookmark.getNick();
+ final String password = bookmark.getPassword();
+ final var itemId = address.toString();
+ final var conference = new Conference();
+ conference.setAutoJoin(bookmark.autojoin());
+ if (nick != null) {
+ conference.addExtension(new Nick()).setContent(nick);
+ }
+ if (name != null) {
+ conference.setConferenceName(name);
+ }
+ if (password != null) {
+ conference.addExtension(new Password()).setContent(password);
+ }
+ conference.addExtension(bookmark.getExtensions());
+ return Futures.transform(
+ getManager(PepManager.class)
+ .publish(conference, itemId, NodeConfiguration.WHITELIST_MAX_ITEMS),
+ result -> null,
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> retract(final Jid address) {
+ final var itemId = address.toString();
+ return Futures.transform(
+ getManager(PepManager.class).retract(itemId, Namespace.BOOKMARKS2),
+ result -> null,
+ MoreExecutors.directExecutor());
+ }
+
+ private void deleteAllItems() {
+ final var account = getAccount();
+ final var previous = account.getBookmarkedJids();
+ account.setBookmarks(Collections.emptyMap());
+ processDeletedBookmarks(previous);
+ }
+
+ public void handleDelete() {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": deleted bookmarks node");
+ this.deleteAllItems();
+ }
+
+ public void handlePurge() {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + ": purged bookmarks");
+ this.deleteAllItems();
+ }
+
+ public boolean hasFeature() {
+ final var pep = getManager(PepManager.class);
+ final var disco = getManager(DiscoManager.class);
+ return pep.hasPublishOptions()
+ && pep.hasConfigNodeMax()
+ && disco.hasAccountFeature(Namespace.BOOKMARKS2_COMPAT);
+ }
+
+ private static Bookmark itemToBookmark(
+ final String id, final Conference conference, final Account account) {
+ if (id == null || conference == null) {
+ return null;
+ }
+ final var jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
+ if (jid == null || jid.isFullJid()) {
+ return null;
+ }
+ final Bookmark bookmark = new Bookmark(account, jid);
+
+ // TODO use proper API
+
+ bookmark.setBookmarkName(conference.getAttribute("name"));
+ bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
+ bookmark.setNick(conference.findChildContent("nick"));
+ bookmark.setPassword(conference.findChildContent("password"));
+ final var extensions = conference.getExtensions();
+ if (extensions != null) {
+ for (final var ext : extensions.getChildren()) {
+ if (ext.getName().equals("group") && ext.getNamespace().equals("jabber:iq:roster")) {
+ bookmark.addGroup(ext.getContent());
+ }
+ }
+
+ bookmark.setExtensions(conference.getExtensions());
+ }
+ return bookmark;
+ }
+}
@@ -0,0 +1,64 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.ListenableFuture;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.nick.Nick;
+import im.conversations.android.xmpp.model.pubsub.Items;
+
+public class NickManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public NickManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public void handleItems(final Jid from, final Items items) {
+ final var item = items.getFirstItem(Nick.class);
+ final var nick = item == null ? null : item.getContent();
+ if (from == null || Strings.isNullOrEmpty(nick)) {
+ return;
+ }
+ setNick(from, nick);
+ }
+
+ private void setNick(final Jid user, final String nick) {
+ final var account = getAccount();
+ if (user.asBareJid().equals(account.getJid().asBareJid())) {
+ account.setDisplayName(nick);
+ if (QuickConversationsService.isQuicksy()) {
+ service.getAvatarService().clear(account);
+ }
+ service.checkMucRequiresRename();
+ } else {
+ final Contact contact = account.getRoster().getContact(user);
+ if (contact.setPresenceName(nick)) {
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
+ service.getAvatarService().clear(contact);
+ }
+ }
+ service.updateConversationUi();
+ service.updateAccountUi();
+ }
+
+ public ListenableFuture<Void> publish(final String name) {
+ if (Strings.isNullOrEmpty(name)) {
+ return getManager(PepManager.class).delete(Namespace.NICK);
+ } else {
+ return getManager(PepManager.class)
+ .publishSingleton(new Nick(name), NodeConfiguration.PRESENCE);
+ }
+ }
+
+ public void handleDelete(final Jid from) {
+ this.setNick(from, null);
+ }
+}
@@ -0,0 +1,31 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.offline.Offline;
+import im.conversations.android.xmpp.model.offline.Purge;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class OfflineMessagesManager extends AbstractManager {
+
+ public OfflineMessagesManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public ListenableFuture<Void> purge() {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.addExtension(new Offline()).addExtension(new Purge());
+ final var future = connection.sendIqPacket(iq);
+ return Futures.transform(future, result -> null, MoreExecutors.directExecutor());
+ }
+
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class)
+ .hasServerFeature(Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
+ }
+}
@@ -0,0 +1,79 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.Map;
+
+public class PepManager extends AbstractManager {
+
+ public PepManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public <T extends Extension> ListenableFuture<Map<String, T>> fetchItems(final Class<T> clazz) {
+ return pubSubManager().fetchItems(pepService(), clazz);
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchMostRecentItem(final Class<T> clazz) {
+ return pubSubManager().fetchMostRecentItem(pepService(), clazz);
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchMostRecentItem(
+ final String node, final Class<T> clazz) {
+ return pubSubManager().fetchMostRecentItem(pepService(), node, clazz);
+ }
+
+ public ListenableFuture<Void> publish(
+ Extension item, final String itemId, final NodeConfiguration nodeConfiguration) {
+ return pubSubManager().publish(pepService(), item, itemId, nodeConfiguration);
+ }
+
+ public ListenableFuture<Void> publishSingleton(
+ Extension item, final String node, final NodeConfiguration nodeConfiguration) {
+ return pubSubManager().publishSingleton(pepService(), item, node, nodeConfiguration);
+ }
+
+ public ListenableFuture<Void> publishSingleton(
+ final Extension item, final NodeConfiguration nodeConfiguration) {
+ return pubSubManager().publishSingleton(pepService(), item, nodeConfiguration);
+ }
+
+ public ListenableFuture<Iq> retract(final String itemId, final String node) {
+ return pubSubManager().retract(pepService(), itemId, node);
+ }
+
+ public ListenableFuture<Void> delete(final String node) {
+ final var future = pubSubManager().delete(pepService(), node);
+ return Futures.transform(future, iq -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Data> getNodeConfiguration(final String node) {
+ return pubSubManager().getNodeConfiguration(pepService(), node);
+ }
+
+ public boolean hasPublishOptions() {
+ return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_PUBLISH_OPTIONS);
+ }
+
+ public boolean hasConfigNodeMax() {
+ return getManager(DiscoManager.class).hasAccountFeature(Namespace.PUBSUB_CONFIG_NODE_MAX);
+ }
+
+ private PubSubManager pubSubManager() {
+ return getManager(PubSubManager.class);
+ }
+
+ private Jid pepService() {
+ return connection.getAccount().getJid().asBareJid();
+ }
+}
@@ -4,7 +4,9 @@ import android.content.Context;
import androidx.annotation.NonNull;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.services.XmppConnectionService;
import im.conversations.android.xmpp.model.ping.Ping;
@@ -25,6 +27,12 @@ public class PingManager extends AbstractManager {
}
}
+ public ListenableFuture<Iq> ping(final Jid address) {
+ final var iq = new Iq(Iq.Type.GET, new Ping());
+ iq.setTo(address);
+ return this.connection.sendIqPacket(iq);
+ }
+
public void ping(final Runnable runnable) {
final var pingFuture = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping()));
Futures.addCallback(
@@ -45,4 +53,8 @@ public class PingManager extends AbstractManager {
},
MoreExecutors.directExecutor());
}
+
+ public void pong(final Iq packet) {
+ this.connection.sendResultFor(packet);
+ }
}
@@ -2,12 +2,24 @@ package eu.siacs.conversations.xmpp.manager;
import android.content.Context;
import eu.siacs.conversations.services.XmppConnectionService;
+import android.util.Log;
+import com.google.common.base.Strings;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.Device;
+import eu.siacs.conversations.generator.AbstractGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.EntityCapabilities2;
import im.conversations.android.xmpp.ServiceDescription;
+import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.capabilties.Capabilities;
import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
+import im.conversations.android.xmpp.model.nick.Nick;
+import im.conversations.android.xmpp.model.pars.PreAuth;
import im.conversations.android.xmpp.model.pgp.Signed;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.HashMap;
@@ -15,11 +27,135 @@ import java.util.Map;
public class PresenceManager extends AbstractManager {
+ private final XmppConnectionService service;
+ private final AppSettings appSettings;
+
private final Map<EntityCapabilities.Hash, ServiceDescription> serviceDescriptions =
new HashMap<>();
- public PresenceManager(XmppConnectionService context, XmppConnection connection) {
- super(context, connection);
+ public PresenceManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.appSettings = new AppSettings(service.getApplicationContext());
+ this.service = service;
+ }
+
+ public void subscribe(final Jid address) {
+ subscribe(address, null);
+ }
+
+ public void subscribe(final Jid address, final String preAuth) {
+
+ var presence = new Presence(Presence.Type.SUBSCRIBE);
+ presence.setTo(address);
+
+ final var displayName = getAccount().getDisplayName();
+ if (!Strings.isNullOrEmpty(displayName)) {
+ presence.addExtension(new Nick(displayName));
+ }
+ if (preAuth != null) {
+ presence.addExtension(new PreAuth()).setToken(preAuth);
+ }
+ this.connection.sendPresencePacket(presence);
+ }
+
+ public void unsubscribe(final Jid address) {
+ var presence = new Presence(Presence.Type.UNSUBSCRIBE);
+ presence.setTo(address);
+ this.connection.sendPresencePacket(presence);
+ }
+
+ public void unsubscribed(final Jid address) {
+ var presence = new Presence(Presence.Type.UNSUBSCRIBED);
+ presence.setTo(address);
+ this.connection.sendPresencePacket(presence);
+ }
+
+ public void subscribed(final Jid address) {
+ var presence = new Presence(Presence.Type.SUBSCRIBED);
+ presence.setTo(address);
+ this.connection.sendPresencePacket(presence);
+ }
+
+ public void available() {
+ available(service.checkListeners() && appSettings.isBroadcastLastActivity());
+ }
+
+ public void available(final boolean withIdle) {
+ final var account = connection.getAccount();
+ final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();
+ final var infoQuery = serviceDiscoveryFeatures.asInfoQuery();
+ final var capsHash = EntityCapabilities.hash(infoQuery);
+ final var caps2Hash = EntityCapabilities2.hash(infoQuery);
+ serviceDescriptions.put(capsHash, serviceDiscoveryFeatures);
+ serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures);
+ final var capabilities = new Capabilities();
+ capabilities.setHash(caps2Hash);
+ final var legacyCapabilities = new LegacyCapabilities();
+ legacyCapabilities.setNode(DiscoManager.CAPABILITY_NODE);
+ legacyCapabilities.setHash(capsHash);
+ final var presence = new Presence();
+ presence.addExtension(capabilities);
+ presence.addExtension(legacyCapabilities);
+ final String pgpSignature = account.getPgpSignature();
+ final String message = account.getPresenceStatusMessage();
+ final Presence.Availability availability;
+ if (appSettings.isUserManagedAvailability()) {
+ availability = account.getPresenceStatus();
+ } else {
+ availability = getTargetPresence();
+ }
+ presence.setAvailability(availability);
+ presence.setStatus(message);
+ if (pgpSignature != null) {
+ presence.addExtension(new Signed(pgpSignature));
+ }
+
+ final var lastActivity = service.getLastActivity();
+ if (lastActivity > 0 && withIdle) {
+ final long since =
+ Math.min(lastActivity, System.currentTimeMillis()); // don't send future dates
+ presence.addChild("idle", Namespace.IDLE)
+ .setAttribute("since", AbstractGenerator.getTimestamp(since));
+ }
+ Log.d(Config.LOGTAG, "--> " + presence);
+ connection.sendPresencePacket(presence);
+ }
+
+ public void unavailable() {
+ var presence = new Presence(Presence.Type.UNAVAILABLE);
+ this.connection.sendPresencePacket(presence);
+ }
+
+ public void available(final Jid to, final Extension... extensions) {
+ available(to, null, extensions);
+ }
+
+ public void available(final Jid to, final String message, final Extension... extensions) {
+ final var presence = new Presence();
+ presence.setTo(to);
+ presence.setStatus(message);
+ for (final var extension : extensions) {
+ presence.addExtension(extension);
+ }
+ connection.sendPresencePacket(presence);
+ }
+
+ public void unavailable(final Jid to) {
+ final var presence = new Presence(Presence.Type.UNAVAILABLE);
+ presence.setTo(to);
+ connection.sendPresencePacket(presence);
+ }
+
+ private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
+ final var device = new Device(context);
+ if (appSettings.isDndOnSilentMode()
+ && device.isPhoneSilenced(appSettings.isTreatVibrateAsSilent())) {
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
+ } else if (appSettings.isAwayWhenScreenLocked() && device.isScreenLocked()) {
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
+ } else {
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
+ }
}
public Presence getPresence(final Presence.Availability availability, final boolean personal) {
@@ -0,0 +1,68 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.storage.PrivateStorage;
+import java.util.Collection;
+import java.util.Map;
+
+public class PrivateStorageManager extends AbstractBookmarkManager {
+
+ public PrivateStorageManager(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ }
+
+ public void fetchBookmarks() {
+ final var iq = new Iq(Iq.Type.GET);
+ final var privateStorage = iq.addExtension(new PrivateStorage());
+ privateStorage.addExtension(new Storage());
+ final var future = this.connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Iq>() {
+ @Override
+ public void onSuccess(Iq result) {
+ final var privateStorage = result.getExtension(PrivateStorage.class);
+ if (privateStorage == null) {
+ return;
+ }
+ final var bookmarkStorage = privateStorage.getExtension(Storage.class);
+ final Map<Jid, Bookmark> bookmarks =
+ Bookmark.parseFromStorage(bookmarkStorage, getAccount());
+ processBookmarksInitial(bookmarks, false);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not fetch bookmark from private storage",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> publishBookmarks(Collection<Bookmark> bookmarks) {
+ final var iq = new Iq(Iq.Type.SET);
+ final var privateStorage = iq.addExtension(new PrivateStorage());
+ final var storage = privateStorage.addExtension(new Storage());
+ for (final var bookmark : bookmarks) {
+ storage.addChild(bookmark);
+ }
+ return Futures.transform(
+ connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,401 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.ExtensionFactory;
+import im.conversations.android.xmpp.IqErrorException;
+import im.conversations.android.xmpp.NodeConfiguration;
+import im.conversations.android.xmpp.PreconditionNotMetException;
+import im.conversations.android.xmpp.PubSubErrorException;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.pubsub.PubSub;
+import im.conversations.android.xmpp.model.pubsub.Publish;
+import im.conversations.android.xmpp.model.pubsub.PublishOptions;
+import im.conversations.android.xmpp.model.pubsub.Retract;
+import im.conversations.android.xmpp.model.pubsub.error.PubSubError;
+import im.conversations.android.xmpp.model.pubsub.event.Delete;
+import im.conversations.android.xmpp.model.pubsub.event.Event;
+import im.conversations.android.xmpp.model.pubsub.event.Purge;
+import im.conversations.android.xmpp.model.pubsub.owner.Configure;
+import im.conversations.android.xmpp.model.pubsub.owner.PubSubOwner;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.stanza.Message;
+import java.util.Map;
+
+public class PubSubManager extends AbstractManager {
+
+ private static final String SINGLETON_ITEM_ID = "current";
+
+ public PubSubManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void handleEvent(final Message message) {
+ final var event = message.getExtension(Event.class);
+ final var action = event.getAction();
+ final var from = message.getFrom();
+
+ if (from instanceof Jid.Invalid) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": ignoring event from invalid jid");
+ return;
+ }
+
+ if (action instanceof Purge purge) {
+ // purge is a deletion of all items in a node
+ handlePurge(message, purge);
+ } else if (action instanceof Items items) {
+ // the items wrapper contains, new and updated items as well as retractions which are
+ // deletions of individual items in a node
+ handleItems(message, items);
+ } else if (action instanceof Delete delete) {
+ // delete is the deletion of the node itself
+ handleDelete(message, delete);
+ }
+ }
+
+ public <T extends Extension> ListenableFuture<Map<String, T>> fetchItems(
+ final Jid address, final Class<T> clazz) {
+ final var id = ExtensionFactory.id(clazz);
+ if (id == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalArgumentException(
+ String.format("%s is not a registered extension", clazz.getName())));
+ }
+ return fetchItems(address, id.namespace, clazz);
+ }
+
+ public <T extends Extension> ListenableFuture<Map<String, T>> fetchItems(
+ final Jid address, final String node, final Class<T> clazz) {
+ final Iq request = new Iq(Iq.Type.GET);
+ request.setTo(address);
+ final var pubSub = request.addExtension(new PubSub());
+ final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
+ itemsWrapper.setNode(node);
+ return Futures.transform(
+ connection.sendIqPacket(request),
+ response -> {
+ final var pubSubResponse = response.getExtension(PubSub.class);
+ if (pubSubResponse == null) {
+ throw new IllegalStateException();
+ }
+ final var items = pubSubResponse.getItems();
+ if (items == null) {
+ throw new IllegalStateException();
+ }
+ return items.getItemMap(clazz);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchItem(
+ final Jid address, final String itemId, final Class<T> clazz) {
+ final var id = ExtensionFactory.id(clazz);
+ if (id == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalArgumentException(
+ String.format("%s is not a registered extension", clazz.getName())));
+ }
+ return fetchItem(address, id.namespace, itemId, clazz);
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchItem(
+ final Jid address, final String node, final String itemId, final Class<T> clazz) {
+ final Iq request = new Iq(Iq.Type.GET);
+ request.setTo(address);
+ final var pubSub = request.addExtension(new PubSub());
+ final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
+ itemsWrapper.setNode(node);
+ final var item = itemsWrapper.addExtension(new PubSub.Item());
+ item.setId(itemId);
+ return Futures.transform(
+ connection.sendIqPacket(request),
+ response -> {
+ final var pubSubResponse = response.getExtension(PubSub.class);
+ if (pubSubResponse == null) {
+ throw new IllegalStateException();
+ }
+ final var items = pubSubResponse.getItems();
+ if (items == null) {
+ throw new IllegalStateException();
+ }
+ return items.getItemOrThrow(itemId, clazz);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchMostRecentItem(
+ final Jid address, final Class<T> clazz) {
+ final var id = ExtensionFactory.id(clazz);
+ if (id == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalArgumentException(
+ String.format("%s is not a registered extension", clazz.getName())));
+ }
+ return fetchMostRecentItem(address, id.namespace, clazz);
+ }
+
+ public <T extends Extension> ListenableFuture<T> fetchMostRecentItem(
+ final Jid address, final String node, final Class<T> clazz) {
+ final Iq request = new Iq(Iq.Type.GET);
+ request.setTo(address);
+ final var pubSub = request.addExtension(new PubSub());
+ final var itemsWrapper = pubSub.addExtension(new PubSub.ItemsWrapper());
+ itemsWrapper.setNode(node);
+ itemsWrapper.setMaxItems(1);
+ return Futures.transform(
+ connection.sendIqPacket(request),
+ response -> {
+ final var pubSubResponse = response.getExtension(PubSub.class);
+ if (pubSubResponse == null) {
+ throw new IllegalStateException();
+ }
+ final var items = pubSubResponse.getItems();
+ if (items == null) {
+ throw new IllegalStateException();
+ }
+ return items.getOnlyItem(clazz);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void handleItems(final Message message, final Items items) {
+ final var from = message.getFrom();
+ final var isFromBare = from == null || from.isBareJid();
+ final var node = items.getNode();
+ if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
+ getManager(NativeBookmarkManager.class).handleItems(items);
+ return;
+ }
+ if (connection.fromAccount(message) && Namespace.BOOKMARKS.equals(node)) {
+ getManager(LegacyBookmarkManager.class).handleItems(items);
+ return;
+ }
+ if (connection.fromAccount(message) && Namespace.MDS_DISPLAYED.equals(node)) {
+ getManager(MessageDisplayedSynchronizationManager.class).handleItems(items);
+ return;
+ }
+ if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) {
+ getManager(AvatarManager.class).handleItems(from, items);
+ return;
+ }
+ if (isFromBare && Namespace.NICK.equals(node)) {
+ getManager(NickManager.class).handleItems(from, items);
+ return;
+ }
+ if (isFromBare && Namespace.AXOLOTL_DEVICE_LIST.equals(node)) {
+ getManager(AxolotlManager.class).handleItems(from, items);
+ }
+ }
+
+ private void handlePurge(final Message message, final Purge purge) {
+ final var from = message.getFrom();
+ final var isFromBare = from == null || from.isBareJid();
+ final var node = purge.getNode();
+ if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
+ getManager(NativeBookmarkManager.class).handlePurge();
+ }
+ if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) {
+ // purge (delete all items in a node) is functionally equivalent to delete
+ getManager(AvatarManager.class).handleDelete(from);
+ }
+ }
+
+ private void handleDelete(final Message message, final Delete delete) {
+ final var from = message.getFrom();
+ final var isFromBare = from == null || from.isBareJid();
+ final var node = delete.getNode();
+ if (connection.fromAccount(message) && Namespace.BOOKMARKS2.equals(node)) {
+ getManager(NativeBookmarkManager.class).handleDelete();
+ return;
+ }
+ if (isFromBare && Namespace.AVATAR_METADATA.equals(node)) {
+ getManager(AvatarManager.class).handleDelete(from);
+ return;
+ }
+ if (isFromBare && Namespace.NICK.equals(node)) {
+ getManager(NickManager.class).handleDelete(from);
+ }
+ }
+
+ public ListenableFuture<Void> publishSingleton(
+ Jid address, Extension item, final NodeConfiguration nodeConfiguration) {
+ final var id = ExtensionFactory.id(item.getClass());
+ return publish(address, item, SINGLETON_ITEM_ID, id.namespace, nodeConfiguration);
+ }
+
+ public ListenableFuture<Void> publishSingleton(
+ Jid address,
+ Extension item,
+ final String node,
+ final NodeConfiguration nodeConfiguration) {
+ return publish(address, item, SINGLETON_ITEM_ID, node, nodeConfiguration);
+ }
+
+ public ListenableFuture<Void> publish(
+ Jid address,
+ Extension item,
+ final String itemId,
+ final NodeConfiguration nodeConfiguration) {
+ final var id = ExtensionFactory.id(item.getClass());
+ return publish(address, item, itemId, id.namespace, nodeConfiguration);
+ }
+
+ public ListenableFuture<Void> publish(
+ final Jid address,
+ final Extension itemPayload,
+ final String itemId,
+ final String node,
+ final NodeConfiguration nodeConfiguration) {
+ final var future = publishNoRetry(address, itemPayload, itemId, node, nodeConfiguration);
+ return Futures.catchingAsync(
+ future,
+ PreconditionNotMetException.class,
+ ex -> {
+ Log.d(
+ Config.LOGTAG,
+ "Node " + node + " on " + address + " requires reconfiguration");
+ final var reconfigurationFuture =
+ reconfigureNode(address, node, nodeConfiguration);
+ return Futures.transformAsync(
+ reconfigurationFuture,
+ ignored ->
+ publishNoRetry(
+ address, itemPayload, itemId, node, nodeConfiguration),
+ MoreExecutors.directExecutor());
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> publishNoRetry(
+ final Jid address,
+ final Extension itemPayload,
+ final String itemId,
+ final String node,
+ final NodeConfiguration nodeConfiguration) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var pubSub = iq.addExtension(new PubSub());
+ final var publish = pubSub.addExtension(new Publish());
+ publish.setNode(node);
+ final var item = publish.addExtension(new PubSub.Item());
+ item.setId(itemId);
+ item.addExtension(itemPayload);
+ pubSub.addExtension(PublishOptions.of(nodeConfiguration));
+ final ListenableFuture<Void> iqFuture =
+ Futures.transform(
+ connection.sendIqPacket(iq),
+ result -> null,
+ MoreExecutors.directExecutor());
+ return Futures.catchingAsync(
+ iqFuture,
+ IqErrorException.class,
+ new PubSubExceptionTransformer<>(),
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> reconfigureNode(
+ final Jid address, final String node, final NodeConfiguration nodeConfiguration) {
+ return Futures.transformAsync(
+ getNodeConfiguration(address, node),
+ data -> setNodeConfiguration(address, node, data.submit(nodeConfiguration)),
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Data> getNodeConfiguration(final Jid address, final String node) {
+ final Iq iq = new Iq(Iq.Type.GET);
+ iq.setTo(address);
+ final var pubSub = iq.addExtension(new PubSubOwner());
+ final var configure = pubSub.addExtension(new Configure());
+ configure.setNode(node);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ result -> {
+ final var pubSubOwnerResult = result.getExtension(PubSubOwner.class);
+ final Configure configureResult =
+ pubSubOwnerResult == null
+ ? null
+ : pubSubOwnerResult.getExtension(Configure.class);
+ if (configureResult == null) {
+ throw new IllegalStateException(
+ "No configuration found in configuration request result");
+ }
+ return configureResult.getData();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Void> setNodeConfiguration(
+ final Jid address, final String node, final Data data) {
+ final Iq iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var pubSub = iq.addExtension(new PubSubOwner());
+ final var configure = pubSub.addExtension(new Configure());
+ configure.setNode(node);
+ configure.addExtension(data);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ result -> {
+ Log.d(Config.LOGTAG, "Modified node configuration " + node + " on " + address);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Iq> retract(final Jid address, final String itemId, final String node) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var pubSub = iq.addExtension(new PubSub());
+ final var retract = pubSub.addExtension(new Retract());
+ retract.setNode(node);
+ retract.setNotify(true);
+ final var item = retract.addExtension(new PubSub.Item());
+ item.setId(itemId);
+ return connection.sendIqPacket(iq);
+ }
+
+ public ListenableFuture<Iq> delete(final Jid address, final String node) {
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(address);
+ final var pubSub = iq.addExtension(new PubSubOwner());
+ final var delete =
+ pubSub.addExtension(new im.conversations.android.xmpp.model.pubsub.owner.Delete());
+ delete.setNode(node);
+ return connection.sendIqPacket(iq);
+ }
+
+ private static class PubSubExceptionTransformer<V>
+ implements AsyncFunction<IqErrorException, V> {
+
+ @Override
+ @NonNull
+ public ListenableFuture<V> apply(@NonNull IqErrorException ex) {
+ final var error = ex.getError();
+ if (error == null) {
+ return Futures.immediateFailedFuture(ex);
+ }
+ final PubSubError pubSubError = error.getExtension(PubSubError.class);
+ if (pubSubError instanceof PubSubError.PreconditionNotMet) {
+ return Futures.immediateFailedFuture(
+ new PreconditionNotMetException(ex.getResponse()));
+ } else if (pubSubError != null) {
+ return Futures.immediateFailedFuture(new PubSubErrorException(ex.getResponse()));
+ } else {
+ return Futures.immediateFailedFuture(ex);
+ }
+ }
+ }
+}
@@ -0,0 +1,314 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import android.util.Patterns;
+import androidx.annotation.NonNull;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.IqErrorException;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.oob.OutOfBandData;
+import im.conversations.android.xmpp.model.pars.PreAuth;
+import im.conversations.android.xmpp.model.register.Instructions;
+import im.conversations.android.xmpp.model.register.Password;
+import im.conversations.android.xmpp.model.register.Register;
+import im.conversations.android.xmpp.model.register.Remove;
+import im.conversations.android.xmpp.model.register.Username;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import okhttp3.HttpUrl;
+
+public class RegistrationManager extends AbstractManager {
+
+ public RegistrationManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public ListenableFuture<Void> setPassword(final String password) {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(account.getJid().getDomain());
+ final var register = iq.addExtension(new Register());
+ register.addUsername(account.getJid().getLocal());
+ register.addPassword(password);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ r -> {
+ account.setPassword(password);
+ account.setOption(Account.OPTION_MAGIC_CREATE, false);
+ getDatabase().updateAccount(account);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> register() {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(account.getJid().getDomain());
+ final var register = iq.addExtension(new Register());
+ register.addUsername(account.getJid().getLocal());
+ register.addPassword(account.getPassword());
+ final ListenableFuture<Void> future =
+ Futures.transform(
+ connection.sendIqPacket(iq, true),
+ result -> null,
+ MoreExecutors.directExecutor());
+ return Futures.catchingAsync(
+ future,
+ IqErrorException.class,
+ ex ->
+ Futures.immediateFailedFuture(
+ new RegistrationFailedException(ex.getResponse())),
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> register(final Data data, final String ocr) {
+ final var account = getAccount();
+ final var submission =
+ data.submit(
+ ImmutableMap.of(
+ "username",
+ account.getJid().getLocal(),
+ "password",
+ account.getPassword(),
+ "ocr",
+ ocr));
+ final var iq = new Iq(Iq.Type.SET);
+ final var register = iq.addExtension(new Register());
+ register.addExtension(submission);
+ final ListenableFuture<Void> future =
+ Futures.transform(
+ connection.sendIqPacket(iq, true),
+ result -> null,
+ MoreExecutors.directExecutor());
+ return Futures.catchingAsync(
+ future,
+ IqErrorException.class,
+ ex ->
+ Futures.immediateFailedFuture(
+ new RegistrationFailedException(ex.getResponse())),
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> unregister() {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(account.getJid().getDomain());
+ final var register = iq.addExtension(new Register());
+ register.addExtension(new Remove());
+ return Futures.transform(
+ connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Registration> getRegistration() {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.GET);
+ iq.setTo(account.getDomain());
+ iq.addExtension(new Register());
+ final var future = connection.sendIqPacket(iq, true);
+ return Futures.transformAsync(
+ future,
+ result -> {
+ final var register = result.getExtension(Register.class);
+ if (register == null) {
+ throw new IllegalStateException(
+ "Server did not include register in response");
+ }
+ if (register.hasExtension(Username.class)
+ && register.hasExtension(Password.class)) {
+ return Futures.immediateFuture(new SimpleRegistration());
+ }
+
+ // find bits of binary and get captcha from there
+
+ final var data = register.getExtension(Data.class);
+ // note that the captcha namespace is incorrect here. That namespace is only
+ // used in message challenges. ejabberd uses the incorrect namespace though
+ if (data != null
+ && Arrays.asList(Namespace.REGISTER, Namespace.CAPTCHA)
+ .contains(data.getFormType())) {
+ return getExtendedRegistration(register, data);
+ }
+ final var oob = register.getExtension(OutOfBandData.class);
+ final var instructions = register.getExtension(Instructions.class);
+ final String instructionsText =
+ instructions == null ? null : instructions.getContent();
+ final String redirectUrl = oob == null ? null : oob.getURL();
+ if (redirectUrl != null) {
+ return Futures.immediateFuture(RedirectRegistration.ifValid(redirectUrl));
+ }
+ if (instructionsText != null) {
+ final Matcher matcher = Patterns.WEB_URL.matcher(instructionsText);
+ if (matcher.find()) {
+ final String instructionsUrl =
+ instructionsText.substring(matcher.start(), matcher.end());
+ return Futures.immediateFuture(
+ RedirectRegistration.ifValid(instructionsUrl));
+ }
+ }
+ throw new IllegalStateException("No supported registration method found");
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Registration> getExtendedRegistration(
+ final Register register, final Data data) {
+ final var ocr = data.getFieldByName("ocr");
+ if (ocr == null) {
+ throw new IllegalArgumentException("Missing OCR form field");
+ }
+ final var ocrMedia = ocr.getMedia();
+ if (ocrMedia == null) {
+ throw new IllegalArgumentException("OCR form field missing media");
+ }
+ final var uris = ocrMedia.getUris();
+ final var bobUri = Iterables.find(uris, u -> "cid".equals(u.getScheme()), null);
+ final Optional<im.conversations.android.xmpp.model.bob.Data> bob;
+ if (bobUri != null) {
+ bob = im.conversations.android.xmpp.model.bob.Data.get(register, bobUri.getPath());
+ } else {
+ bob = Optional.absent();
+ }
+ if (bob.isPresent()) {
+ final var bytes = bob.get().asBytes();
+ final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ return Futures.immediateFuture(new ExtendedRegistration(bitmap, data));
+ }
+ final var captchaFallbackUrl = data.getValue("captcha-fallback-url");
+ if (captchaFallbackUrl == null) {
+ throw new IllegalStateException("No captcha fallback URL provided");
+ }
+ final var captchFallbackHttpUrl = HttpUrl.parse(captchaFallbackUrl);
+ Log.d(Config.LOGTAG, "fallback url: " + captchFallbackHttpUrl);
+ throw new IllegalStateException("Not implemented");
+ }
+
+ public ListenableFuture<Registration> getRegistration(final String token) {
+ final var preAuthentication = sendPreAuthentication(token);
+ final var caught =
+ Futures.catchingAsync(
+ preAuthentication,
+ IqErrorException.class,
+ ex -> {
+ final var error = ex.getError();
+ final var condition = error == null ? null : error.getCondition();
+ if (condition instanceof Condition.ItemNotFound) {
+ return Futures.immediateFailedFuture(
+ new InvalidTokenException(ex.getResponse()));
+ } else {
+ return Futures.immediateFuture(ex);
+ }
+ },
+ MoreExecutors.directExecutor());
+ return Futures.transformAsync(
+ caught, v -> getRegistration(), MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> sendPreAuthentication(final String token) {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.GET);
+ iq.setTo(account.getJid().getDomain());
+ final var preAuthentication = iq.addExtension(new PreAuth());
+ preAuthentication.setToken(token);
+ final var future = connection.sendIqPacket(iq, true);
+ return Futures.transform(future, result -> null, MoreExecutors.directExecutor());
+ }
+
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class).hasServerFeature(Namespace.REGISTER);
+ }
+
+ public abstract static class Registration {}
+
+ // only requires Username + Password
+ public static class SimpleRegistration extends Registration {}
+
+ // Captcha as shown here: https://xmpp.org/extensions/xep-0158.html#register
+ public static class ExtendedRegistration extends Registration {
+ private final Bitmap captcha;
+ private final Data data;
+
+ public ExtendedRegistration(final Bitmap captcha, final Data data) {
+ this.captcha = captcha;
+ this.data = data;
+ }
+
+ public Bitmap getCaptcha() {
+ return this.captcha;
+ }
+
+ public Data getData() {
+ return this.data;
+ }
+ }
+
+ // Redirection as show here: https://xmpp.org/extensions/xep-0077.html#redirect
+ public static class RedirectRegistration extends Registration {
+ private final HttpUrl url;
+
+ private RedirectRegistration(@NonNull HttpUrl url) {
+ this.url = url;
+ }
+
+ public @NonNull HttpUrl getURL() {
+ return this.url;
+ }
+
+ public static RedirectRegistration ifValid(final String url) {
+ final HttpUrl httpUrl = HttpUrl.parse(url);
+ if (httpUrl != null && httpUrl.isHttps()) {
+ return new RedirectRegistration(httpUrl);
+ }
+ throw new IllegalStateException(
+ "A URL found the registration instructions is not valid");
+ }
+ }
+
+ public static class InvalidTokenException extends IqErrorException {
+
+ public InvalidTokenException(final Iq response) {
+ super(response);
+ }
+ }
+
+ public static class RegistrationFailedException extends IqErrorException {
+
+ private final List<String> PASSWORD_TOO_WEAK_MESSAGES =
+ Arrays.asList("The password is too weak", "Please use a longer password.");
+
+ public RegistrationFailedException(final Iq response) {
+ super(response);
+ }
+
+ public Account.State asAccountState() {
+ final var error = getError();
+ final var condition = error == null ? null : error.getCondition();
+ if (condition instanceof Condition.Conflict) {
+ return Account.State.REGISTRATION_CONFLICT;
+ } else if (condition instanceof Condition.ResourceConstraint) {
+ return Account.State.REGISTRATION_PLEASE_WAIT;
+ } else if (condition instanceof Condition.NotAcceptable
+ && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
+ return Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
+ } else {
+ return Account.State.REGISTRATION_FAILED;
+ }
+ }
+ }
+}
@@ -0,0 +1,377 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.AbstractPhoneContact;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.roster.Item;
+import im.conversations.android.xmpp.model.roster.Query;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class RosterManager extends AbstractManager implements Roster {
+
+ private final ReplacingSerialSingleThreadExecutor dbExecutor =
+ new ReplacingSerialSingleThreadExecutor(RosterManager.class.getName());
+
+ private final Map<Jid, Contact> contacts = new HashMap<>();
+ private String version;
+
+ private final XmppConnectionService service;
+
+ public RosterManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.version = getAccount().getRosterVersion();
+ ;
+ this.service = service;
+ }
+
+ public void request() {
+ final var iq = new Iq(Iq.Type.GET);
+ final var query = iq.addExtension(new Query());
+ final var version = this.version;
+ if (version != null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": requesting roster version " + version);
+ query.setVersion(version);
+ } else {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + " requesting roster");
+ }
+ final var future = connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ final var query = result.getExtension(Query.class);
+ if (query == null) {
+ // No query in result means further modifications are sent via pushes
+ return;
+ }
+ final var version = query.getVersion();
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": received full roster (version="
+ + version
+ + ")");
+ final var items = query.getItems();
+ // In a roster result (Section 2.1.4), the client MUST ignore values of the
+ // 'subscription'
+ // attribute other than "none", "to", "from", or "both".
+ final var validItems =
+ Collections2.filter(
+ items,
+ i ->
+ Item.RESULT_SUBSCRIPTIONS.contains(
+ i.getSubscription())
+ && Objects.nonNull(i.getJid()));
+
+ setRosterItems(version, validItems);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": could not fetch roster",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setRosterItems(final String version, final Collection<Item> items) {
+ synchronized (this.contacts) {
+ markAllAsNotInRoster();
+ for (final var item : items) {
+ processRosterItem(item);
+ }
+ this.version = version;
+ }
+ this.triggerUiUpdates();
+ this.writeToDatabaseAsync();
+ }
+
+ private void modifyRosterItems(final String version, final Collection<Item> items) {
+ synchronized (this.contacts) {
+ for (final var item : items) {
+ processRosterItem(item);
+ }
+ this.version = version;
+ }
+ this.triggerUiUpdates();
+ this.writeToDatabaseAsync();
+ }
+
+ private void triggerUiUpdates() {
+ this.service.updateConversationUi();
+ this.service.updateRosterUi(XmppConnectionService.UpdateRosterReason.PUSH);
+ this.service.getShortcutService().refresh();
+ }
+
+ public void push(final Iq packet) {
+ if (connection.fromServer(packet)) {
+ final var query = packet.getExtension(Query.class);
+ final var version = query.getVersion();
+ modifyRosterItems(version, query.getItems());
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid() + ": received roster push (version=" + version + ")");
+ } else {
+ connection.sendErrorFor(packet, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ private void processRosterItem(final Item item) {
+ // this is verbatim the original code from IqParser.
+ // TODO there are likely better ways to handle roster management
+ final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
+ if (jid == null) {
+ return;
+ }
+ final var name = item.getItemName();
+ final var subscription = item.getSubscription();
+ // getContactInternal is not synchronized because all access to processRosterItem is
+ final var contact = getContactInternal(jid);
+ boolean bothPre =
+ contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
+ if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ contact.setServerName(name);
+ contact.parseGroupsFromElement(item);
+ }
+ if (subscription == Item.Subscription.REMOVE) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else {
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ // TODO use subscription; and set asking separately
+ contact.parseSubscriptionFromElement(item);
+ }
+ boolean both =
+ contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
+ if ((both != bothPre) && both) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": gained mutual presence subscription with "
+ + contact.getJid());
+ final var axolotlService = account.getAxolotlService();
+ if (axolotlService != null) {
+ axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
+ }
+ }
+ service.getAvatarService().clear(contact);
+ }
+
+ @Override
+ @NonNull
+ public Contact getContact(@NonNull final Jid jid) {
+ synchronized (this.contacts) {
+ return this.getContactInternal(jid);
+ }
+ }
+
+ @NonNull
+ public Contact getContactInternal(@NonNull final Jid jid) {
+ final var existing = this.contacts.get(jid.asBareJid());
+ if (existing != null) {
+ return existing;
+ }
+ final var contact = new Contact(jid.asBareJid());
+ contact.setAccount(getAccount());
+ this.contacts.put(jid.asBareJid(), contact);
+ return contact;
+ }
+
+ @Override
+ @Nullable
+ public Contact getContactFromContactList(@NonNull final Jid jid) {
+ synchronized (this.contacts) {
+ final var contact = this.contacts.get(jid.asBareJid());
+ if (contact != null && contact.showInContactList()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public List<Contact> getContacts() {
+ synchronized (this.contacts) {
+ return ImmutableList.copyOf(this.contacts.values());
+ }
+ }
+
+ @Override
+ public ImmutableList<Contact> getWithSystemAccounts(
+ final Class<? extends AbstractPhoneContact> clazz) {
+ final int option = Contact.getOption(clazz);
+ synchronized (this.contacts) {
+ return ImmutableList.copyOf(
+ Collections2.filter(this.contacts.values(), c -> c.getOption(option)));
+ }
+ }
+
+ public void clearPresences() {
+ synchronized (this.contacts) {
+ for (final var contact : this.contacts.values()) {
+ contact.clearPresences();
+ }
+ }
+ }
+
+ private void markAllAsNotInRoster() {
+ for (final var contact : this.contacts.values()) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public void restore() {
+ synchronized (this.contacts) {
+ this.contacts.clear();
+ this.contacts.putAll(getDatabase().readRoster(getAccount()));
+ }
+ }
+
+ public void writeToDatabaseAsync() {
+ this.dbExecutor.execute(this::writeToDatabase);
+ }
+
+ public void writeToDatabase() {
+ final var account = getAccount();
+ final List<Contact> contacts;
+ final String version;
+ synchronized (this.contacts) {
+ contacts = ImmutableList.copyOf(this.contacts.values());
+ version = this.version;
+ }
+ getDatabase().writeRoster(account, version, contacts);
+ context.unregisterPhoneAccounts(account);
+ try { Thread.sleep(500); } catch (InterruptedException e) { }
+ }
+
+ public void syncDirtyContacts() {
+ synchronized (this.contacts) {
+ for (final var contact : this.contacts.values()) {
+ if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ addRosterItem(contact, null);
+ }
+ if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+ deleteRosterItem(contact);
+ }
+ }
+ }
+ }
+
+ public void addRosterItem(final Contact contact, final String preAuth) {
+ final var address = contact.getJid().asBareJid();
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.setOption(Contact.Options.DIRTY_PUSH);
+ // sync the 'dirty push' flag to disk in case we are offline
+ this.writeToDatabaseAsync();
+ final boolean ask = contact.getOption(Contact.Options.ASKING);
+ final boolean sendUpdates =
+ contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+ && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+ final Iq iq = new Iq(Iq.Type.SET);
+ final var query = iq.addExtension(new Query());
+ final var item = query.addExtension(new Item());
+ item.setJid(address);
+ final var serverName = contact.getServerName();
+ if (serverName != null) {
+ item.setItemName(serverName);
+ }
+ item.setGroups(contact.getGroups(false));
+ final var future = this.connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Iq>() {
+ @Override
+ public void onSuccess(Iq result) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": pushed roster item "
+ + address);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not push roster item "
+ + address,
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ if (sendUpdates) {
+ getManager(PresenceManager.class).subscribed(contact.getJid().asBareJid());
+ }
+ if (ask) {
+ getManager(PresenceManager.class).subscribe(contact.getJid().asBareJid(), preAuth);
+ }
+ }
+
+ public void deleteRosterItem(final Contact contact) {
+ final var address = contact.getJid().asBareJid();
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.setOption(Contact.Options.DIRTY_DELETE);
+ this.writeToDatabaseAsync();
+ final Iq iq = new Iq(Iq.Type.SET);
+ final var query = iq.addExtension(new Query());
+ final var item = query.addExtension(new Item());
+ item.setJid(address);
+ item.setSubscription(Item.Subscription.REMOVE);
+ final var future = this.connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<Iq>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": removed roster item "
+ + address);
+ }
+
+ @Override
+ public void onFailure(final @NonNull Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not remove roster item "
+ + address,
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,69 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+import im.conversations.android.xmpp.model.socks5.Query;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.UUID;
+
+public class StreamHostManager extends AbstractManager {
+
+ public StreamHostManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public ListenableFuture<SocksByteStreamsTransport.Candidate> getProxyCandidate(
+ final boolean asInitiator) {
+ if (Config.DISABLE_PROXY_LOOKUP) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("Proxy look up is disabled"));
+ }
+ final var streamer =
+ getManager(DiscoManager.class).findDiscoItemByFeature(Namespace.BYTE_STREAMS);
+ if (streamer == null) {
+ return Futures.immediateFailedFuture(
+ new IllegalStateException("No proxy/streamer found"));
+ }
+ return getProxyCandidate(asInitiator, streamer.getKey());
+ }
+
+ private ListenableFuture<SocksByteStreamsTransport.Candidate> getProxyCandidate(
+ final boolean asInitiator, final Jid streamer) {
+ final var iq = new Iq(Iq.Type.GET, new Query());
+ iq.setTo(streamer);
+ return Futures.transform(
+ connection.sendIqPacket(iq),
+ response -> {
+ final var query = response.getExtension(Query.class);
+ if (query == null) {
+ throw new IllegalStateException("No stream host query found in response");
+ }
+ final var streamHost = query.getStreamHost();
+ if (streamHost == null) {
+ throw new IllegalStateException("no stream host found in query");
+ }
+ final var jid = streamHost.getJid();
+ final var host = streamHost.getHost();
+ final var port = streamHost.getPort();
+ if (jid == null || host == null || port == null) {
+ throw new IllegalStateException("StreamHost had incomplete information");
+ }
+ return new SocksByteStreamsTransport.Candidate(
+ UUID.randomUUID().toString(),
+ host,
+ streamer,
+ port,
+ 655360 + (asInitiator ? 0 : 15),
+ SocksByteStreamsTransport.CandidateType.PROXY);
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.up.Push;
+
+public class UnifiedPushManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public UnifiedPushManager(
+ final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public void push(final Iq packet) {
+ final Jid transport = packet.getFrom();
+ final var push = packet.getOnlyExtension(Push.class);
+ if (push == null || transport == null) {
+ connection.sendErrorFor(packet, Error.Type.MODIFY, new Condition.BadRequest());
+ return;
+ }
+ if (service.processUnifiedPushMessage(getAccount(), transport, push)) {
+ connection.sendResultFor(packet);
+ } else {
+ connection.sendErrorFor(packet, Error.Type.CANCEL, new Condition.ItemNotFound());
+ }
+ }
+}
@@ -0,0 +1,155 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Log;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+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.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.IqErrorException;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.vcard.BinaryValue;
+import im.conversations.android.xmpp.model.vcard.Photo;
+import im.conversations.android.xmpp.model.vcard.VCard;
+import java.time.Duration;
+import java.util.Objects;
+
+public class VCardManager extends AbstractManager {
+
+ private final Cache<Jid, Exception> photoExceptionCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(24_576)
+ .expireAfterWrite(Duration.ofHours(36))
+ .build();
+
+ public VCardManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public ListenableFuture<VCard> retrieve(final Jid address) {
+ final var iq = new Iq(Iq.Type.GET, new VCard());
+ iq.setTo(address);
+ return Futures.transform(
+ this.connection.sendIqPacket(iq),
+ result -> {
+ final var vCard = result.getExtension(VCard.class);
+ if (vCard == null) {
+ throw new IllegalStateException("Result did not include vCard");
+ }
+ return vCard;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<byte[]> retrievePhotoCacheException(final Jid address) {
+ final var existingException = this.photoExceptionCache.getIfPresent(address);
+ if (existingException != null) {
+ return Futures.immediateFailedFuture(existingException);
+ }
+ final var future = retrievePhoto(address);
+ return Futures.catchingAsync(
+ future,
+ Exception.class,
+ ex -> {
+ if (ex instanceof IllegalStateException || ex instanceof IqErrorException) {
+ photoExceptionCache.put(address, ex);
+ }
+ return Futures.immediateFailedFuture(ex);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<byte[]> retrievePhoto(final Jid address) {
+ final var vCardFuture = retrieve(address);
+ return Futures.transform(
+ vCardFuture,
+ vCard -> {
+ final var photo = vCard.getPhoto();
+ if (photo == null) {
+ throw new IllegalStateException(
+ String.format("No photo in vCard of %s", address));
+ }
+ final var binaryValue = photo.getBinaryValue();
+ if (binaryValue == null) {
+ throw new IllegalStateException(
+ String.format("Photo has no binary value in vCard of %s", address));
+ }
+ return binaryValue.asBytes();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> publish(final VCard vCard) {
+ return publish(getAccount().getJid().asBareJid(), vCard);
+ }
+
+ public ListenableFuture<Void> publish(final Jid address, final VCard vCard) {
+ final var iq = new Iq(Iq.Type.SET, vCard);
+ iq.setTo(address);
+ return Futures.transform(
+ connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> deletePhoto() {
+ final var vCardFuture = retrieve(getAccount().getJid().asBareJid());
+ return Futures.transformAsync(
+ vCardFuture,
+ vCard -> {
+ final var photo = vCard.getPhoto();
+ if (photo == null) {
+ return Futures.immediateFuture(null);
+ }
+ Log.d(
+ Config.LOGTAG,
+ "deleting photo from vCard. binaryValue="
+ + Objects.nonNull(photo.getBinaryValue()));
+ photo.clearChildren();
+ return publish(vCard);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> publishPhoto(
+ final Jid address, final String type, final byte[] image) {
+ final var retrieveFuture = this.retrieve(address);
+
+ final var caughtFuture =
+ Futures.catchingAsync(
+ retrieveFuture,
+ IqErrorException.class,
+ ex -> {
+ final var error = ex.getError();
+ if (error != null
+ && error.getCondition() instanceof Condition.ItemNotFound) {
+ return Futures.immediateFuture(null);
+ } else {
+ return Futures.immediateFailedFuture(ex);
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ return Futures.transformAsync(
+ caughtFuture,
+ existing -> {
+ final VCard vCard;
+ if (existing == null) {
+ Log.d(Config.LOGTAG, "item-not-found. created fresh vCard");
+ vCard = new VCard();
+ } else {
+ vCard = existing;
+ }
+ final var photo = new Photo();
+ photo.setType(type);
+ photo.addExtension(new BinaryValue()).setContent(image);
+ vCard.setExtension(photo);
+ return publish(address, vCard);
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -8,11 +8,29 @@ import io.ipfs.cid.Cid;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.Element;
+import androidx.annotation.NonNull;
+import com.google.common.base.MoreObjects;
import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.avatar.Metadata;
+import okhttp3.HttpUrl;
public class Avatar {
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("type", type)
+ .add("sha1sum", sha1sum)
+ .add("url", url)
+ .add("image", image)
+ .add("height", height)
+ .add("width", width)
+ .add("size", size)
+ .add("owner", owner)
+ .add("origin", origin)
+ .toString();
+ }
+
public enum Origin {
PEP,
VCARD
@@ -20,6 +38,7 @@ public class Avatar {
public String type;
public String sha1sum;
+ public HttpUrl url;
public String image;
public int height;
public int width;
@@ -35,42 +54,6 @@ public class Avatar {
return sha1sum;
}
- public static Avatar parseMetadata(final String primaryId, final Metadata metadata) {
- if (primaryId == null || metadata == null) {
- return null;
- }
- for (Element child : metadata.getChildren()) {
- if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) {
- Avatar avatar = new Avatar();
- String height = child.getAttribute("height");
- String width = child.getAttribute("width");
- String size = child.getAttribute("bytes");
- try {
- if (height != null) {
- avatar.height = Integer.parseInt(height);
- }
- if (width != null) {
- avatar.width = Integer.parseInt(width);
- }
- if (size != null) {
- avatar.size = Long.parseLong(size);
- }
- } catch (NumberFormatException e) {
- return null;
- }
- avatar.type = child.getAttribute("type");
- String hash = child.getAttribute("id");
- if (!isValidSHA1(hash)) {
- return null;
- }
- avatar.sha1sum = hash;
- avatar.origin = Origin.PEP;
- return avatar;
- }
- }
- return null;
- }
-
@Override
public boolean equals(Object object) {
if (object != null && object instanceof Avatar other) {
@@ -80,31 +63,13 @@ public class Avatar {
}
}
- public Cid cid() {
- if (sha1sum == null) return null;
-
- try {
- return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1sum), "sha-1");
- } catch (final NoSuchAlgorithmException e) {
- return null;
- }
- }
+ public Cid cid() {
+ if (sha1sum == null) return null;
- public static Avatar parsePresence(Element x) {
- String hash = x == null ? null : x.findChildContent("photo");
- if (hash == null) {
- return null;
- }
- if (!isValidSHA1(hash)) {
- return null;
- }
- Avatar avatar = new Avatar();
- avatar.sha1sum = hash;
- avatar.origin = Origin.VCARD;
- return avatar;
- }
-
- private static boolean isValidSHA1(String s) {
- return s != null && s.matches("[a-fA-F0-9]{40}");
+ try {
+ return CryptoHelper.cid(CryptoHelper.hexToBytes(sha1sum), "sha-1");
+ } catch (final NoSuchAlgorithmException e) {
+ return null;
+ }
}
}
@@ -23,26 +23,6 @@ public class PublishOptions {
return options;
}
- public static Bundle persistentWhitelistAccess() {
- final Bundle options = new Bundle();
- options.putString("pubsub#persist_items", "true");
- options.putString("pubsub#access_model", "whitelist");
- return options;
- }
-
- public static Bundle persistentWhitelistAccessMaxItems() {
- final Bundle options = new Bundle();
- options.putString("pubsub#persist_items", "true");
- options.putString("pubsub#access_model", "whitelist");
- options.putString("pubsub#send_last_published_item", "never");
- options.putString("pubsub#max_items", "max");
- options.putString("pubsub#notify_delete", "true");
- options.putString(
- "pubsub#notify_retract", "true"); // one could also set notify=true on the retract
-
- return options;
- }
-
public static boolean preconditionNotMet(Iq response) {
final Element error =
response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
@@ -10,7 +10,7 @@ import java.util.Set;
public class NodeConfiguration implements Map<String, Object> {
private static final String PERSIST_ITEMS = "pubsub#persist_items";
- private static final String ACCESS_MODEL = "pubsub#access_model";
+ public static final String ACCESS_MODEL = "pubsub#access_model";
private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item";
private static final String MAX_ITEMS = "pubsub#max_items";
private static final String NOTIFY_DELETE = "pubsub#notify_delete";
@@ -20,19 +20,25 @@ public class NodeConfiguration implements Map<String, Object> {
new NodeConfiguration(
new ImmutableMap.Builder<String, Object>()
.put(PERSIST_ITEMS, Boolean.TRUE)
- .put(ACCESS_MODEL, "open")
+ .put(ACCESS_MODEL, AccessModel.OPEN)
.build());
public static final NodeConfiguration PRESENCE =
new NodeConfiguration(
new ImmutableMap.Builder<String, Object>()
.put(PERSIST_ITEMS, Boolean.TRUE)
- .put(ACCESS_MODEL, "presence")
+ .put(ACCESS_MODEL, AccessModel.PRESENCE)
+ .build());
+ public static final NodeConfiguration WHITELIST =
+ new NodeConfiguration(
+ new ImmutableMap.Builder<String, Object>()
+ .put(PERSIST_ITEMS, Boolean.TRUE)
+ .put(ACCESS_MODEL, AccessModel.WHITELIST)
.build());
public static final NodeConfiguration WHITELIST_MAX_ITEMS =
new NodeConfiguration(
new ImmutableMap.Builder<String, Object>()
.put(PERSIST_ITEMS, Boolean.TRUE)
- .put(ACCESS_MODEL, "whitelist")
+ .put(ACCESS_MODEL, AccessModel.WHITELIST)
.put(SEND_LAST_PUBLISHED_ITEM, "never")
.put(MAX_ITEMS, "max")
.put(NOTIFY_DELETE, Boolean.TRUE)
@@ -109,4 +115,10 @@ public class NodeConfiguration implements Map<String, Object> {
public Set<Entry<String, Object>> entrySet() {
return this.delegate.entrySet();
}
+
+ public enum AccessModel {
+ OPEN,
+ WHITELIST,
+ PRESENCE
+ }
}
@@ -59,6 +59,16 @@ public class Extension extends Element {
return child;
}
+ public <T extends Extension> T setExtension(T child) {
+ final var iterator = getChildren().iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().getClass().isInstance(child)) {
+ iterator.remove();
+ }
+ }
+ return this.addExtension(child);
+ }
+
public void addExtensions(final Collection<? extends Extension> extensions) {
for (final Extension extension : extensions) {
addExtension(extension);
@@ -1,8 +1,10 @@
package im.conversations.android.xmpp.model.avatar;
+import com.google.common.base.Strings;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import okhttp3.HttpUrl;
@XmlElement(namespace = Namespace.AVATAR_METADATA)
public class Info extends Extension {
@@ -11,6 +13,20 @@ public class Info extends Extension {
super(Info.class);
}
+ public Info(
+ final String id,
+ final long bytes,
+ final String type,
+ final int width,
+ final int height) {
+ this();
+ this.setId(id);
+ this.setBytes(bytes);
+ this.setType(type);
+ this.setWidth(width);
+ this.setHeight(height);
+ }
+
public long getHeight() {
return this.getLongAttribute("height");
}
@@ -27,11 +43,39 @@ public class Info extends Extension {
return this.getAttribute("type");
}
- public String getUrl() {
- return this.getAttribute("url");
+ public HttpUrl getUrl() {
+ final var url = this.getAttribute("url");
+ if (Strings.isNullOrEmpty(url)) {
+ return null;
+ }
+ return HttpUrl.parse(url);
}
public String getId() {
return this.getAttribute("id");
}
+
+ public void setBytes(final long size) {
+ this.setAttribute("bytes", size);
+ }
+
+ public void setId(final String id) {
+ this.setAttribute("id", id);
+ }
+
+ public void setHeight(final long height) {
+ this.setAttribute("height", height);
+ }
+
+ public void setWidth(final long width) {
+ this.setAttribute("width", width);
+ }
+
+ public void setType(final String type) {
+ this.setAttribute("type", type);
+ }
+
+ public void setUrl(final HttpUrl url) {
+ this.setAttribute("url", url.toString());
+ }
}
@@ -3,6 +3,7 @@ package im.conversations.android.xmpp.model.avatar;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement(namespace = Namespace.AVATAR_METADATA)
public class Metadata extends Extension {
@@ -10,4 +11,8 @@ public class Metadata extends Extension {
public Metadata() {
super(Metadata.class);
}
+
+ public Collection<Info> getInfos() {
+ return this.getExtensions(Info.class);
+ }
}
@@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Block extends Extension {
@@ -9,4 +10,8 @@ public class Block extends Extension {
public Block() {
super(Block.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -2,10 +2,15 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Blocklist extends Extension {
public Blocklist() {
super(Blocklist.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -14,4 +14,8 @@ public class Item extends Extension {
public Jid getJid() {
return getAttributeAsJid("jid");
}
+
+ public void setJid(final Jid address) {
+ this.setAttribute("jid", address);
+ }
}
@@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Unblock extends Extension {
@@ -9,4 +10,8 @@ public class Unblock extends Extension {
public Unblock() {
super(Unblock.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -0,0 +1,27 @@
+package im.conversations.android.xmpp.model.bob;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Data extends Extension implements ByteContent {
+
+ public Data() {
+ super(Data.class);
+ }
+
+ public String getCid() {
+ return this.getAttribute("cid");
+ }
+
+ public String getType() {
+ return this.getAttribute("type");
+ }
+
+ public static Optional<Data> get(final Extension stanza, final String cid) {
+ return Iterables.tryFind(stanza.getExtensions(Data.class), d -> cid.equals(d.getCid()));
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.BOB)
+package im.conversations.android.xmpp.model.bob;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -29,4 +29,8 @@ public class Conference extends Extension {
public Extensions getExtensions() {
return this.getExtension(Extensions.class);
}
+
+ public void setConferenceName(String name) {
+ this.setAttribute("name", name);
+ }
}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.bookmark2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Password extends Extension {
+
+ public Password() {
+ super(Password.class);
+ }
+}
@@ -0,0 +1,21 @@
+package im.conversations.android.xmpp.model.conference;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "x")
+public class DirectInvite extends Extension {
+
+ public DirectInvite() {
+ super(DirectInvite.class);
+ }
+
+ public void setJid(final Jid jid) {
+ this.setAttribute("jid", jid);
+ }
+
+ public void setPassword(final String password) {
+ this.setAttribute("password", password);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.DIRECT_MUC_INVITATIONS)
+package im.conversations.android.xmpp.model.conference;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,7 +1,10 @@
package im.conversations.android.xmpp.model.data;
+import android.util.Log;
+import com.google.common.base.CaseFormat;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
+import eu.siacs.conversations.Config;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@@ -51,11 +54,14 @@ public class Data extends Extension {
if (type != null) {
field.setType(type);
}
- if (value instanceof Collection) {
- for (final Object subValue : (Collection<?>) value) {
- if (subValue instanceof String) {
+ if (value instanceof Collection<?> collection) {
+ Log.d(Config.LOGTAG, "submitting collection: " + collection);
+ for (final Object subValue : collection) {
+ if (subValue == null) {
+ Log.d(Config.LOGTAG, "null value in the values for " + name);
+ } else if (subValue instanceof String s) {
final var valueExtension = field.addExtension(new Value());
- valueExtension.setContent((String) subValue);
+ valueExtension.setContent(s);
} else {
throw new IllegalArgumentException(
String.format(
@@ -65,12 +71,15 @@ public class Data extends Extension {
}
} 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");
+ if (value instanceof String s) {
+ valueExtension.setContent(s);
+ } else if (value instanceof Enum<?> e) {
+ valueExtension.setContent(
+ CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, e.toString()));
+ } else if (value instanceof Integer i) {
+ valueExtension.setContent(String.valueOf(i));
+ } else if (value instanceof Boolean b) {
+ valueExtension.setContent(Boolean.TRUE.equals(b) ? "1" : "0");
} else {
throw new IllegalArgumentException(
String.format(
@@ -105,9 +114,11 @@ public class Data extends Extension {
final var fieldName = existingField.getFieldName();
final Object submittedValue = values.get(fieldName);
if (submittedValue != null) {
+ Log.d(Config.LOGTAG, "submitting value " + fieldName + ": " + submittedValue);
submit.addField(fieldName, submittedValue);
} else {
- submit.addField(fieldName, existingField.getValues());
+ Log.d(Config.LOGTAG, "staying with default for: " + fieldName);
+ submit.addExtension(existingField);
}
}
return submit;
@@ -5,6 +5,7 @@ import com.google.common.collect.Iterables;
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.media.Media;
import java.util.Collection;
@XmlElement
@@ -18,6 +19,7 @@ public class Field extends Extension {
}
public Collection<String> getValues() {
+ // TODO filter null
return Collections2.transform(getExtensions(Value.class), Element::getContent);
}
@@ -32,4 +34,8 @@ public class Field extends Extension {
public void setType(String type) {
this.setAttribute("type", type);
}
+
+ public Media getMedia() {
+ return getOnlyExtension(Media.class);
+ }
}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.hints;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class NoCopy extends Extension {
+ public NoCopy() {
+ super(NoCopy.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.hints;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class NoStore extends Extension {
+
+ public NoStore() {
+ super(NoStore.class);
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Close extends InBandByteStream {
+
+ public Close() {
+ super(Close.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+
+@XmlElement
+public class Data extends InBandByteStream implements ByteContent {
+
+ public Data() {
+ super(Data.class);
+ }
+}
@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class InBandByteStream extends Extension {
+
+ public InBandByteStream(Class<? extends InBandByteStream> clazz) {
+ super(clazz);
+ }
+
+ public String getSid() {
+ return this.getAttribute("sid");
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Open extends InBandByteStream {
+
+ public Open() {
+ super(Open.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.IBB)
+package im.conversations.android.xmpp.model.ibb;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -9,4 +9,9 @@ public class Subject extends Extension {
public Subject() {
super(Subject.class);
}
+
+ public Subject(final String subject) {
+ this();
+ this.setContent(subject);
+ }
}
@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.media;
+
+import com.google.common.collect.Collections2;
+import de.gultsch.common.MiniUri;
+import eu.siacs.conversations.xml.Element;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Media extends Extension {
+
+ public Media() {
+ super(Media.class);
+ }
+
+ public Collection<MiniUri> getUris() {
+ final var uris =
+ Collections2.filter(
+ Collections2.transform(this.getExtensions(Uri.class), Element::getContent),
+ Objects::nonNull);
+ return Collections2.transform(uris, MiniUri::new);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.media;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Uri extends Extension {
+
+ public Uri() {
+ super(Uri.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MEDIA_ELEMENT)
+package im.conversations.android.xmpp.model.media;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,5 +1,6 @@
package im.conversations.android.xmpp.model.muc;
+import eu.siacs.conversations.generator.AbstractGenerator;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@@ -17,4 +18,8 @@ public class History extends Extension {
public void setMaxStanzas(final int maxStanzas) {
this.setAttribute("maxstanzas", maxStanzas);
}
+
+ public void setSince(long timestamp) {
+ this.setAttribute("since", AbstractGenerator.getTimestamp(timestamp));
+ }
}
@@ -0,0 +1,54 @@
+package im.conversations.android.xmpp.model.muc;
+
+import android.util.Log;
+import com.google.common.base.Strings;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Locale;
+
+public abstract class Item extends Extension {
+
+ public Item(final Class<? extends Item> clazz) {
+ super(clazz);
+ }
+
+ public Affiliation getAffiliation() {
+ return affiliationOrNone(this.getAttribute("affiliation"));
+ }
+
+ public static Affiliation affiliationOrNone(final String affiliation) {
+ if (Strings.isNullOrEmpty(affiliation)) {
+ return Affiliation.NONE;
+ }
+ try {
+ return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT));
+ } catch (final IllegalArgumentException e) {
+ return Affiliation.NONE;
+ }
+ }
+
+ public Role getRole() {
+ return roleOrNone(this.getAttribute("role"));
+ }
+
+ public static Role roleOrNone(final String role) {
+ if (Strings.isNullOrEmpty(role)) {
+ return Role.NONE;
+ }
+ try {
+ return Role.valueOf(role.toUpperCase(Locale.ROOT));
+ } catch (final IllegalArgumentException e) {
+ Log.d(Config.LOGTAG, "could not parse role " + role);
+ return Role.NONE;
+ }
+ }
+
+ public String getNick() {
+ return this.getAttribute("nick");
+ }
+
+ public Jid getJid() {
+ return this.getAttributeAsJid("jid");
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Password extends Extension {
+
+ public Password() {
+ super(Password.class);
+ }
+
+ public Password(final String password) {
+ this();
+ this.setContent(password);
+ }
+}
@@ -0,0 +1,30 @@
+package im.conversations.android.xmpp.model.muc.admin;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.muc.Affiliation;
+import im.conversations.android.xmpp.model.muc.Role;
+
+@XmlElement
+public class Item extends im.conversations.android.xmpp.model.muc.Item {
+
+ public Item() {
+ super(Item.class);
+ }
+
+ public void setAffiliation(final Affiliation affiliation) {
+ this.setAttribute("affiliation", affiliation);
+ }
+
+ public void setRole(final Role role) {
+ this.setAttribute("role", role);
+ }
+
+ public void setJid(final Jid jid) {
+ this.setAttribute("jid", jid);
+ }
+
+ public void setNick(String user) {
+ this.setAttribute("nick", user);
+ }
+}
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc.admin;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+
+@XmlElement(name = "query")
+public class MucAdmin extends Extension {
+
+ public MucAdmin() {
+ super(MucAdmin.class);
+ }
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MUC_ADMIN)
+package im.conversations.android.xmpp.model.muc.admin;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.muc.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Destroy extends Extension {
+
+ public Destroy() {
+ super(Destroy.class);
+ }
+}
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.muc.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
+
+@XmlElement(name = "query")
+public class MucOwner extends Extension {
+ public MucOwner() {
+ super(MucOwner.class);
+ }
+
+ public Data getConfiguration() {
+ return this.getExtension(Data.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MUC_OWNER)
+package im.conversations.android.xmpp.model.muc.owner;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,17 @@
+package im.conversations.android.xmpp.model.muc.user;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Invite extends Extension {
+
+ public Invite() {
+ super(Invite.class);
+ }
+
+ public void setTo(final Jid to) {
+ this.setAttribute("to", to);
+ }
+}
@@ -1,58 +1,11 @@
package im.conversations.android.xmpp.model.muc.user;
-import android.util.Log;
-
-import com.google.common.base.Strings;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.Jid;
-
import im.conversations.android.annotation.XmlElement;
-import im.conversations.android.xmpp.model.Extension;
-import im.conversations.android.xmpp.model.muc.Affiliation;
-import im.conversations.android.xmpp.model.muc.Role;
-
-import java.util.Locale;
@XmlElement
-public class Item extends Extension {
-
+public class Item extends im.conversations.android.xmpp.model.muc.Item {
public Item() {
super(Item.class);
}
-
- public Affiliation getAffiliation() {
- final var affiliation = this.getAttribute("affiliation");
- if (Strings.isNullOrEmpty(affiliation)) {
- return Affiliation.NONE;
- }
- try {
- return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT));
- } catch (final IllegalArgumentException e) {
- Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation);
- return Affiliation.NONE;
- }
- }
-
- public Role getRole() {
- final var role = this.getAttribute("role");
- if (Strings.isNullOrEmpty(role)) {
- return Role.NONE;
- }
- try {
- return Role.valueOf(role.toUpperCase(Locale.ROOT));
- } catch (final IllegalArgumentException e) {
- Log.d(Config.LOGTAG,"could not parse role "+ role);
- return Role.NONE;
- }
- }
-
- public String getNick() {
- return this.getAttribute("nick");
- }
-
- public Jid getJid() {
- return this.getAttributeAsJid("jid");
- }
}
@@ -10,4 +10,9 @@ public class Nick extends Extension {
public Nick() {
super(Nick.class);
}
+
+ public Nick(final String nick) {
+ this();
+ this.setContent(nick);
+ }
}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.offline;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Offline extends Extension {
+
+ public Offline() {
+ super(Offline.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.offline;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Purge extends Extension {
+
+ public Purge() {
+ super(Purge.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL)
+package im.conversations.android.xmpp.model.offline;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,15 +1,18 @@
package im.conversations.android.xmpp.model.pgp;
import eu.siacs.conversations.xml.Namespace;
-
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
-@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED)
+@XmlElement(name = "x", namespace = Namespace.PGP_SIGNED)
public class Signed extends Extension {
-
public Signed() {
super(Signed.class);
}
+
+ public Signed(final String signature) {
+ this();
+ this.setContent(signature);
+ }
}
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.pubsub.owner;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Delete extends Extension {
+
+ public Delete() {
+ super(Delete.class);
+ }
+
+ public void setNode(final String node) {
+ this.setAttribute("node", node);
+ }
+}
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Instructions extends Extension {
public Instructions() {
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Password extends Extension {
public Password() {
@@ -2,7 +2,6 @@ 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 {
@@ -11,8 +10,8 @@ public class Register extends Extension {
super(Register.class);
}
- public void addUsername(final Localpart username) {
- this.addExtension(new Username()).setContent(username.toString());
+ public void addUsername(final String username) {
+ this.addExtension(new Username()).setContent(username);
}
public void addPassword(final String password) {
@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.register;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement(name = "register", namespace = Namespace.REGISTER_STREAM_FEATURE)
+public class RegisterStreamFeature extends StreamFeature {
+
+ public RegisterStreamFeature() {
+ super(RegisterStreamFeature.class);
+ }
+}
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Remove extends Extension {
public Remove() {
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.reporting;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.REPORTING)
+public class Report extends Extension {
+ public Report() {
+ super(Report.class);
+ }
+
+ public void setReason(final String reason) {
+ this.setAttribute("reason", reason);
+ }
+}
@@ -9,4 +9,9 @@ public class Group extends Extension {
public Group() {
super(Group.class);
}
+
+ public Group(final String group) {
+ this();
+ this.setContent(group);
+ }
}
@@ -1,13 +1,10 @@
package im.conversations.android.xmpp.model.roster;
import com.google.common.collect.Collections2;
-
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
-
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
-
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -28,10 +25,18 @@ public class Item extends Extension {
return getAttributeAsJid("jid");
}
+ public void setJid(final Jid jid) {
+ this.setAttribute("jid", jid);
+ }
+
public String getItemName() {
return this.getAttribute("name");
}
+ public void setItemName(final String serverName) {
+ this.setAttribute("name", serverName);
+ }
+
public boolean isPendingOut() {
return "subscribe".equalsIgnoreCase(this.getAttribute("ask"));
}
@@ -45,12 +50,26 @@ public class Item extends Extension {
}
}
+ public void setSubscription(final Subscription subscription) {
+ if (subscription == null) {
+ this.removeAttribute("subscription");
+ } else {
+ this.setAttribute("subscription", subscription.toString().toLowerCase(Locale.ROOT));
+ }
+ }
+
public Collection<String> getGroups() {
return Collections2.filter(
Collections2.transform(getExtensions(Group.class), Element::getContent),
Objects::nonNull);
}
+ public void setGroups(final Collection<String> groups) {
+ for (final String group : groups) {
+ this.addExtension(new Group());
+ }
+ }
+
public enum Subscription {
NONE,
TO,
@@ -3,6 +3,7 @@ package im.conversations.android.xmpp.model.roster;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement(name = "query", namespace = Namespace.ROSTER)
public class Query extends Extension {
@@ -18,4 +19,8 @@ public class Query extends Extension {
public String getVersion() {
return this.getAttribute("ver");
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -0,0 +1,18 @@
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Activate extends Extension {
+
+ public Activate() {
+ super(Activate.class);
+ }
+
+ public Activate(final Jid jid) {
+ this();
+ this.setContent(jid.toString());
+ }
+}
@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.socks5;
+
+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 StreamHost getStreamHost() {
+ return this.getExtension(StreamHost.class);
+ }
+
+ public void setSid(final String streamId) {
+ this.setAttribute("sid", streamId);
+ }
+}
@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "streamhost")
+public class StreamHost extends Extension {
+
+ public StreamHost() {
+ super(StreamHost.class);
+ }
+
+ public Jid getJid() {
+ return this.getAttributeAsJid("jid");
+ }
+
+ public String getHost() {
+ return this.getAttribute("host");
+ }
+
+ public Integer getPort() {
+ return this.getOptionalIntAttribute("port").orNull();
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.BYTE_STREAMS)
+package im.conversations.android.xmpp.model.socks5;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,7 +1,6 @@
package im.conversations.android.xmpp.model.stanza;
import com.google.common.base.Strings;
-import eu.siacs.conversations.xml.Element;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.error.Error;
@@ -50,22 +49,6 @@ public class Iq extends Stanza {
return super.isInvalid();
}
- // Legacy methods that need to be refactored:
-
- public Element query() {
- final Element query = findChild("query");
- if (query != null) {
- return query;
- }
- return addChild("query");
- }
-
- public Element query(final String xmlns) {
- final Element query = query();
- query.setAttribute("xmlns", xmlns);
- return query();
- }
-
public Iq generateResponse(final Iq.Type type) {
final var packet = new Iq(type);
packet.setTo(this.getFrom());
@@ -5,6 +5,7 @@ import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
import im.conversations.android.xmpp.model.jabber.Show;
import im.conversations.android.xmpp.model.jabber.Status;
+import java.util.Locale;
@XmlElement
public class Presence extends Stanza implements EntityCapabilities {
@@ -13,6 +14,11 @@ public class Presence extends Stanza implements EntityCapabilities {
super(Presence.class);
}
+ public Presence(final Type type) {
+ this();
+ this.setType(type);
+ }
+
public Availability getAvailability() {
final var show = getExtension(Show.class);
if (show == null) {
@@ -28,6 +34,18 @@ public class Presence extends Stanza implements EntityCapabilities {
this.addExtension(new Show()).setContent(availability.toShowString());
}
+ public void setType(final Type type) {
+ if (type == null) {
+ this.removeAttribute("type");
+ } else {
+ this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
+ }
+ }
+
+ public Type getType() {
+ return Type.valueOfOrNull(this.getAttribute("type"));
+ }
+
public void setStatus(final String status) {
if (Strings.isNullOrEmpty(status)) {
return;
@@ -40,6 +58,27 @@ public class Presence extends Stanza implements EntityCapabilities {
return status == null ? null : status.getContent();
}
+ public enum Type {
+ ERROR,
+ PROBE,
+ SUBSCRIBE,
+ SUBSCRIBED,
+ UNAVAILABLE,
+ UNSUBSCRIBE,
+ UNSUBSCRIBED;
+
+ public static Type valueOfOrNull(final String type) {
+ if (Strings.isNullOrEmpty(type)) {
+ return null;
+ }
+ try {
+ return valueOf(type.toUpperCase(Locale.ROOT));
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ }
+ }
+
public enum Availability {
CHAT,
ONLINE,
@@ -1,12 +1,13 @@
package im.conversations.android.xmpp.model.streams;
+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.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.register.RegisterStreamFeature;
import im.conversations.android.xmpp.model.sm.StreamManagement;
+import im.conversations.android.xmpp.model.token.Register;
@XmlElement
public class Features extends StreamElement implements EntityCapabilities {
@@ -18,14 +19,17 @@ public class Features extends StreamElement implements EntityCapabilities {
return hasStreamFeature(StreamManagement.class);
}
- public boolean invite() {
- return this.hasChild("register", Namespace.INVITE);
- }
-
public boolean clientStateIndication() {
return this.hasChild("csi", Namespace.CSI);
}
+ public boolean register() {
+ return hasStreamFeature(RegisterStreamFeature.class);
+ }
+
+ public boolean preAuthenticatedInBandRegistration() {
+ return hasStreamFeature(Register.class);
+ }
public boolean hasStreamFeature(final Class<? extends StreamFeature> clazz) {
return hasExtension(clazz);
@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Time extends Extension {
+
+ public Time() {
+ super(Time.class);
+ }
+
+ public void setTimeZoneOffset(final String tzo) {
+ this.addExtension(new TimeZoneOffset()).setContent(tzo);
+ }
+
+ public void setUniversalTime(final String utc) {
+ this.addExtension(new UniversalTime()).setContent(utc);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "tzo")
+public class TimeZoneOffset extends Extension {
+
+ public TimeZoneOffset() {
+ super(TimeZoneOffset.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "utc")
+public class UniversalTime extends Extension {
+
+ public UniversalTime() {
+ super(UniversalTime.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.TIME)
+package im.conversations.android.xmpp.model.time;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.token;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement
+public class Register extends StreamFeature {
+
+ public Register() {
+ super(Register.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.PRE_AUTHENTICATED_IN_BAND_REGISTRATION)
+package im.conversations.android.xmpp.model.token;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -13,10 +13,19 @@ public class StanzaId extends Extension {
super(StanzaId.class);
}
+ public StanzaId(final String id) {
+ this();
+ this.setAttribute("id", id);
+ }
+
public Jid getBy() {
return this.getAttributeAsJid("by");
}
+ public void setBy(final Jid by) {
+ this.setAttribute("by", by);
+ }
+
public String getId() {
return this.getAttribute("id");
}
@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.up;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Push extends Extension implements ByteContent {
+
+ public Push() {
+ super(Push.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.UNIFIED_PUSH)
+package im.conversations.android.xmpp.model.up;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,14 +1,21 @@
package im.conversations.android.xmpp.model.upload;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import okhttp3.HttpUrl;
@XmlElement
public class Put extends Extension {
+ private static final List<String> HEADER_ALLOW_LIST =
+ Arrays.asList("Authorization", "Cookie", "Expires");
+
public Put() {
super(Put.class);
}
@@ -24,4 +31,19 @@ public class Put extends Extension {
public Collection<Header> getHeaders() {
return this.getExtensions(Header.class);
}
+
+ public Map<String, String> getHeadersAllowList() {
+ final var headers = new ImmutableMap.Builder<String, String>();
+ for (final Header header : this.getHeaders()) {
+ final String name = header.getHeaderName();
+ final String value = Strings.nullToEmpty(header.getContent()).trim();
+ if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
+ continue;
+ }
+ if (HEADER_ALLOW_LIST.contains(name)) {
+ headers.put(name, value);
+ }
+ }
+ return headers.buildKeepingLast();
+ }
}
@@ -10,15 +10,15 @@ public class Request extends Extension {
super(Request.class);
}
- public void setFilename(String filename) {
+ public void setFilename(final String filename) {
this.setAttribute("filename", filename);
}
- public void setSize(long size) {
+ public void setSize(final long size) {
this.setAttribute("size", size);
}
- public void setContentType(String type) {
- this.setAttribute("content-ype", type);
+ public void setContentType(final String type) {
+ this.setAttribute("content-type", type);
}
}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Ephemeral extends Purpose {
+
+ public Ephemeral() {
+ super(Ephemeral.class);
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Message extends Purpose {
+
+ public Message() {
+ super(Purpose.class);
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Permanent extends Purpose {
+
+ public Permanent() {
+ super(Permanent.class);
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Profile extends Purpose {
+
+ public Profile() {
+ super(Profile.class);
+ }
+}
@@ -0,0 +1,10 @@
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class Purpose extends Extension {
+
+ protected Purpose(final Class<? extends Purpose> clazz) {
+ super(clazz);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.HTTP_UPLOAD_PURPOSE)
+package im.conversations.android.xmpp.model.upload.purpose;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -8,4 +8,13 @@ public class Photo extends Extension {
public Photo() {
super(Photo.class);
}
+
+ public BinaryValue getBinaryValue() {
+ return this.getExtension(BinaryValue.class);
+ }
+
+ public void setType(final String value) {
+ final var type = this.addExtension(new Type());
+ type.setContent(value);
+ }
}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.vcard;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "TYPE")
+public class Type extends Extension {
+
+ public Type() {
+ super(Type.class);
+ }
+}
@@ -9,4 +9,8 @@ public class VCard extends Extension {
public VCard() {
super(VCard.class);
}
+
+ public Photo getPhoto() {
+ return this.getExtension(Photo.class);
+ }
}
@@ -16,6 +16,11 @@ public class VCardUpdate extends Extension {
public String getHash() {
final var photo = getPhoto();
- return photo == null ? null : photo.getContent();
+ final var hash = photo == null ? null : photo.getContent();
+ return isValidSHA1(hash) ? hash : null;
+ }
+
+ public static boolean isValidSHA1(final String s) {
+ return s != null && s.matches("[a-fA-F0-9]{40}");
}
}
@@ -0,0 +1,128 @@
+package im.conversations.android.xmpp.processor;
+
+import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
+
+import android.util.Log;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.http.ServiceOutageStatus;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+public class AccountStateProcessor extends XmppConnection.Delegate
+ implements Consumer<Account.State> {
+
+ private final XmppConnectionService service;
+
+ public AccountStateProcessor(final XmppConnectionService service, XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ @Override
+ public void accept(final Account.State status) {
+ final var account = getAccount();
+ if (ServiceOutageStatus.isPossibleOutage(status)) {
+ this.service.fetchServiceOutageStatus(account);
+ }
+ this.service.updateAccountUi();
+
+ if (account.getStatus() == Account.State.ONLINE || account.getStatus().isError()) {
+ this.service.getQuickConversationsService().signalAccountStateChange();
+ }
+
+ if (account.getStatus() == Account.State.ONLINE) {
+ synchronized (this.service.mLowPingTimeoutMode) {
+ if (this.service.mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": leaving low ping timeout mode");
+ }
+ }
+ if (account.setShowErrorNotification(true)) {
+ this.service.databaseBackend.updateAccount(account);
+ }
+ this.service.getMessageArchiveService().executePendingQueries(account);
+ if (this.connection.getFeatures().csi()) {
+ if (this.service.checkListeners()) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive");
+ connection.sendInactive();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//active");
+ connection.sendActive();
+ }
+ }
+ final var mucManager = getManager(MultiUserChatManager.class);
+ final var conversations = this.service.getConversations();
+ for (final var conversation : conversations) {
+ final boolean inProgressJoin = mucManager.isJoinInProgress(conversation);
+ if (conversation.getAccount() == account && !inProgressJoin) {
+ this.service.sendUnsentMessages(conversation);
+ }
+ }
+ this.service.scheduleWakeUpCall(
+ Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
+ } else if (account.getStatus() == Account.State.OFFLINE
+ || account.getStatus() == Account.State.DISABLED
+ || account.getStatus() == Account.State.LOGGED_OUT) {
+ this.service.resetSendingToWaiting(account);
+ if (account.isConnectionEnabled() && this.service.isInLowPingTimeoutMode(account)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": went into offline state during low ping mode."
+ + " reconnecting now");
+ this.service.reconnectAccount(account, true, false);
+ } else {
+ final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
+ this.service.scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
+ }
+ } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
+ this.service.databaseBackend.updateAccount(account);
+ this.service.reconnectAccount(account, true, false);
+ } else if (account.getStatus() != Account.State.CONNECTING
+ && account.getStatus() != Account.State.NO_INTERNET) {
+ this.service.resetSendingToWaiting(account);
+ if (connection != null && account.getStatus().isAttemptReconnect()) {
+ final boolean aggressive =
+ account.getStatus() == Account.State.SEE_OTHER_HOST
+ || this.service.hasJingleRtpConnection(account);
+ final int next = connection.getTimeToNextAttempt(aggressive);
+ final boolean lowPingTimeoutMode = this.service.isInLowPingTimeoutMode(account);
+ if (next <= 0) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": error connecting account. reconnecting now."
+ + " lowPingTimeout="
+ + lowPingTimeoutMode);
+ this.service.reconnectAccount(account, true, false);
+ } else {
+ final int attempt = connection.getAttempt() + 1;
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": error connecting account. try again in "
+ + next
+ + "s for the "
+ + attempt
+ + " time. lowPingTimeout="
+ + lowPingTimeoutMode
+ + ", aggressive="
+ + aggressive);
+ this.service.scheduleWakeUpCall(next, account.getUuid().hashCode());
+ if (aggressive) {
+ this.service.internalPingExecutor.schedule(
+ service::manageAccountConnectionStatesInternal,
+ (next * 1000L) + 50,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+ }
+ this.service.getNotificationService().updateErrorNotification();
+ }
+}
@@ -1,13 +1,23 @@
package im.conversations.android.xmpp.processor;
-import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
import 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;
+import eu.siacs.conversations.xmpp.manager.BookmarkManager;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
+import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
+import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
+import eu.siacs.conversations.xmpp.manager.NickManager;
+import eu.siacs.conversations.xmpp.manager.OfflineMessagesManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
public class BindProcessor extends XmppConnection.Delegate implements Runnable {
@@ -22,7 +32,6 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
public void run() {
final var account = connection.getAccount();
final var features = connection.getFeatures();
- service.cancelAvatarFetches(account);
final boolean loggedInSuccessfully =
account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
final boolean sosModified;
@@ -34,70 +43,71 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
sosModified = false;
}
final boolean gainedFeature =
- account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, features.httpUpload(0));
+ account.setOption(
+ Account.OPTION_HTTP_UPLOAD_AVAILABLE,
+ getManager(HttpUploadManager.class).isAvailableForSize(0));
if (loggedInSuccessfully || gainedFeature || sosModified) {
service.databaseBackend.updateAccount(account);
}
if (loggedInSuccessfully) {
- if (!TextUtils.isEmpty(account.getDisplayName())) {
+ final String displayName = account.getDisplayName();
+ if (!Strings.isNullOrEmpty(displayName)) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": display name wasn't empty on first log in. publishing");
- service.publishDisplayName(account);
+ getManager(NickManager.class).publish(displayName);
}
}
- account.getRoster().clearPresences();
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.clear();
- }
- synchronized (account.inProgressConferencePings) {
- account.inProgressConferencePings.clear();
- }
+ getManager(RosterManager.class).clearPresences();
+ getManager(MultiUserChatManager.class).clearInProgress();
service.getJingleConnectionManager().notifyRebound(account);
service.getQuickConversationsService().considerSyncBackground(false);
- connection.fetchRoster();
-
- if (features.bookmarks2()) {
- service.fetchBookmarks2(account);
- } else if (!features.bookmarksConversion()) {
- service.fetchBookmarks(account);
- }
+ getManager(RosterManager.class).request();
+ getManager(BookmarkManager.class).request();
if (features.mds()) {
- service.fetchMessageDisplayedSynchronization(account);
+ getManager(MessageDisplayedSynchronizationManager.class).fetch();
} else {
Log.d(Config.LOGTAG, account.getJid() + ": server has no support for mds");
}
+ final var offlineManager = getManager(OfflineMessagesManager.class);
final boolean bind2 = features.bind2();
- final boolean flexible = features.flexibleOfflineMessageRetrieval();
+ final boolean flexible = offlineManager.hasFeature();
final boolean catchup = service.getMessageArchiveService().inCatchup(account);
final boolean trackOfflineMessageRetrieval;
if (!bind2 && flexible && catchup && connection.isMamPreferenceAlways()) {
trackOfflineMessageRetrieval = false;
- connection.sendIqPacket(
- IqGenerator.purgeOfflineMessages(),
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
+ Futures.addCallback(
+ offlineManager.purge(),
+ new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": successfully purged offline messages");
}
- });
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not purge offline messages", t);
+ }
+ },
+ MoreExecutors.directExecutor());
} else {
trackOfflineMessageRetrieval = true;
}
- service.sendPresence(account);
+ getManager(PresenceManager.class).available();
connection.trackOfflineMessageRetrieval(trackOfflineMessageRetrieval);
if (service.getPushManagementService().available(account)) {
service.getPushManagementService().registerPushTokenOnServer(account);
}
service.connectMultiModeConversations(account);
- service.syncDirtyContacts(account);
+ getManager(RosterManager.class).syncDirtyContacts();
service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account);
}
@@ -0,0 +1,53 @@
+package im.conversations.android.xmpp.processor;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
+import java.util.function.BiFunction;
+
+public class MessageAcknowledgedProcessor extends XmppConnection.Delegate
+ implements BiFunction<Jid, String, Boolean> {
+
+ private final XmppConnectionService service;
+
+ public MessageAcknowledgedProcessor(
+ final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ @Override
+ public Boolean apply(final Jid to, final String id) {
+ if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
+ final String sessionId =
+ id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
+ this.service
+ .getJingleConnectionManager()
+ .updateProposedSessionDiscovered(
+ getAccount(),
+ to,
+ sessionId,
+ JingleConnectionManager.DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED);
+ }
+
+ final Jid bare = to.asBareJid();
+
+ for (final Conversation conversation : service.getConversations()) {
+ if (conversation.getAccount() == getAccount()
+ && conversation.getJid().asBareJid().equals(bare)) {
+ final Message message = conversation.findUnsentMessageWithUuid(id);
+ if (message != null) {
+ message.setStatus(Message.STATUS_SEND);
+ message.setErrorMessage(null);
+ getDatabase().updateMessage(message, false);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M160,920L160,840L800,840L800,920L160,920ZM160,120L160,40L800,40L800,120L160,120ZM480,520Q530,520 565,485Q600,450 600,400Q600,350 565,315Q530,280 480,280Q430,280 395,315Q360,350 360,400Q360,450 395,485Q430,520 480,520ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM230,720L730,720Q685,664 621,632Q557,600 480,600Q403,600 339,632Q275,664 230,720Z"/>
+</vector>
@@ -45,48 +45,94 @@
android:padding="@dimen/card_padding_regular">
<com.google.android.material.imageview.ShapeableImageView
- android:id="@+id/details_contact_badge"
+ android:id="@+id/details_avatar"
android:layout_width="@dimen/avatar_on_details_screen_size"
android:layout_height="@dimen/avatar_on_details_screen_size"
android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Photo"
android:scaleType="centerCrop" />
+ <TextView
+ android:id="@+id/details_contact_xmpp_address"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details_avatar"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="16sp"
+ android:gravity="center_horizontal"
+ android:minWidth="288dp"
+ android:text="@string/account_settings_example_jabber_id"
+ android:textIsSelectable="true"
+ android:textAppearance="?textAppearanceTitleLarge" />
+
<LinearLayout
- android:id="@+id/details_jidbox"
+ android:id="@+id/button_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_toRightOf="@+id/details_contact_badge"
- android:orientation="vertical">
+ android:layout_below="@+id/details_contact_xmpp_address"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="16dp"
+ android:orientation="horizontal">
- <TextView
- android:id="@+id/details_contactjid"
+ <Button
+ android:id="@+id/add_contact_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/account_settings_example_jabber_id"
- android:textIsSelectable="true"
- android:textAppearance="?textAppearanceTitleMedium" />
+ android:layout_marginHorizontal="8dp"
+ android:text="@string/add_contact" />
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/tags"
+ <Button
+ android:id="@+id/add_address_book"
+ style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="8sp">
-
- <androidx.constraintlayout.helper.widget.Flow
- android:id="@+id/flow_widget"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:flow_horizontalBias="0"
- app:flow_horizontalGap="8sp"
- app:flow_horizontalStyle="packed"
- app:flow_verticalGap="4sp"
- app:flow_wrapMode="chain"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
- </androidx.constraintlayout.widget.ConstraintLayout>
+ android:layout_marginHorizontal="8dp"
+ android:text="@string/save_to_contact"
+ app:icon="@drawable/ic_contacts_24dp" />
+ </LinearLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/tags"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/button_box"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="16sp"
+ android:visibility="gone">
+
+ <androidx.constraintlayout.helper.widget.Flow
+ android:id="@+id/flow_widget"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:flow_horizontalBias="0"
+ app:flow_horizontalGap="8sp"
+ app:flow_horizontalStyle="packed"
+ app:flow_verticalGap="4sp"
+ app:flow_wrapMode="chain"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <TextView
+ android:id="@+id/details_last_seen"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/tags"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="8sp"
+ android:textAppearance="?textAppearanceTitleSmall"
+ tools:text="@string/just_now" />
+
+ <LinearLayout
+ android:id="@+id/status_message_subscription_box"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/details_last_seen"
+ android:layout_alignStart="@+id/details_contact_xmpp_address"
+ android:layout_marginTop="8sp"
+ android:orientation="vertical">
<com.cheogram.android.TagEditorView
android:id="@+id/edit_tags"
@@ -110,23 +156,14 @@
android:id="@+id/status_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
+ android:layout_marginBottom="8sp"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="Hey there! I’m using Conversations" />
- <Button
- android:id="@+id/add_contact_button"
- style="@style/Widget.Material3.Button.ElevatedButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- android:text="@string/add_contact" />
-
<CheckBox
android:id="@+id/details_send_presence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
android:text="@string/send_presence_updates" />
<CheckBox
@@ -140,9 +177,10 @@
android:id="@+id/details_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@+id/details_jidbox"
+ android:layout_below="@+id/status_message_subscription_box"
android:layout_alignParentEnd="true"
- android:layout_marginTop="32dp"
+ android:layout_alignParentBottom="true"
+ android:layout_marginTop="16sp"
android:text="@string/using_account"
android:textAppearance="?textAppearanceLabelMedium" />
</RelativeLayout>
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ android:orientation="vertical">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </LinearLayout>
+</layout>
@@ -8,7 +8,6 @@
<string name="channel_details">تفاصيل القناة</string>
<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_block_contact">حجب جهة إتصال</string>
<string name="action_unblock_contact">إنهاء حجب جهة اتصال</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Подробности за канала</string>
<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_block_contact">Блокиране на контакта</string>
<string name="action_unblock_contact">Деблокиране на контакта</string>
@@ -8,7 +8,6 @@
<string name="channel_details">চ্যনেলের বিশদ বিবরণ</string>
<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_block_contact">এই ব্যক্তিকে ব্লক্ করা যাক</string>
<string name="action_unblock_contact">ব্লকটা সরিয়ে ফেলা যাক</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalls del canal</string>
<string name="action_add_account">Afegeix un compte</string>
<string name="action_edit_contact">Edita el nom</string>
- <string name="action_add_phone_book">Afegeix a la llibreta d\'adreces</string>
<string name="action_delete_contact">Elimina de la llista de contactes</string>
<string name="action_block_contact">Bloqueja el contacte</string>
<string name="action_unblock_contact">Desbloqueja el contacte</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Detaily kanálu</string>
<string name="action_add_account">Přidat účet</string>
<string name="action_edit_contact">Upravit jméno</string>
- <string name="action_add_phone_book">Přidat do adresáře</string>
<string name="action_delete_contact">Smazat ze seznamu</string>
<string name="action_block_contact">Zablokovat kontakt</string>
<string name="action_unblock_contact">Odblokovat kontakt</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Kanaldetaljer </string>
<string name="action_add_account">Tilføj konto</string>
<string name="action_edit_contact">Rediger navn</string>
- <string name="action_add_phone_book">Tilføj til adressebog</string>
<string name="action_delete_contact">Slet fra liste</string>
<string name="action_block_contact">Blokerer kontakt</string>
<string name="action_unblock_contact">Frigiv kontakt</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Channeldetails</string>
<string name="action_add_account">Konto hinzufügen</string>
<string name="action_edit_contact">Namen bearbeiten</string>
- <string name="action_add_phone_book">Zum Telefonbuch hinzufügen</string>
<string name="action_delete_contact">Aus Kontaktliste entfernen</string>
<string name="action_block_contact">Kontakt sperren</string>
<string name="action_unblock_contact">Kontakt entsperren</string>
@@ -1124,4 +1123,7 @@
<string name="account_status_service_outage_known">Dienst nicht verfügbar (bekanntes Problem)</string>
<string name="sos_scheduled_return">Geplante Wiederaufnahme des Dienstes um %s</string>
<string name="account_status_service_outage_scheduled">Geplante Ausfallzeit</string>
+ <string name="no_contacts_permission">%1$s benötigt Zugriff auf deine Kontakte</string>
+ <string name="save_to_contact">In Kontakten speichern</string>
+ <string name="show_in_contacts">In Kontakten anzeigen</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Λεπτομέρειες καναλιού</string>
<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_block_contact">Αποκλεισμός επαφής</string>
<string name="action_unblock_contact">Άρση αποκλεισμού επαφής</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalles del canal</string>
<string name="action_add_account">Añadir cuenta</string>
<string name="action_edit_contact">Editar contacto</string>
- <string name="action_add_phone_book">Añadir a contactos</string>
<string name="action_delete_contact">Eliminar contacto de la lista</string>
<string name="action_block_contact">Bloquear contacto</string>
<string name="action_unblock_contact">Desbloquear contacto</string>
@@ -1124,7 +1123,7 @@
<string name="non_quicksy_backup">Quicksy solo puede restaurar las copias de seguridad de las cuentas quicksy.im</string>
<string name="pref_backup_location">Ubicación de la copia de seguridad</string>
<string name="restore_omemo_key">Restaurar claves OMEMO</string>
- <string name="account_status_channel_binding">Canal no disponible</string>
+ <string name="account_status_channel_binding">Sin enlace de canal</string>
<string name="uri">URI</string>
<string name="copy_geo_uri">Copiar geolocalización</string>
<string name="copy_email_address">Copiar dirección email</string>
@@ -1133,4 +1132,10 @@
<string name="copy_URI">Copiar URI</string>
<string name="copied_phone_number">Número de teléfono copiado al portapapeles</string>
<string name="copy_telephone_number">Copiar numero de teléfono</string>
+ <string name="sos_scheduled_return">Está previsto que el servicio vuelva a las %s</string>
+ <string name="account_status_service_outage_scheduled">Interrupción Programada del Servicio</string>
+ <string name="account_status_service_outage_known">Servicio Interrumpido (Problema Conocido)</string>
+ <string name="no_contacts_permission">%1$s necesita acceso a sus contactos</string>
+ <string name="save_to_contact">Guardar en contactos</string>
+ <string name="show_in_contacts">Mostrar en contactos</string>
</resources>
@@ -2,7 +2,6 @@
<resources>
<string name="action_settings">Seadistused</string>
<string name="action_edit_contact">Muuda nime</string>
- <string name="action_add_phone_book">Lisa aadressiraamatusse</string>
<string name="title_activity_choose_contacts">Vali kontaktid</string>
<string name="minute_ago">1 minut tagasi</string>
<string name="action_contact_details">Kontaktandmed</string>
@@ -907,7 +906,7 @@
<string name="rtp_state_accepting_call">Võtame kõnet vastu</string>
<string name="rtp_state_ending_call">Lõpetame kõnet</string>
<string name="answer_call">Vasta</string>
- <string name="dismiss_call">Keeldu</string>
+ <string name="dismiss_call">Loobu</string>
<string name="rtp_state_finding_device">Tuvastame seadmeid</string>
<string name="rtp_state_ringing">Helistame</string>
<string name="rtp_state_contact_offline">Kontakt pole kättesaadav</string>
@@ -1065,7 +1064,7 @@
<string name="delete_from_server">Eemalda kasutajakonto serverist</string>
<string name="could_not_delete_account_from_server">Kasutajakonto eemaldamine serverist ei õnnestunud</string>
<string name="contact_uses_unverified_keys">Vestluse teine osapool kasutab verifitseerimata nutiseadmeid. Verifitseerimiseks skaneeri tema QR-koodi ja takista võimalikke vahendusründeid.</string>
- <string name="privacy_policy">Privaatsuspoliitika</string>
+ <string name="privacy_policy">Andmekaitsepõhimõtted</string>
<string name="contact_list_integration_not_available">Kontaktiloendi lõimimine pole hetkel saadaval</string>
<string name="unverified_devices">Sina kasutad verifitseerimata nutiseadmeid. Verifitseerimiseks skaneeri oma muus sedames kuvatavat QR-koodi ja takista võimalikke vahendusründeid.</string>
<string name="report_spam">Teata spämmist</string>
@@ -1144,4 +1143,7 @@
<string name="account_status_service_outage_scheduled">Planeeritud teenuse katkestus</string>
<string name="account_status_service_outage_known">Teenus ei tööta (teadaolev probleem)</string>
<string name="sos_scheduled_return">Teenuse plaanilise taastumise aeg on %s</string>
+ <string name="save_to_contact">Salvesta kontaktidesse</string>
+ <string name="no_contacts_permission">%1$s vajab ligipääsu sinu kontaktidele</string>
+ <string name="show_in_contacts">Näita kontaktides</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Kanalaren xehetasunak</string>
<string name="action_add_account">Kontua gehitu</string>
<string name="action_edit_contact">Izena editatu</string>
- <string name="action_add_phone_book">Helbideen liburura gehitu</string>
<string name="action_delete_contact">Zerrendatik ezabatu</string>
<string name="action_block_contact">Kontaktua blokeatu</string>
<string name="action_unblock_contact">Kontaktua desblokeatu</string>
@@ -6,7 +6,6 @@
<string name="action_muc_details">جزئیات چت گروهی</string>
<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_block_contact">بلاک مخاطب</string>
<string name="action_unblock_contact">غیر بلاک کردن مخاطب</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Kanavan tiedot</string>
<string name="action_add_account">Lisää tili</string>
<string name="action_edit_contact">Muokkaa nimeä</string>
- <string name="action_add_phone_book">Lisää yhteystietoihin</string>
<string name="action_delete_contact">Poista yhteystietolistasta</string>
<string name="action_block_contact">Estä yhteystieto</string>
<string name="action_unblock_contact">Peru yhteystiedon esto</string>
@@ -8,7 +8,6 @@
<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>
<string name="action_delete_contact">Retirer des contacts</string>
<string name="action_block_contact">Bloquer le contact</string>
<string name="action_unblock_contact">Débloquer le contact</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalles da canle</string>
<string name="action_add_account">Engadir conta</string>
<string name="action_edit_contact">Editar contacto</string>
- <string name="action_add_phone_book">Engadir a libreta de enderezos</string>
<string name="action_delete_contact">Eliminar contacto da lista</string>
<string name="action_block_contact">Bloquear contacto</string>
<string name="action_unblock_contact">Desbloquear contacto</string>
@@ -1124,4 +1123,7 @@
<string name="account_status_service_outage_scheduled">Desconexión programada</string>
<string name="sos_scheduled_return">Está previsto reactivar o servizo ás %s</string>
<string name="account_status_service_outage_known">Sen servizo (incidencia coñecida)</string>
+ <string name="no_contacts_permission">%1$s precisa acceso aos teus contactos</string>
+ <string name="save_to_contact">Gardar nos contactos</string>
+ <string name="show_in_contacts">Mostrar nos contactos</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalji kanala</string>
<string name="action_add_account">Dodaj račun</string>
<string name="action_edit_contact">Uredi ime</string>
- <string name="action_add_phone_book">Dodaj u adresar</string>
<string name="action_delete_contact">Izbriši s popisa</string>
<string name="action_block_contact">Blokiraj kontakt</string>
<string name="action_unblock_contact">Odblokiraj kontakt</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Csatorna részletei</string>
<string name="action_add_account">Fiók hozzáadása</string>
<string name="action_edit_contact">Név szerkesztése</string>
- <string name="action_add_phone_book">Hozzáadás a címjegyzékhez</string>
<string name="action_delete_contact">Törlés a névsorból</string>
<string name="action_block_contact">Partner tiltása</string>
<string name="action_unblock_contact">Partner tiltásának feloldása</string>
@@ -1095,4 +1094,6 @@
<string name="pref_chat_bubbles">Csevegőbuborékok</string>
<string name="pref_chat_bubbles_summary">Háttérszín, betűméret, avatárok</string>
<string name="pref_title_bubbles">Csevegőbuborékok</string>
+ <string name="account_status_channel_binding">Nincs channel binding</string>
+ <string name="account_status_connection_timeout">Kapcsolati időkorlát</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Detil channel </string>
<string name="action_add_account">Tambah Akun</string>
<string name="action_edit_contact">Ubah Nama</string>
- <string name="action_add_phone_book">Tambahkan ke daftar kontak</string>
<string name="action_delete_contact">Hapus dari roster</string>
<string name="action_block_contact">Blokir kontak</string>
<string name="action_unblock_contact">Batal blokir kontak</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Dettagli canale</string>
<string name="action_add_account">Aggiungi profilo</string>
<string name="action_edit_contact">Modifica il nome</string>
- <string name="action_add_phone_book">Aggiungi alla rubrica</string>
<string name="action_delete_contact">Cancella dalla lista</string>
<string name="action_block_contact">Blocca contatto</string>
<string name="action_unblock_contact">Sblocca contatto</string>
@@ -1138,4 +1137,7 @@
<string name="account_status_service_outage_scheduled">Manutenzione programmata</string>
<string name="account_status_service_outage_known">Servizio in manutenzione (problema noto)</string>
<string name="sos_scheduled_return">Il servizio è programmato per tornare il %s</string>
+ <string name="no_contacts_permission">%1$s richiede l\'accesso ai tuoi contatti</string>
+ <string name="save_to_contact">Salva nei contatti</string>
+ <string name="show_in_contacts">Mostra nei contatti</string>
</resources>
@@ -296,7 +296,6 @@
<string name="crash_report_message">השימוש בחשבון XMPP שלך לשליחת באגים עוזר לפיתוח מתמשך של %1$s.</string>
<string name="title_activity_share_with">שתף עם…</string>
<string name="action_archive_chat">ארכיון צ\'אט</string>
- <string name="action_add_phone_book">הוסף לפנקס הכתובות</string>
<string name="action_block_participant">חסום משתתף</string>
<string name="action_unblock_participant">בטל חסימת משתמש</string>
<string name="title_activity_choose_contacts">בחר אנשי קשר</string>
@@ -8,7 +8,6 @@
<string name="channel_details">談話室の詳細</string>
<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_block_contact">連絡先をブロック</string>
<string name="action_unblock_contact">連絡先をブロック解除</string>
@@ -317,7 +316,7 @@
<string name="pref_keep_foreground_service">フォアグラウンドサービス</string>
<string name="pref_keep_foreground_service_summary">OSが接続を切断するのを防止します</string>
<string name="pref_create_backup">バックアップを作成</string>
- <string name="pref_create_backup_summary">バックアップファイルは %s に保存されます</string>
+ <string name="pref_create_backup_summary">バックアップは %s に保存されます</string>
<string name="notification_create_backup_title">バックアップファイルを作成しています</string>
<string name="notification_backup_created_title">バックアップを作成しました</string>
<string name="notification_backup_created_subtitle">バックアップファイルは %s に保存されました</string>
@@ -807,12 +806,12 @@
<string name="ebook">電子書籍</string>
<string name="video_original">そのまま (非圧縮)</string>
<string name="open_with">…で開く</string>
- <string name="set_profile_picture">Conversations プロフィール画像</string>
+ <string name="set_profile_picture">アバター</string>
<string name="choose_account">アカウントを選択</string>
<string name="restore_backup">バックアップを復元</string>
<string name="restore">復元</string>
<string name="enter_password_to_restore">バックアップを復元するアカウント %s のパスワードを入力してください。</string>
- <string name="restore_warning">インストールの複製(同時実行)を作成する際に、バックアップの復元機能を使用しないでください。バックアップの復元は、移行時や元のデバイスを紛失した場合にのみ使用してください。</string>
+ <string name="restore_warning">インストールの複製(同時実行)を作成する際に、 OMEMO 鍵を復元しないでください。 OMEMO 鍵の復元は、移行時や元のデバイスを紛失した場合にのみ使用してください。</string>
<string name="unable_to_restore_backup">バックアップを復元できません。</string>
<string name="unable_to_decrypt_backup">バックアップを復号できません。パスワードは正しいですか?</string>
<string name="backup_channel_name">バックアップ&復元</string>
@@ -966,7 +965,7 @@
<string name="account_state_logged_out">ログアウトしました</string>
<string name="outgoing_call_timestamp">発信通話 · %s</string>
<string name="audiobook">オーディオブック</string>
- <string name="restore_warning_continued">自分で保存したバックアップしか復元しないでください!</string>
+ <string name="restore_warning_continued">復元するバックアップは、あなたが個人的に作成したものだけにしてください。</string>
<string name="log_in">ログイン</string>
<string name="hide_notification">通知を表示しない</string>
<string name="delete_from_server">アカウントをサーバーから削除</string>
@@ -1094,4 +1093,18 @@
<string name="non_quicksy_backup">Quicksyはquicksy.imのアカウントのバックアップしか復元できません</string>
<string name="pref_backup_location">バックアップの保存先</string>
<string name="word_document">Word 文書</string>
+ <string name="copy_geo_uri">位置情報をコピー</string>
+ <string name="copy_email_address">電子メールアドレスをコピー</string>
+ <string name="copied_phone_number">電話番号をクリップボードにコピーしました</string>
+ <string name="retry_with_p2p">P2P で再試行</string>
+ <string name="copy_telephone_number">電話番号をコピー</string>
+ <string name="uri_copied_to_clipboard">URI をクリップボードにコピーしました</string>
+ <string name="copy_URI">URI をコピー</string>
+ <string name="account_status_service_outage_scheduled">予定停止期間</string>
+ <string name="no_contacts_permission">%1$s には、あなたの連絡先集へのアクセスが必要です</string>
+ <string name="copied_email_address">電子メールアドレスをクリップボードにコピーしました</string>
+ <string name="save_to_contact">連絡先に保存</string>
+ <string name="show_in_contacts">連絡先内で表示</string>
+ <string name="account_status_service_outage_known">機能停止(既知の問題)</string>
+ <string name="sos_scheduled_return">%s に復帰するように予定されています</string>
</resources>
@@ -100,7 +100,7 @@
<string name="try_again">Ɛreḍ tikkelt nniḍen</string>
<string name="file">afaylu</string>
<string name="choose_file">Fren afaylu</string>
- <string name="download_x_file">Zḍem-d %S</string>
+ <string name="download_x_file">Sader %s</string>
<string name="delete_x_file">Kkes %s</string>
<string name="open_x_file">Ldi %s</string>
<string name="file_deleted">Afaylu yettwakkes</string>
@@ -155,7 +155,7 @@
<string name="gif">GIF</string>
<string name="group_chat_name">Isem</string>
<string name="create_shortcut">Rnu anegzum</string>
- <string name="action_copy_location">Nɣel adeg</string>
+ <string name="action_copy_location">Nɣel adig</string>
<string name="action_share_location">Bḍu adeg</string>
<string name="title_activity_share_location">Bḍu adeg</string>
<string name="title_activity_show_location">Sken-d adeg</string>
@@ -183,7 +183,7 @@
<string name="event">Tadyant</string>
<string name="jabber_network">jabber.network</string>
<string name="local_server">Aqeddac adigan</string>
- <string name="rtp_state_ringing">Yettṣuni…</string>
+ <string name="rtp_state_ringing">Yettṣuni</string>
<string name="rtp_state_declined_or_busy">Ur yestuffi ara</string>
<string name="help">Tallalt</string>
<string name="ongoing_call">Asiwel iteddu</string>
@@ -231,4 +231,45 @@
<string name="new_password">Awal n uɛeddi amaynut</string>
<string name="view_conversation">Wali asqerdec</string>
<string name="add_anway">Rnu-t akken yebɣu yili</string>
+ <string name="set">Sbadu</string>
+ <string name="unblock">Kkes asekkeṛ</string>
+ <string name="pref_ui_options">Agrudem n useqdac</string>
+ <string name="server_info_unavailable">ulac</string>
+ <string name="decrypt">Kkes awgelhen</string>
+ <string name="channel_bare_jid_example">abadu@conference.example.com</string>
+ <string name="leave">Ffeɣ</string>
+ <string name="gp_short">Meẓẓiy</string>
+ <string name="gp_medium">Alemmas</string>
+ <string name="gp_long">Meqqer</string>
+ <string name="invite">Snubget</string>
+ <string name="title_activity_settings">Iɣewwaren</string>
+ <string name="pref_attachments">Imeddayen</string>
+ <string name="server_info_available">yella</string>
+ <string name="location">Adig</string>
+ <string name="blocked">Yettusewḥel</string>
+ <string name="channel_full_jid_example">abadu@conference.example.com/nick</string>
+ <string name="publishing">Asuffeɣ…</string>
+ <string name="pref_input_options">Sekcem</string>
+ <string name="server_info_broken">Yerreẓ</string>
+ <string name="copy_omemo_clipboard_description">Nɣel adsil umḍin OMEMO ɣer tecfawt</string>
+ <string name="copy_fingerprint">Nɣel adsil umḍin</string>
+ <string name="copy_link">Nɣel tansa web</string>
+ <string name="copy_URI">Nɣel URI</string>
+ <string name="copy_telephone_number">Nɣel uṭṭun n tiliɣri</string>
+ <string name="copy_jabber_id">Nɣel tansa XMPP</string>
+ <string name="copy_to_clipboard">Nɣel ɣer tecfawt</string>
+ <string name="copy_email_address">Nɣel tansa n yimayl</string>
+ <string name="copy_original_url">Nɣel URL aɣbalu</string>
+ <string name="action_archive_chat">Ḥrez adiwenni</string>
+ <string name="channel_details">Talqayt n ubadu</string>
+ <string name="action_delete_contact">Kkes seg yinermisen</string>
+ <string name="action_block_contact">Sewḥel anermis</string>
+ <string name="action_unblock_contact">Serreḥ i unermis</string>
+ <string name="action_block_domain">Sewḥel taɣult</string>
+ <string name="action_unblock_domain">Serreḥ i taɣult</string>
+ <string name="action_block_participant">Sewḥel imttekki</string>
+ <string name="action_unblock_participant">Serreḥ imttekki</string>
+ <string name="title_activity_block_list">Sewḥel tabdart</string>
+ <string name="minute_ago">1 tesdidt aya</string>
+ <string name="minutes_ago">%d tesdidin aya</string>
</resources>
@@ -5,7 +5,6 @@
<string name="action_contact_details">연락처 정보</string>
<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_block_contact">연락처 </string>
<string name="action_unblock_contact">연락처 차단 해제 </string>
@@ -6,7 +6,6 @@
<string name="action_muc_details">Gruppesludringsdetaljer</string>
<string name="action_add_account">Legg til samtale</string>
<string name="action_edit_contact">Rediger navn</string>
- <string name="action_add_phone_book">Legg til i kontaktliste</string>
<string name="action_delete_contact">Fjern fra kontaktliste</string>
<string name="action_block_contact">Blokker kontakt</string>
<string name="action_unblock_contact">Avblokker kontakt</string>
@@ -1,35 +1,6 @@
<resources>
- <style name="Theme.Conversations3" parent="Theme.Material3.Dark.NoActionBar">
- <item name="windowActionModeOverlay">true</item>
- <item name="colorPrimary">@color/md_theme_dark_primary</item>
- <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
- <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
- <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
- <item name="colorSecondary">@color/md_theme_dark_secondary</item>
- <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
- <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
- <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
- <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
- <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
- <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
- <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
- <item name="colorError">@color/md_theme_dark_error</item>
- <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
- <item name="colorOnError">@color/md_theme_dark_onError</item>
- <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
- <item name="android:colorBackground">@color/md_theme_dark_background</item>
- <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
- <item name="colorSurface">@color/md_theme_dark_surface</item>
- <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
- <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
- <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
- <item name="colorOutline">@color/md_theme_dark_outline</item>
- <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
- <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
- <item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
- <item name="preferenceTheme">@style/MaterialPreferenceThemeOverlay</item>
- </style>
+ <style name="Theme.Conversations3" parent="Theme.Conversations3.Dark"/>
<style name="Theme.Conversations3.SplashScreen" parent="@style/Theme.Conversations3">
<item name="android:windowBackground">@drawable/background_splash_screen</item>
@@ -8,7 +8,6 @@
<string name="channel_details">Kanaalinformatie</string>
<string name="action_add_account">Account toevoegen</string>
<string name="action_edit_contact">Naam veranderen</string>
- <string name="action_add_phone_book">Toevoegen aan adresboek</string>
<string name="action_delete_contact">Verwijderen uit lijst</string>
<string name="action_block_contact">Contact blokkeren</string>
<string name="action_unblock_contact">Contact deblokkeren</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Szczegóły kanału</string>
<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_block_contact">Zablokuj kontakt</string>
<string name="action_unblock_contact">Odblokuj kontakt</string>
@@ -1156,4 +1155,7 @@
<string name="account_status_service_outage_scheduled">Planowana niedostępność</string>
<string name="sos_scheduled_return">Przywrócenie usługi planowane na %s</string>
<string name="account_status_service_outage_known">Usługa niedostępna (znany problem)</string>
+ <string name="save_to_contact">Zapisz do kontaktów</string>
+ <string name="show_in_contacts">Pokaż w kontaktach</string>
+ <string name="no_contacts_permission">%1$s wymaga dostępu do Twoich kontaktów</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalhes do canal</string>
<string name="action_add_account">Adicionar conta</string>
<string name="action_edit_contact">Editar o nome</string>
- <string name="action_add_phone_book">Adicionar ao livro de endereços</string>
<string name="action_delete_contact">Excluir da lista de contatos</string>
<string name="action_block_contact">Bloquear contato</string>
<string name="action_unblock_contact">Desbloquear contato</string>
@@ -1142,4 +1141,7 @@
<string name="account_status_service_outage_scheduled">Tempo de inatividade planejado</string>
<string name="account_status_service_outage_known">Serviço caído (problema conhecido)</string>
<string name="sos_scheduled_return">Se espera que o serviço volte às %s</string>
+ <string name="no_contacts_permission">%1$s precisa de acesso aos seus contatos</string>
+ <string name="save_to_contact">Salvar nos contatos</string>
+ <string name="show_in_contacts">Mostrar nos contatos</string>
</resources>
@@ -5,7 +5,6 @@
<string name="action_contact_details">Detalhes do contacto</string>
<string name="action_add_account">Adicionar conta</string>
<string name="action_edit_contact">Editar nome</string>
- <string name="action_add_phone_book">Adicionar ao livro de endereços</string>
<string name="action_delete_contact">Apagar da lista</string>
<string name="action_block_contact">Bloquear contacto</string>
<string name="action_unblock_contact">Desbloquear contacto</string>
@@ -57,20 +56,20 @@
<string name="send_failed">a entrega falhou</string>
<string name="sharing_files_please_wait">Partilhando os ficheiros. Por favor aguarde...</string>
<string name="action_clear_history">Limpar histórico</string>
- <string name="clear_conversation_history">Limpar o histórico de conversas</string>
+ <string name="clear_conversation_history">Limpar o histórico do chat</string>
<string name="choose_presence">Escolher dispositivo</string>
- <string name="send_unencrypted_message">Enviar mensagem não cifrada</string>
+ <string name="send_unencrypted_message">Enviar mensagem em texto normal</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 cifrada com v\\OMEMO</string>
- <string name="send_unencrypted">Enviar não cifrada</string>
+ <string name="send_unencrypted">Enviar texto normal</string>
<string name="decryption_failed">A decifragem falhou. Talvez não tenha a chave privada correta.</string>
<string name="openkeychain_required">OpenKeychain</string>
<string name="restart">Reiniciar</string>
<string name="install">Instalar</string>
<string name="openkeychain_not_installed">Por favor instale o OpenKeychain</string>
<string name="offering">oferecendo...</string>
- <string name="waiting">aguardando...</string>
+ <string name="waiting">aguardando…</string>
<string name="no_pgp_key">Nenhuma chave OpenPGP encontrada</string>
<string name="no_pgp_keys">Não foram encontradas chaves OpenPGP</string>
<string name="pref_general">Geral</string>
@@ -170,7 +169,7 @@
<string name="contact_has_read_up_to_this_point">%s leu até este ponto</string>
<string name="everyone_has_read_up_to_this_point">Todos leram até este ponto</string>
<string name="publish">Publicar</string>
- <string name="publishing">Publicando...</string>
+ <string name="publishing">Publicando…</string>
<string name="error_publish_avatar_server_reject">O servidor rejeitou a sua publicação</string>
<string name="error_saving_avatar">Não foi possível guardar o avatar no disco</string>
<string name="or_long_press_for_default">(Ou mantenha pressionado por um tempo para voltar para o padrão)</string>
@@ -188,7 +187,7 @@
<string name="request_now">Solicitar agora</string>
<string name="ignore">Ignorar</string>
<string name="pref_security_settings">Segurança</string>
- <string name="pref_allow_message_correction">Permitir a correção de mensagens</string>
+ <string name="pref_allow_message_correction">Correção de mensagens</string>
<string name="pref_allow_message_correction_summary">Permitir que os seus contactos editem as suas mensagens depois de enviadas</string>
<string name="pref_expert_options">Definições avançadas</string>
<string name="pref_expert_options_summary">Por favor tenha cuidado com estas</string>
@@ -213,8 +212,8 @@
<string name="file_url">URL do ficheiro</string>
<string name="url_copied_to_clipboard">URL copiado para a área de transferência</string>
<string name="web_address">endereço web</string>
- <string name="scan_qr_code">Ler código de barras 2D</string>
- <string name="show_qr_code">Mostrar código de barras 2D</string>
+ <string name="scan_qr_code">Ler código QR</string>
+ <string name="show_qr_code">Mostrar código QR</string>
<string name="show_block_list">Mostar lista de bloqueios</string>
<string name="account_details">Detalhes da conta</string>
<string name="confirm">Confirmar</string>
@@ -238,7 +237,7 @@
<string name="clear_other_devices">Apagar dispositivos</string>
<string name="fetching_history_from_server">Obtendo o histórico do servidor</string>
<string name="no_more_history_on_server">Não existe mais histórico no servidor</string>
- <string name="updating">A atualizar...</string>
+ <string name="updating">A atualizar…</string>
<string name="password_changed">Palavra-passe alterada!</string>
<string name="could_not_change_password">Não foi possível alterar a palavra-passe</string>
<string name="change_password">Alterar palavra-passe</string>
@@ -417,4 +416,126 @@
<string name="remove_bookmark">Gostaria de remover %s como favorito?</string>
<string name="remove_bookmark_and_close">Gostaria de remover o favorito de %s e arquivar a conversa?</string>
<string name="remove_contact_text">Gostaria de remover %s da sua lista de contatos? A conversa não será removida.</string>
-</resources>
+ <string name="openpgp_error">O OpenKeychain produziu um erro.</string>
+ <string name="bad_key_for_encryption">Má chave para encriptação.</string>
+ <string name="pref_send_crash_reports">Enviar crash reports</string>
+ <string name="account_status_connection_timeout">Timeout da conexão</string>
+ <string name="account_status_regis_invalid_token">Token de registo inválido</string>
+ <string name="account_status_tls_error">Erro de negociação do TLS</string>
+ <string name="invite">Convidar</string>
+ <string name="crash_report_title">%1$s crashou</string>
+ <string name="crash_report_message">Usar a conta XMPP para enviar stack traces aujda o desenvolvimento do %1$s.</string>
+ <string name="problem_connecting_to_account">Não foi possível conectar à conta</string>
+ <string name="problem_connecting_to_accounts">Não foi possível conectar a várias contas</string>
+ <string name="touch_to_fix">Clique para gerir as suas contas</string>
+ <string name="not_in_roster">Adicionar este contacto à sua lista de contactos?</string>
+ <string name="preparing_images">Preparando para enviar as imagens</string>
+ <string name="delete_file_dialog">Apagar ficheiro</string>
+ <string name="archive_this_chat">Apagar chat depois</string>
+ <string name="send_encrypted_message">Enviar mensagem encriptada</string>
+ <string name="contact_has_no_pgp_key">Não foi possível encriptar a sua mensagem porque o contacto não está a anunciar a sua chave pública.\n\n<small>Por favor peça ao seu contacto para configurar o OpenPGP.</small></string>
+ <string name="pref_notification_sound">Som de notificação</string>
+ <string name="pref_notification_sound_summary">Sons de notificação para novas mensagens</string>
+ <string name="pref_call_ringtone_summary">Toque para chamadas recebidas</string>
+ <string name="pref_notification_grace_period">Período de espera</string>
+ <string name="pref_notification_grace_period_summary">Por quanto tempo as notificações são silenciadas após detetar atividade num dos seus outros dispositivos.</string>
+ <string name="pref_never_send_crash_summary">Ao mandar stack traces, ajuda o desenvolvimento</string>
+ <string name="pref_confirm_messages_summary">Permitir os contactos saberem quando recebeu e leu as mensagens deles</string>
+ <string name="pref_prevent_screenshots">Impedir Capturas de Ecrã</string>
+ <string name="pref_prevent_screenshots_summary">Esconde conteúdos da aplicação no app switcher e bloqueia capturas de ecrã</string>
+ <string name="error_security_exception">A aplicação que usou para partilhar o ficheiro não forneceu permissões suficientes.</string>
+ <string name="account_state_logged_out">Desconectado</string>
+ <string name="account_status_tls_error_domain">Domínio não verificável</string>
+ <string name="account_status_policy_violation">Violação da política</string>
+ <string name="account_status_incompatible_client">Cliente incompatível</string>
+ <string name="account_status_stream_error">Erro de stream</string>
+ <string name="account_status_stream_opening_error">Erro na abertura da stream</string>
+ <string name="openpgp_has_been_published">Chave pública OpenPGP publicada.</string>
+ <string name="delete_file_dialog_msg">Tem a certeza que quer apagar este ficheiro?\n\n<b>Aviso:</b> Isto não irá apagar cópias deste ficheiro que estão guardadas noutros dispositivos ou servidores.</string>
+ <string name="preparing_image">Preparando para enviar a imagem</string>
+ <string name="your_nick_has_been_changed">Novo nickname em uso</string>
+ <string name="contacts_have_no_pgp_keys">Não foi possível encriptar a sua mensagem porque os contactos não estão a anunciar a sua chave pública.\n\n<small>Por favor peça-lhes para configurarem o OpenPGP.</small></string>
+ <string name="clear_histor_msg">Quer apagar todas as mensagens neste chat?\n\n<b>Aviso:</b> Isto não irá influenciar as mensagens guardadas noutros dispositivos ou servidores.</string>
+ <string name="openkeychain_required_long"><![CDATA[%1$s usa o <b>OpenKeychain</b> para encriptar e desencriptar mensagens e gerir as suas chaves públicas.<br><br>Está licenciado sob GPLv3+ e disponível no F-Droid e na Google Play.<br><br><small>(Reinicie %1$s depois.)</small>]]></string>
+ <string name="error_compressing_image">Não foi possível converter o ficheiro de imagem</string>
+ <string name="account_status_channel_binding">Nenhum canal vinculado</string>
+ <string name="error_security_exception_during_image_copy">A aplicação que usou para selecionar esta imagem não providenciou permissões suficientes para ler o ficheiro.\n\n<small>Use um diferente gestor de ficheiros para escolher uma imagem</small>.</string>
+ <string name="account_status_regis_not_sup">Registação não suportada pelo servidor</string>
+ <string name="unpublish_pgp_message">Tem a certeza que quer remover a sua chave pública OpenPGP do seu anúncio de presença?\nOs seus contactos não conseguirão mais lhe enviar mensagens encriptadas com OpenPGP.</string>
+ <string name="omemo_fingerprint_x509_selected_message">Impressão digital v\\OMEMO (origem da mensagem)</string>
+ <string name="mgmt_account_delete_confirm_text">Tem a certeza que quer apagar a sua conta? Ao apagá-la, o seu histórico de conversa também é apagado</string>
+ <string name="last_seen_hour">visto há um hora</string>
+ <string name="last_seen_day">visto há um dia</string>
+ <string name="contacts_have_read_up_to_this_point">%s leu até aqui</string>
+ <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s+%2$d outros leram até aqui</string>
+ <string name="channel_bare_jid_example">canal@conferencia.exemplo.com</string>
+ <string name="channel_full_jid_example">canal@conferencia.exemplo.com/nick</string>
+ <string name="omemo_fingerprint_selected_message">Impressão digital OMEMO (origem da mensagem)</string>
+ <string name="joining_conference">Entrando no canal de grupo…</string>
+ <string name="account_settings_jabber_id">endereço XMPP</string>
+ <string name="block_jabber_id">Bloquear endereço XMPP</string>
+ <string name="error_out_of_memory">Sem memória. Imagem demasiado grande</string>
+ <string name="invalid_jid">Isto não é um endereço XMPP válido</string>
+ <string name="server_info_external_service_discovery">XEP-0215: Descoberta de Serviço Externo</string>
+ <string name="last_seen_min">visto há um minuto</string>
+ <string name="openpgp_messages_found">Novas mensagens encriptadas com OpenPGP encontradas</string>
+ <string name="install_openkeychain">Mensagem encriptada. Por favor, instale OpenKeychain para desencriptá-la.</string>
+ <string name="group_chats">Conversas em grupo</string>
+ <string name="destroy_room">Destruir conversa de grupo</string>
+ <string name="destroy_channel">Destruir canal</string>
+ <string name="destroy_channel_dialog">Tem a certeza que quer destruir este canal público?\n\n<b>Aviso:</b> O canal será completamente removido do servidor.</string>
+ <string name="could_not_destroy_room">Não foi possível destruir conversa de grupo</string>
+ <string name="could_not_destroy_channel">Não foi possível destruir canal</string>
+ <string name="destroy_room_dialog">Tem a certeza que quer destruir esta conversa de grupo?\n\n<b>Aviso:</b> A conversa de grupo será completamente removida do servidor.</string>
+ <string name="server_info_bind2">XEP-0386: Vinculação 2</string>
+ <string name="server_info_sasl2">XEP-0388: Perfil SASL extensível</string>
+ <string name="conference_technical_problems">Saiu desta conversa de grupo por questões técnicas</string>
+ <string name="pref_keep_foreground_service">Serviço em primeiro plano</string>
+ <string name="jabber_id_copied_to_clipboard">Endereço XMPP copiado para a área de transferência</string>
+ <string name="uri">URI</string>
+ <string name="server_info_session_established">Sessão estabelecida</string>
+ <string name="request_presence_updates">Por favor solicite atualizações de presença do seu contacto primeiro.\n\n<small>Isto será utilizado para determinar que aplicação de chat o seu contacto usa</small>.</string>
+ <string name="conference_resource_constraint">Limitação de recursos</string>
+ <string name="conference_kicked">Foi expulso desta conversa de grupo</string>
+ <string name="conference_shutdown">A conversa de grupo foi encerrada</string>
+ <string name="hosted_on">hospedado em %s</string>
+ <string name="retry_with_p2p">Tentar outra vez com P2P</string>
+ <string name="uri_copied_to_clipboard">URI copiado para a área de transferência</string>
+ <string name="error_message_copied_to_clipboard">Mensagem de erro copiada para a área de transferência</string>
+ <string name="touch_to_choose_picture">Clica no avatar para selecionar uma imagem da galeria</string>
+ <string name="error_publish_avatar_no_server_support">O seu servidor não suporta publicação de avatares</string>
+ <string name="without_mutual_presence_updates"><b>Aviso:</b> se enviar isto sem atualização de presença mútua com os seus contactos podem se produzir problemas inesperados.\n\n<b>Vá a “Detalhes do contacto” para verificar a sua inscrição de presença.</small></string>
+ <string name="title_activity_about_x">Sobre %s</string>
+ <string name="pref_autojoin">Sincronizar marcadores</string>
+ <string name="pref_autojoin_summary">Defina a opção “autojoin” quando entra ou sai de um MUC e reagir a modificações feitas por outros clientes.</string>
+ <string name="toast_message_omemo_fingerprint">Impressão digital OMEMO copiada para a área de transferência</string>
+ <string name="conference_unknown_error">Já não está mais nesta conversa de grupo</string>
+ <string name="error_publish_avatar_converting">Não foi possível converter a imagem</string>
+ <string name="notification_backup_created_subtitle">Os ficheiros de backup foram guardados em %s</string>
+ <string name="no_application_found_to_open_link">Nenhuma aplicação foi encontrada para abrir o link</string>
+ <string name="error_no_keys_to_trust_server_error">Não há chaves utilizáveis disponíveis para este contacto.\nNão foi possível obter novas chaves do servidor. Pode ser um problema com o servidor de contactos?</string>
+ <string name="grant_membership">Conceder privilégios de membro</string>
+ <string name="pref_create_backup">Criar backup</string>
+ <string name="pref_create_backup_summary">Backups serão guardados em %s</string>
+ <string name="notification_create_backup_title">Criando ficheiros de backup</string>
+ <string name="notification_backup_created_title">O backup foi criado</string>
+ <string name="restoring_backup">Restaurando do backup</string>
+ <string name="notification_restored_backup_title">O backup foi restaurado</string>
+ <string name="notification_restored_backup_subtitle">Não se esqueça de ativar a conta.</string>
+ <string name="preparing_file">Preparando para partilhar ficheiro</string>
+ <string name="file_deleted">Ficheiro apagado</string>
+ <string name="no_application_found_to_open_file">Nenhuma aplicação encontrada para abrir este ficheiro</string>
+ <string name="remove_membership">Revogar privilégios de membro</string>
+ <string name="grant_owner_privileges">Conceder privilégios de dono</string>
+ <string name="remove_owner_privileges">Revogar privilégios de dono</string>
+ <string name="remove_from_channel">Remover do canal</string>
+ <string name="file_transmission_failed">não foi possivel partilhar ficheiro</string>
+ <string name="file_transmission_cancelled">transmissão de ficheiro cancelada</string>
+ <string name="no_application_found_to_view_contact">Nenhuma aplicação foi encontrada para ver o contacto</string>
+ <string name="pref_show_dynamic_tags">Etiquetas Dinâmicas</string>
+ <string name="conference_creation_failed">Não foi possível criar chat de grupo</string>
+ <string name="error_no_keys_to_trust_presence">Não há chaves usáveis disponíveis para este contacto.\nVerifique que ambos têm subscrição de presença.</string>
+ <string name="clear_other_devices_desc">Tem a certeza que quer remover todos os outros dispositivos do anúncio OMEMO? Da próxima vez que os seus dispositivos se conectarem, eles serão anunciados novamente, mas podem não receber mensagens enviadas entretanto.</string>
+ <string name="error_trustkeys_title">Algo de errado não está certo</string>
+ <string name="password_should_not_be_empty">A password não pode estar vazia</string>
+</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Detalii canal</string>
<string name="action_add_account">Adaugă cont</string>
<string name="action_edit_contact">Editează nume</string>
- <string name="action_add_phone_book">Adaugă la lista de contacte</string>
<string name="action_delete_contact">Șterge din lista de contacte</string>
<string name="action_block_contact">Blochează contact</string>
<string name="action_unblock_contact">Deblochează contact</string>
@@ -1143,4 +1142,7 @@
<string name="account_status_service_outage_known">Serviciu oprit (Problemă cunoscută)</string>
<string name="sos_scheduled_return">Serviciul este programat să revină la %s</string>
<string name="account_status_service_outage_scheduled">Oprire planificată</string>
+ <string name="save_to_contact">Salvează în contacte</string>
+ <string name="show_in_contacts">Arată în contacte</string>
+ <string name="no_contacts_permission">%1$s are nevoie de acces la contacte</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Сведения о канале</string>
<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_block_contact">Заблокировать контакт</string>
<string name="action_unblock_contact">Разблокировать контакт</string>
@@ -1171,4 +1170,7 @@
<string name="account_status_service_outage_known">Сервис не работает (известная проблема)</string>
<string name="account_status_service_outage_scheduled">Запланированный простой</string>
<string name="sos_scheduled_return">Сервис должен заработать %s</string>
+ <string name="no_contacts_permission">%1$s требуется доступ к вашим контактам</string>
+ <string name="save_to_contact">Сохранить в контакты</string>
+ <string name="show_in_contacts">Показать в контактах</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Detaily kanála</string>
<string name="action_add_account">Pridať účet</string>
<string name="action_edit_contact">Upraviť meno</string>
- <string name="action_add_phone_book">Pridať do kontaktov</string>
<string name="action_delete_contact">Vymazať zo zoznamu</string>
<string name="action_block_contact">Zablokovať kontakt</string>
<string name="action_unblock_contact">Odblokovať kontakt</string>
@@ -7,7 +7,6 @@
<string name="channel_details">Hollësi kanali</string>
<string name="action_add_account">Shtoni llogari</string>
<string name="action_edit_contact">Përpunoni emër</string>
- <string name="action_add_phone_book">Shtoje te libër adresash</string>
<string name="action_block_contact">Bllokojeni kontaktin</string>
<string name="action_unblock_contact">Zhbllokoje kontaktin</string>
<string name="action_block_domain">Blloko përkatësin</string>
@@ -1134,4 +1133,7 @@
<string name="sos_scheduled_return">Shërbimi është vënë në plan të rikthehet në funksionim më %s</string>
<string name="account_status_service_outage_known">Shërbim Jashtë Funksionimi (Problem i Ditur)</string>
<string name="account_status_service_outage_scheduled">Kohë Mosfunksionimi e Planifikuar</string>
+ <string name="save_to_contact">Ruaje te Kontaktet</string>
+ <string name="show_in_contacts">Shfaqe te Kontakte</string>
+ <string name="no_contacts_permission">%1$s ka nevojë për hyrje te kontaktet tuaja</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Детаљи канала</string>
<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_block_contact">Блокирај контакт</string>
<string name="action_unblock_contact">Одблокирај контакт</string>
@@ -1158,4 +1157,7 @@
<string name="account_status_service_outage_scheduled">Планирана недоступност</string>
<string name="account_status_service_outage_known">Сервис недоступан (познат проблем)</string>
<string name="sos_scheduled_return">Опоравак сервиса предвиђен за %s</string>
+ <string name="save_to_contact">Сачувај у контакте</string>
+ <string name="no_contacts_permission">%1$s тражи приступ твојим контактима</string>
+ <string name="show_in_contacts">Прикажи у контактима</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Gruppdetaljer</string>
<string name="action_add_account">Lägg till konto</string>
<string name="action_edit_contact">Redigera namn</string>
- <string name="action_add_phone_book">Lägg till i kontakter</string>
<string name="action_delete_contact">Ta bort kontakt</string>
<string name="action_block_contact">Blockera kontakt</string>
<string name="action_unblock_contact">Avblockera kontakt</string>
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="portrait_only">false</bool>
+ <bool name="align_start">true</bool>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Informacyje kanału</string>
<string name="action_add_account">Przidej kōnto</string>
<string name="action_edit_contact">Edytuj miano</string>
- <string name="action_add_phone_book">Przidej do kōntaktōw</string>
<string name="action_delete_contact">Skasuj z rostera</string>
<string name="action_block_contact">Zablokuj kōntakt</string>
<string name="action_unblock_contact">Ôdblokuj kōntakt</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Kanal ayrıntıları</string>
<string name="action_add_account">Hesap ekle</string>
<string name="action_edit_contact">İsmi düzenle</string>
- <string name="action_add_phone_book">Telefon rehberine ekle</string>
<string name="action_delete_contact">Kişi listesinden sil</string>
<string name="action_block_contact">Kişiyi engelle</string>
<string name="action_unblock_contact">Kişiyi engellemekten vazgeç</string>
@@ -8,7 +8,6 @@
<string name="channel_details">Деталі каналу</string>
<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_block_contact">Заблокувати контакт</string>
<string name="action_unblock_contact">Розблокувати контакт</string>
@@ -1172,4 +1171,7 @@
<string name="account_status_service_outage_scheduled">Запланований простій</string>
<string name="account_status_service_outage_known">Сервіс не працює (відома проблема)</string>
<string name="sos_scheduled_return">Відновлення роботи заплановано на %s</string>
+ <string name="no_contacts_permission">%1$s потребує доступу до Ваших контактів</string>
+ <string name="save_to_contact">Зберегти до контактів</string>
+ <string name="show_in_contacts">Показати в контактах</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">Chi tiết kênh</string>
<string name="action_add_account">Thêm tài khoản</string>
<string name="action_edit_contact">Chỉnh sửa tên</string>
- <string name="action_add_phone_book">Thêm vào danh bạ</string>
<string name="action_delete_contact">Xoá khỏi danh sách bạn bè</string>
<string name="action_block_contact">Chặn liên hệ</string>
<string name="action_unblock_contact">Bỏ chặn liên hệ</string>
@@ -8,7 +8,6 @@
<string name="channel_details">频道详情</string>
<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_block_contact">屏蔽联系人</string>
<string name="action_unblock_contact">取消屏蔽联系人</string>
@@ -1119,4 +1118,7 @@
<string name="account_status_service_outage_scheduled">计划停机时间</string>
<string name="account_status_service_outage_known">服务中断(已知问题)</string>
<string name="sos_scheduled_return">服务预计将于 %s 恢复</string>
+ <string name="save_to_contact">保存到联系人</string>
+ <string name="show_in_contacts">在联系人中显示</string>
+ <string name="no_contacts_permission">%1$s 需要访问您的联系人</string>
</resources>
@@ -8,7 +8,6 @@
<string name="channel_details">頻道詳細資料</string>
<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_block_contact">封鎖聯絡人</string>
<string name="action_unblock_contact">解除封鎖聯絡人</string>
@@ -26,7 +26,7 @@
<dimen name="avatar_radius">10dp</dimen>
<dimen name="bubble_radius">10dp</dimen>
<dimen name="image_radius">6dp</dimen>
- <dimen name="avatar_on_details_screen_size">56dp</dimen>
+ <dimen name="avatar_on_details_screen_size">96dp</dimen>
<dimen name="avatar_on_conversation_overview">56dp</dimen>
<dimen name="avatar_on_drawer">128dp</dimen>
@@ -96,7 +96,7 @@
<string name="send_message_to_x">Send message to %s</string>
<string name="send_encrypted_message">Send encrypted message</string>
<string name="send_omemo_x509_message">Send v\\OMEMO encrypted message</string>
- <string name="your_nick_has_been_changed">New nickname in use</string>
+ <string name="your_nick_has_been_changed">Your nickname has been changed</string>
<string name="send_unencrypted">Send clear text</string>
<string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string>
<string name="openkeychain_required">OpenKeychain</string>
@@ -523,6 +523,7 @@
<string name="shared_text_with_x">Text shared with %s</string>
<string name="no_storage_permission">Grant %1$s access to external storage</string>
<string name="no_camera_permission">Grant %1$s access to the camera</string>
+ <string name="no_contacts_permission">%1$s needs access to your contacts</string>
<string name="quicksy_wants_your_consent">Quicksy asks for your consent to use your data</string>
<string name="sync_with_contacts">Contact list integration</string>
<string name="sync_with_contacts_long">%1$s processes your contact list locally, on your device, to show you the names and profile pictures for matching contacts on the Jabber network.\n\nNo contact list data ever leaves your device!</string>
@@ -1130,4 +1131,6 @@
<string name="account_status_service_outage_scheduled">Planned Downtime</string>
<string name="account_status_service_outage_known">Service Down (Known Issue)</string>
<string name="sos_scheduled_return">The service is scheduled to return at %s</string>
+ <string name="save_to_contact">Save to Device Contacts</string>
+ <string name="show_in_contacts">Show in Contacts</string>
</resources>
@@ -56,6 +56,37 @@
<item name="preferenceTheme">@style/MaterialPreferenceThemeOverlay</item>
</style>
+ <style name="Theme.Conversations3.Dark" parent="Theme.Material3.Dark.NoActionBar">
+ <item name="windowActionModeOverlay">true</item>
+ <item name="colorPrimary">@color/md_theme_dark_primary</item>
+ <item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
+ <item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
+ <item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
+ <item name="colorSecondary">@color/md_theme_dark_secondary</item>
+ <item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
+ <item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
+ <item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
+ <item name="colorTertiary">@color/md_theme_dark_tertiary</item>
+ <item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
+ <item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
+ <item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
+ <item name="colorError">@color/md_theme_dark_error</item>
+ <item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
+ <item name="colorOnError">@color/md_theme_dark_onError</item>
+ <item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
+ <item name="android:colorBackground">@color/md_theme_dark_background</item>
+ <item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
+ <item name="colorSurface">@color/md_theme_dark_surface</item>
+ <item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
+ <item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
+ <item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
+ <item name="colorOutline">@color/md_theme_dark_outline</item>
+ <item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
+ <item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
+ <item name="colorPrimaryInverse">@color/md_theme_dark_inversePrimary</item>
+ <item name="preferenceTheme">@style/MaterialPreferenceThemeOverlay</item>
+ </style>
+
<style name="MaterialPreferenceThemeOverlay" parent="@style/PreferenceThemeOverlay">
<item name="switchPreferenceCompatStyle">@style/MaterialSwitchPreference</item>
</style>
@@ -7,6 +7,7 @@
android:summary="@string/pref_use_colorful_bubbles_summary"
android:title="@string/pref_use_colorful_bubbles" />
<SwitchPreferenceCompat
+ android:defaultValue="@bool/align_start"
android:icon="@drawable/ic_format_align_left_24dp"
android:key="align_start"
android:summary="@string/pref_align_start_summary"
@@ -1,13 +1,13 @@
-Quicksy es un spin-off del popular cliente Jabber/XMPP Conversations, que ofrece descubrimiento automático de contactos.
+Quicksy es una bifurcación del popular cliente Jabber/XMPP Conversations con búsqueda automática de contactos.
-Te registras con tu número de teléfono y Quicksy, basándose en los números de teléfono de tu agenda, te sugerirá automáticamente posibles contactos.
+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.
-Bajo el capó, Quicksy es un cliente Jabber completamente funcional que te permite comunicarte con cualquier usuario en cualquier servidor público que federé. Asimismo, los usuarios de Quicksy pueden ser contactados desde el exterior simplemente agregando +número de teléfono@quicksy.im a tu lista de contactos.
+Quicksy es un cliente Jabber completo que te permite comunicarte con cualquier usuario en cualquier servidor público. Asimismo, los usuarios de Quicksy pueden ser contactados desde el exterior simplemente agregando +NúmeroDeTeléfono@quicksy.im a tu lista de contactos.
-Aparte de la sincronización de contactos, la interfaz de usuario está diseñada para ser lo más parecida posible a la de Conversations. Esto permite a los usuarios migrar eventualmente 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 es deliberadamente lo más parecida posible a Conversations. Esto permite que los usuarios migren 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 regulares de Jabber/XMPP que han ingresado su ID Jabber en el Directorio 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 ingresar tu ID Jabber en el Directorio Quicksy (https://quicksy.im/enter/), se requiere un pago de registro único.
+NOTA: Para ingresar (https://quicksy.im/enter/) tu ID de Jabber en el Directorio Quicksy se requiere un pago único de registro.
-Lee la Política de Privacidad para más información.
+Lee la Política de Privacidad (https://quicksy.im/#privacy) para obtener más información.
@@ -1,6 +1,6 @@
Quicksy on variatsioon populaarsest Jabber/XMPP kliendist Conversationsist, kuhu on lisandunud kontaktide automaatne tuvastus.
-Sa lisad liitumisel oma telefoninumbri ja Quicksyautomaatselt telefoniraamatus leiduvate numbrite alusel pakub sulle välja võimalikke suhtluspartnereid.
+Sa lisad liitumisel oma telefoninumbri ja Quicksy telefoniraamatus leiduvate numbrite alusel pakub sulle automaatselt välja võimalikke suhtluspartnereid.
Olemuselt on Quicksy tavaline täisfunktsionaalne Jabberi klient, mis lubab sul suhelda kasutajatega ükspuha missuguses muus avalikus XMPP/Jabberi serveris. Samamoodi saad ükspuha missuguse Quicksy kasutajaga suhelda lisades +telefoninumber@quicksy.im oma kontaktide loendisse.
@@ -11,4 +11,4 @@ Suhtlusparterite soovitused leitakse teiste Quicksy kasutajate hulgast ja tavali
MÄRKUS: Oma Jabberi kasutajatunnuse lisamine (https://quicksy.im/enter/) Quicksy kataloogi
eeldab ühekordse registreerimistasu maksmist.
-Lisateavet leiad meie privaatsuspoliitkast (https://quicksy.im/#privacy).
+Lisateavet leiad meie privaatsusreeglitest ja andmekaitsepõhimõtetest (https://quicksy.im/#privacy).
@@ -26,6 +26,7 @@ import eu.siacs.conversations.utils.TLSSocketFactory;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.stanza.Iq;
import io.michaelrocks.libphonenumber.android.Phonenumber;
import java.io.BufferedWriter;
@@ -407,7 +408,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
refresh(account, contacts.values());
if (!considerSync(account, contacts, forced)) {
- service.syncRoster(account);
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
}
}
}
@@ -422,7 +423,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
private void refresh(Account account, Collection<PhoneNumberContact> contacts) {
- for (Contact contact :
+ for (final var contact :
account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
final Uri uri = contact.getSystemAccount();
if (uri == null) {
@@ -498,9 +499,11 @@ public class QuickConversationsService extends AbstractQuickConversationsService
final Element phoneBook =
response.findChild("phone-book", Namespace.SYNCHRONIZATION);
if (phoneBook != null) {
- final List<Contact> withSystemAccounts =
- account.getRoster()
- .getWithSystemAccounts(PhoneNumberContact.class);
+ final var remaining =
+ new ArrayList<>(
+ account.getRoster()
+ .getWithSystemAccounts(
+ PhoneNumberContact.class));
for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
final PhoneNumberContact phoneContact =
contacts.get(entry.getNumber());
@@ -514,10 +517,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
if (needsCacheClean) {
service.getAvatarService().clear(contact);
}
- withSystemAccounts.remove(contact);
+ remaining.remove(contact);
}
}
- for (final Contact contact : withSystemAccounts) {
+ for (final Contact contact : remaining) {
final boolean needsCacheClean =
contact.unsetPhoneContact(PhoneNumberContact.class);
if (needsCacheClean) {
@@ -539,7 +542,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
+ ": failed to sync contact list with api server");
}
mRunningSyncJobs.decrementAndGet();
- service.syncRoster(account);
+ account.getXmppConnection()
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
service.updateRosterUi();
});
return true;
@@ -1,11 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="21.637"
- android:viewportHeight="21.637">
- <group>
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="472"
+ android:viewportHeight="472">
+ <group
+ android:translateX="20"
+ android:translateY="20">
<path
android:fillColor="@android:color/white"
@@ -1,13 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="49.41353"
- android:viewportHeight="49.413532">
+ android:width="432dp"
+ android:height="432dp"
+ android:viewportWidth="732"
+ android:viewportHeight="732">
+
<group
- android:translateX="13.888052"
- android:translateY="13.888054">
+ android:translateX="150"
+ android:translateY="150">
<path
- android:fillColor="#ffffff"
@@ -1,13 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="49.41353"
- android:viewportHeight="49.413532">
- <group
- android:translateX="13.888052"
- android:translateY="13.888054">
- <path
- android:fillColor="#000000"
@@ -2,13 +2,13 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
- <LinearLayout
+ <RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true"
- android:orientation="vertical">
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -19,100 +19,96 @@
android:minHeight="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
- <androidx.coordinatorlayout.widget.CoordinatorLayout
- android:id="@+id/coordinator"
+
+ <ScrollView
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/next"
+ android:layout_below="@id/app_bar_layout">
- <ScrollView
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:fillViewport="true">
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/instructions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:gravity="center_horizontal"
+ android:text="@string/enter_country_code_and_phone_number"
+ android:textAppearance="?textAppearanceBodyMedium" />
- <TextView
- android:id="@+id/instructions"
- android:layout_width="wrap_content"
+ <LinearLayout
+ android:id="@+id/phone_number_box"
+ android:layout_width="256dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="vertical">
+
+ <EditText
+ android:id="@+id/country"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_margin="16dp"
- android:gravity="center_horizontal"
- android:text="@string/enter_country_code_and_phone_number"
- android:textAppearance="?textAppearanceBodyMedium" />
+ android:cursorVisible="false"
+ android:drawableEnd="@drawable/ic_arrow_drop_down_18dp"
+ android:focusable="false"
+ android:gravity="bottom|center_horizontal"
+ android:imeOptions="flagNoExtractUi"
+ android:inputType="textNoSuggestions"
+ android:longClickable="false"
+ app:drawableTint="?android:attr/textColorPrimary" />
<LinearLayout
- android:id="@+id/phone_number_box"
- android:layout_width="256dp"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_above="@+id/next"
- android:layout_below="@+id/instructions"
- android:layout_centerHorizontal="true"
- android:orientation="vertical">
+ android:orientation="horizontal">
<EditText
- android:id="@+id/country"
- android:layout_width="match_parent"
+ android:id="@+id/country_code"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:cursorVisible="false"
- android:drawableEnd="@drawable/ic_arrow_drop_down_18dp"
- android:focusable="false"
+ android:layout_weight="1"
android:gravity="bottom|center_horizontal"
android:imeOptions="flagNoExtractUi"
- android:inputType="textNoSuggestions"
+ android:inputType="number"
android:longClickable="false"
- app:drawableTint="?android:attr/textColorPrimary" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <EditText
- android:id="@+id/country_code"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="bottom|center_horizontal"
- android:imeOptions="flagNoExtractUi"
- android:inputType="number"
- android:longClickable="false"
- android:maxLength="3"
- android:maxLines="1" />
+ android:maxLength="3"
+ android:maxLines="1" />
- <EditText
- android:id="@+id/number"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="3"
- android:gravity="bottom|start"
- android:hint="@string/phone_number"
- android:imeOptions="flagNoExtractUi"
- android:inputType="number"
- android:longClickable="false"
- android:maxLines="1" />
- </LinearLayout>
-
- <ProgressBar
- android:id="@+id/progressBar"
- style="?android:attr/progressBarStyle"
- android:layout_width="wrap_content"
+ <EditText
+ android:id="@+id/number"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_gravity="center" />
+ android:layout_weight="3"
+ android:gravity="bottom|start"
+ android:hint="@string/phone_number"
+ android:imeOptions="flagNoExtractUi"
+ android:inputType="number"
+ android:longClickable="false"
+ android:maxLines="1" />
</LinearLayout>
- <Button
- android:id="@+id/next"
- style="@style/Widget.Material3.Button.ElevatedButton"
+ <ProgressBar
+ android:layout_margin="16dp"
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentBottom="true"
- android:layout_margin="16dp"
- android:text="@string/next" />
- </RelativeLayout>
- </ScrollView>
- </androidx.coordinatorlayout.widget.CoordinatorLayout>
- </LinearLayout>
+ android:layout_gravity="center" />
+ </LinearLayout>
+ </LinearLayout>
+ </ScrollView>
+
+ <Button
+ android:id="@+id/next"
+ style="@style/Widget.Material3.Button.ElevatedButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentBottom="true"
+ android:layout_margin="16dp"
+ android:text="@string/next" />
+ </RelativeLayout>
</layout>
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="set_profile_picture">Tugna n umaɣnu n Quicksy</string>
- <string name="unknown_security_error">Tuccḍa n tɣellist tarussint</string>
+ <string name="unknown_security_error">Tuccḍa n tɣellist tarussint.</string>
<string name="not_available_in_your_country">Quicksy ur yelli ara deg tmurt-nnwen.</string>
+ <string name="unable_to_verify_server_identity">D awezɣi asefqed n timagit n uqeddac.</string>
+ <string name="timeout_while_connecting_to_server">Iɛedda wakud lawan n tuqqna ɣer uqeddac.</string>
+ <string name="pref_broadcast_last_activity_summary">Eǧǧ akk inermisen-ik⋅im ad ẓrenmi ara tesqedceḍ Quicksy</string>
</resources>
@@ -0,0 +1,16 @@
+package eu.siacs.conversations.xmpp;
+
+import org.junit.Test;
+
+public class JidTest {
+
+ @Test
+ public void testDoubleDash() {
+ Jid.ofUserInput("user@a--z.com");
+ }
+
+ @Test
+ public void testUnicode() {
+ Jid.ofUserInput("test@գծոոոց.հայ");
+ }
+}
@@ -82,7 +82,7 @@ public class EntityCapabilitiesTest {
}
@Test
- public void entityCapsOpenFire() throws IOException {
+ public void entityCapsOpenFireOrg() throws IOException {
final String xml =
"""
<iq type="result" xmlns="jabber:client" to="inputmice3@igniterealtime.org/Conversations.cI4W" from="igniterealtime.org" id="L3xl8X8_kzvx">
@@ -206,6 +206,104 @@ public class EntityCapabilitiesTest {
Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var);
}
+ @Test
+ public void entityCapsOpenFireTestServer() throws IOException {
+ final String xml =
+ """
+<iq type="result" id="779-6" to="jane@example.org" xmlns="jabber:client">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="server" name="Openfire Server" type="im"/>
+ <identity category="pubsub" type="pep"/>
+ <feature var="http://jabber.org/protocol/caps"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-default"/>
+ <feature var="http://jabber.org/protocol/pubsub#purge-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscription-options"/>
+ <feature var="http://jabber.org/protocol/pubsub#outcast-affiliation"/>
+ <feature var="msgoffline"/>
+ <feature var="jabber:iq:register"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#config-node"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-items"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-create"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-items"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="http://jabber.org/protocol/pubsub#persistent-items"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-and-configure"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations"/>
+ <feature var="urn:xmpp:time"/>
+ <feature var="http://jabber.org/protocol/pubsub#manage-subscriptions"/>
+ <feature var="urn:xmpp:bookmarks-conversion:0"/>
+ <feature var="http://jabber.org/protocol/offline"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish-options"/>
+ <feature var="urn:xmpp:carbons:2"/>
+ <feature var="http://jabber.org/protocol/address"/>
+ <feature var="http://jabber.org/protocol/pubsub#collections"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions"/>
+ <feature var="vcard-temp"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#get-pending"/>
+ <feature var="urn:xmpp:blocking"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#presence-notifications"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="http://jabber.org/protocol/pubsub#filtered-notifications"/>
+ <feature var="http://jabber.org/protocol/pubsub#item-ids"/>
+ <feature var="http://jabber.org/protocol/pubsub#meta-data"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-items"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="http://jabber.org/protocol/pubsub#instant-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#modify-affiliations"/>
+ <feature var="http://jabber.org/protocol/pubsub"/>
+ <feature var="http://jabber.org/protocol/pubsub#publisher-affiliation"/>
+ <feature var="http://jabber.org/protocol/pubsub#access-open"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="http://jabber.org/protocol/pubsub#retract-items"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:last"/>
+ <feature var="http://jabber.org/protocol/commands"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:private"/>
+ <feature var="http://jabber.org/protocol/rsm"/>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/network/serverinfo</value>
+ </field>
+ <field var="admin-addresses" type="list-multi">
+ <value>xmpp:admin@example.org</value>
+ <value>mailto:admin@example.com</value>
+ </field>
+ </x>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field type="text-single" var="os">
+ <value>Linux</value>
+ </field>
+ <field type="text-single" var="os_version">
+ <value>6.8.0-59-generic amd64 - Java 21.0.7</value>
+ </field>
+ <field type="text-single" var="software">
+ <value>Openfire</value>
+ </field>
+ <field type="text-single" var="software_version">
+ <value>5.0.0 Alpha</value>
+ </field>
+ </x>
+ </query>
+</iq>
+""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(Iq.class));
+ final var iq = (Iq) element;
+ final InfoQuery info = iq.getExtension(InfoQuery.class);
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("3wkXXN9QL/i/AyVoHaqaiTT8BFA=", var);
+ }
+
@Test
public void caps2() throws IOException {
final String xml =