diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e41547d4e87ce517d5b3d71f8644d714f2f650b..e3d5714334e1c02147a5b31f4cb6a205db52a9df 100644
--- a/CHANGELOG.md
+++ b/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'
diff --git a/build.gradle b/build.gradle
index 34ab7e138bade0ac88fea91cfa6ba35a1612b89e..dd774f09600e808135f1f18d914a8ac14205ad61 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.9.1'
+ classpath 'com.android.tools.build:gradle:8.9.3'
classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2"
}
}
@@ -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'
diff --git a/fastlane/metadata/android/es-ES/changelogs/4213904.txt b/fastlane/metadata/android/es-ES/changelogs/4213904.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2b50104fe1492891bcd6d0b7a68f13f5fdff0de0
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/4213904.txt
@@ -0,0 +1,2 @@
+* Posibilidad de elegir ubicación de copia de respaldo
+* Más URIs (tel:, mailto:) cliqueables
diff --git a/fastlane/metadata/android/es-ES/changelogs/4214004.txt b/fastlane/metadata/android/es-ES/changelogs/4214004.txt
new file mode 100644
index 0000000000000000000000000000000000000000..53c6141a6b4cd66c45e43e7ea913749acf848f8e
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/4214004.txt
@@ -0,0 +1,2 @@
+* Corregir reacciones a archivos recibidos mediante P2P
+* Mejorar detección de URI
diff --git a/fastlane/metadata/android/es-ES/changelogs/4214204.txt b/fastlane/metadata/android/es-ES/changelogs/4214204.txt
new file mode 100644
index 0000000000000000000000000000000000000000..457acfc63684d95df866f003407705555b9f37e1
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/4214204.txt
@@ -0,0 +1,2 @@
+* Compatibilidad con "Service Outage Status"
+* Correcciones menores de seguridad para resolver múltiples bodies, occupant-ids y stanza-id
diff --git a/fastlane/metadata/android/et/changelogs/4209204.txt b/fastlane/metadata/android/et/changelogs/4209204.txt
index cdb8e7cab41b7c30310ba9edd0a6b3558e89acaf..3539fac8d0b841f6b19a45eeb006a8825d739953 100644
--- a/fastlane/metadata/android/et/changelogs/4209204.txt
+++ b/fastlane/metadata/android/et/changelogs/4209204.txt
@@ -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
diff --git a/fastlane/metadata/android/uk/changelogs/395.txt b/fastlane/metadata/android/uk/changelogs/395.txt
index b7d80af5796dda8c3d6925a162cbda3865fdd2c8..5e306a3b1dbb487c9373eacb55a0e63f815b5ba3 100644
--- a/fastlane/metadata/android/uk/changelogs/395.txt
+++ b/fastlane/metadata/android/uk/changelogs/395.txt
@@ -1,3 +1,3 @@
* Додано «Повернутися до чату» на екрані голосового виклику
-* Удосконалено комбінації клавіш
+* Удосконалено клавіатурні скорочення
* Виправлення помилок
diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java
index 4006eb9b1c38fac09094dcece77be1164b3617af..5274892d0ced3b2ba15f05c8745a8682075b5366 100644
--- a/src/cheogram/java/com/cheogram/android/BobTransfer.java
+++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java
@@ -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);
diff --git a/src/cheogram/java/com/cheogram/android/FinishOnboarding.java b/src/cheogram/java/com/cheogram/android/FinishOnboarding.java
index fabcd644b0cf89becb8c0e9ed211576ed500729f..2a1d611d3d5c216cf379815cedfcf80e4d34110a 100644
--- a/src/cheogram/java/com/cheogram/android/FinishOnboarding.java
+++ b/src/cheogram/java/com/cheogram/android/FinishOnboarding.java
@@ -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);
diff --git a/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
index cf79c67c755b6f970a621f06616a9e2949aba52f..2f939adc05786fa15baa1371a05c1d49325e735e 100644
--- a/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
+++ b/src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java
@@ -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;
}
diff --git a/src/conversations/fastlane/metadata/android/es-ES/full_description.txt b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt
index 8c314f3f0cef64263bab72b783c17c0052faea48..5227a649702957916b88663dbc08cbe0aec6b3e5 100644
--- a/src/conversations/fastlane/metadata/android/es-ES/full_description.txt
+++ b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt
@@ -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 OMEMO o OpenPGP.
-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 OMEMO u OpenPGP.
+* 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.
diff --git a/src/conversations/res/drawable/ic_app_icon_notification.xml b/src/conversations/res/drawable/ic_app_icon_notification.xml
index 51ccc1fae5df3e885d759dfcc4c1524610cc20d3..defaebdd58ad9c45df44e6a1fd2a7f7ccf3e4386 100644
--- a/src/conversations/res/drawable/ic_app_icon_notification.xml
+++ b/src/conversations/res/drawable/ic_app_icon_notification.xml
@@ -1,11 +1,13 @@
-
+ android:viewportWidth="472"
+ android:viewportHeight="472">
+
+ android:pathData="M216 22C106.95 22.53 18 109.75 18 216.9c0 107.15 88.95 193.6 198 193.1 33.91-0.17 60.08-7.25 89.93-18.23l94.8 37.77c9.45 3.79 19.26-4.77 16.81-14.67l-25.7-103.69c16.51-29.05 22.16-62.1 22.16-96.13C414 107.9 325.05 21.5 216 22.01Zm-93.84 175.75c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.84 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Zm94.15-0.44c11.85-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.45 21.55-11.85 0.06-21.46-9.5-21.46-21.35 0-11.85 9.6-21.5 21.46-21.55Zm94.4-0.44c11.84-0.06 21.45 9.5 21.45 21.35 0 11.85-9.6 21.5-21.46 21.55-11.85 0.06-21.45-9.5-21.45-21.35 0-11.85 9.6-21.5 21.45-21.55Z" />
diff --git a/src/conversations/res/drawable/ic_launcher_foreground.xml b/src/conversations/res/drawable/ic_launcher_foreground.xml
index 5851e5f2c06f45ed22d3b15f01d9469a05396c1e..75550aecc7f7febdc24514f284a5801ec777e2a7 100644
--- a/src/conversations/res/drawable/ic_launcher_foreground.xml
+++ b/src/conversations/res/drawable/ic_launcher_foreground.xml
@@ -1,13 +1,13 @@
+ android:width="432dp"
+ android:height="432dp"
+ android:viewportWidth="732"
+ android:viewportHeight="732">
+ android:translateX="150"
+ android:translateY="150">
+ android:pathData="M216 74.92c-79.3 0.37-144 63.8-144 141.73 0 77.93 64.69 140.8 144 140.43 24.66-0.11 43.7-5.26 65.4-13.25l68.95 27.47c6.87 2.76 14.01-3.47 12.22-10.67l-18.68-75.4c12-21.14 16.11-45.17 16.11-69.92 0-77.93-64.7-140.76-144-140.4Zm-68.25 127.8c8.62-0.04 15.6 6.92 15.6 15.53 0 8.62-6.98 15.64-15.6 15.68-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.64 15.6-15.68Zm68.47-0.31c8.62-0.04 15.6 6.9 15.6 15.53 0 8.61-6.98 15.63-15.6 15.67-8.61 0.04-15.6-6.91-15.6-15.53s6.99-15.64 15.6-15.68Zm68.66-0.32c8.61-0.05 15.6 6.9 15.6 15.53 0 8.61-6.99 15.63-15.6 15.67-8.62 0.04-15.6-6.91-15.6-15.53s6.98-15.63 15.6-15.67Z" />
-
+
\ No newline at end of file
diff --git a/src/conversations/res/drawable/ic_launcher_monochrome.xml b/src/conversations/res/drawable/ic_launcher_monochrome.xml
index 56895d60519f596e8d611ca7cefe6a2bfbf10f7f..c7927359d29f59dc5c47922862cb3ef35044f486 100644
--- a/src/conversations/res/drawable/ic_launcher_monochrome.xml
+++ b/src/conversations/res/drawable/ic_launcher_monochrome.xml
@@ -1,13 +1,13 @@
-
-
-
-
+android:width="432dp"
+android:height="432dp"
+android:viewportWidth="732"
+android:viewportHeight="732">
+
+
+
+
\ No newline at end of file
diff --git a/src/conversations/res/values-pt/strings.xml b/src/conversations/res/values-pt/strings.xml
index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..930de12c5bd8f8b308d94d86bd5c5e1a32e46269 100644
--- a/src/conversations/res/values-pt/strings.xml
+++ b/src/conversations/res/values-pt/strings.xml
@@ -1,2 +1,16 @@
-
\ No newline at end of file
+
+ Selecione o provedor XMPP
+ Usar conversations.im
+ Criar nova conta
+ 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.
+ O teu convite do servidor
+ Código de provisionamento formatado incorretamente
+ Toque no botão de partilha para enviar ao seu contacto um convite para %1$s.
+ Se o seu contacto está por perto, também podem scanerizar o código abaixo para aceitar o convite.
+ Junte-se a %1$s e fale comigo: %2$s
+ Partilhar convite com…
+ 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.
+ 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.
+ 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.
+
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 054bd7d0a6c9d535cea8c729971c0a76cb81ce37..f8468b4235d2732b79c6b1d6e6963fd7874cd00a 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -60,6 +60,8 @@
android:name="android.hardware.microphone"
android:required="false" />
+
+
@@ -400,6 +402,8 @@
+
ListenableFuture> successfulAsList(
+ final Collection>> futures) {
+ return Futures.transform(
+ Futures.successfulAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder();
+ for (final Collection list : lists) {
+ if (list == null) {
+ continue;
+ }
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public static ListenableFuture> allAsList(
+ final Collection>> futures) {
+ return Futures.transform(
+ Futures.allAsList(futures),
+ lists -> {
+ final var builder = new ImmutableList.Builder();
+ for (final Collection list : lists) {
+ builder.addAll(list);
+ }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ }
+}
diff --git a/src/main/java/de/gultsch/common/IntMap.java b/src/main/java/de/gultsch/common/IntMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..a7b679abfef8cc385a34414829b2a93255168a3d
--- /dev/null
+++ b/src/main/java/de/gultsch/common/IntMap.java
@@ -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 implements Map {
+
+ private final ImmutableMap inner;
+
+ public IntMap(ImmutableMap 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 keySet() {
+ return this.inner.keySet();
+ }
+
+ @NonNull
+ @Override
+ public Collection values() {
+ return this.inner.values();
+ }
+
+ @NonNull
+ @Override
+ public Set> 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);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java
index 9eca1d69aff86fd3d6dbf768da159c38a03249c4..dc4babf7518cb2cb1abb1e056f5ec7c5b648b8dd 100644
--- a/src/main/java/eu/siacs/conversations/AppSettings.java
+++ b/src/main/java/eu/siacs/conversations/AppSettings.java
@@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.BoolRes;
+import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.google.common.base.Joiner;
@@ -14,6 +15,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.utils.Compatibility;
import java.security.SecureRandom;
+import java.util.Optional;
public class AppSettings {
@@ -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 getAutoAcceptFileSize() {
+ final long autoAcceptFileSize =
+ getLongPreference(AUTO_ACCEPT_FILE_SIZE, R.integer.auto_accept_filesize);
+ return autoAcceptFileSize <= 0 ? Optional.empty() : Optional.of(autoAcceptFileSize);
+ }
+
public synchronized void resetInstallationId() {
final var secureRandom = new SecureRandom();
final var installationId = secureRandom.nextLong();
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 1afa6f482a0baf79b5c2c2850e9383089542b197..8a5111b49f1d8dede58b3ca456a50ae0b0973134 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -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 =
diff --git a/src/main/java/eu/siacs/conversations/android/Device.java b/src/main/java/eu/siacs/conversations/android/Device.java
new file mode 100644
index 0000000000000000000000000000000000000000..92c9a1871e9245bc101e009a25b5045e8a53d319
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/android/Device.java
@@ -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");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index 28c3b0075b991e88628c1e1977545c468288507a..b3c5365dc0399384d01d90b1c1474a1230e31348 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -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 blocklist = new CopyOnWriteArraySet<>();
- public final Set pendingConferenceJoins = new HashSet<>();
- public final Set pendingConferenceLeaves = new HashSet<>();
- public final Set inProgressConferenceJoins = new HashSet<>();
- public final Set 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 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 getFingerprints() {
ArrayList 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 getBlocklist() {
- return this.blocklist;
- }
-
- public void clearBlocklist() {
- getBlocklist().clear();
+ public Set getBlocklist() {
+ final var connection = this.xmppConnection;
+ if (connection == null) {
+ return Collections.emptySet();
+ }
+ return connection.getManager(BlockingManager.class).getBlocklist();
}
public boolean isOnlineAndConnected() {
@@ -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),
diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java
index 0d1ab6361198fad4df90651518fbe6d1119db669..98f53d9dddc407e0d07bbfc69a050ba19ddbd727 100644
--- a/src/main/java/eu/siacs/conversations/entities/Blockable.java
+++ b/src/main/java/eu/siacs/conversations/entities/Blockable.java
@@ -1,11 +1,17 @@
package eu.siacs.conversations.entities;
+import androidx.annotation.NonNull;
import eu.siacs.conversations.xmpp.Jid;
public interface Blockable {
- boolean isBlocked();
- boolean isDomainBlocked();
- Jid getBlockedJid();
- Jid getJid();
- Account getAccount();
+ boolean isBlocked();
+
+ boolean isDomainBlocked();
+
+ @NonNull
+ Jid getBlockedJid();
+
+ Jid getJid();
+
+ Account getAccount();
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
index 3d9d2594b98a6a5a8b8634b8d6639c15e7158568..eab9afffaa0508cc06bcca969c573aab14065e9d 100644
--- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java
+++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java
@@ -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;
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 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 parseFromPubSub(final PubSub pubSub, final Account account) {
- if (pubSub == null) {
- return Collections.emptyMap();
- }
- final var items = pubSub.getItems();
- if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) {
- return Collections.emptyMap();
- }
- final Map bookmarks = new HashMap<>();
- for (final var item : items.getItemMap(Conference.class).entrySet()) {
- final Bookmark bookmark =
- Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
- if (bookmark == null) {
- continue;
- }
- bookmarks.put(bookmark.jid, bookmark);
- }
- return bookmarks;
- }
-
public static Bookmark parse(Element element, Account account) {
Bookmark bookmark = new Bookmark(account);
bookmark.setAttributes(element.getAttributes());
@@ -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;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index 4e7d351e61c2a7a009d96305f85f5ecfefc895cb..6952434c96aadcbf09509774cd17c385b9e4a070 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -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 getGroups(final boolean unique) {
+ public Collection getGroups(final boolean unique) {
final Collection groups = unique ? new HashSet<>() : new ArrayList<>();
for (int i = 0; i < this.groups.length(); ++i) {
try {
@@ -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() {
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 6c5e7e34023448bb51b2de37b54b832f7241393b..b424cc9d4949fffdd5e0fd2bd710a8c213f471e8 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -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")) {
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index 390fc10eeab4827ecbd7d378c356dfbeccaed744..f1166ad253dcd76c68f5dc398fd3e0e21fefe998 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -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_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder()
+ .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_RANKS =
+ new IntMap<>(
+ new ImmutableMap.Builder()
+ .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 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 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 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 sub(List users, int max) {
- ArrayList subset = new ArrayList<>();
- HashSet jids = new HashSet<>();
- for (User user : users) {
- jids.add(user.getAccount().getJid().asBareJid());
- if (user.getRealJid() == null
- || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
+ public static List sub(final List users, final int max) {
+ final var subset = new ArrayList();
+ final var addresses = new HashSet();
+ for (final var user : users) {
+ addresses.add(user.getAccount().getJid().asBareJid());
+ final var address = user.getRealJid();
+ if (address == null || (address.getLocal() != null && addresses.add(address))) {
subset.add(user);
}
if (subset.size() >= max) {
- break;
+ return subset;
}
}
return subset;
@@ -781,7 +826,7 @@ public class MucOptions {
ArrayList 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 hats;
protected String occupantId;
protected boolean online = true;
- public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set hats) {
+ public User(final MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set 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 getHats() {
@@ -1022,11 +975,11 @@ public class MucOptions {
public List getPseudoHats(Context context) {
List 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);
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java
index 97f63d99cfe6eb4a13bb224687a28f0b3309dda4..24117a168956b4531aaf9720b3ccaa6dfde205fa 100644
--- a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java
+++ b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java
@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
import android.content.Context;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
import java.util.Collections;
@@ -13,7 +14,7 @@ public class RawBlockable implements ListItem, Blockable {
private final Account account;
private final Jid jid;
- public RawBlockable(Account account, Jid jid) {
+ public RawBlockable(@NonNull Account account, @NonNull Jid jid) {
this.account = account;
this.jid = jid;
}
@@ -29,6 +30,7 @@ public class RawBlockable implements ListItem, Blockable {
}
@Override
+ @NonNull
public Jid getBlockedJid() {
return this.jid;
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java
index bc4c6b9747c05c8d21c45a4f4623210a991a38b1..c1dc4bfe5a28d375b989b04e56b0c765c1f959c7 100644
--- a/src/main/java/eu/siacs/conversations/entities/Roster.java
+++ b/src/main/java/eu/siacs/conversations/entities/Roster.java
@@ -1,98 +1,17 @@
package eu.siacs.conversations.entities;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
+import androidx.annotation.NonNull;
import eu.siacs.conversations.android.AbstractPhoneContact;
import eu.siacs.conversations.xmpp.Jid;
+import java.util.List;
+public interface Roster {
-public class Roster {
- private final Account account;
- private final HashMap contacts = new HashMap<>();
- private String version = null;
-
- public Roster(Account account) {
- this.account = account;
- }
-
- public Contact getContactFromContactList(Jid jid) {
- if (jid == null) {
- return null;
- }
- synchronized (this.contacts) {
- Contact contact = contacts.get(jid.asBareJid());
- if (contact != null && contact.showInContactList()) {
- return contact;
- } else {
- return null;
- }
- }
- }
-
- public Contact getContact(final Jid jid) {
- synchronized (this.contacts) {
- if (!contacts.containsKey(jid.asBareJid())) {
- Contact contact = new Contact(jid.asBareJid());
- contact.setAccount(account);
- contacts.put(contact.getJid().asBareJid(), contact);
- return contact;
- }
- return contacts.get(jid.asBareJid());
- }
- }
-
- public void clearPresences() {
- for (Contact contact : getContacts()) {
- contact.clearPresences();
- }
- }
-
- public void markAllAsNotInRoster() {
- for (Contact contact : getContacts()) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- }
- }
-
- public List getWithSystemAccounts(Class clazz) {
- int option = Contact.getOption(clazz);
- List with = getContacts();
- for(Iterator iterator = with.iterator(); iterator.hasNext();) {
- Contact contact = iterator.next();
- if (!contact.getOption(option)) {
- iterator.remove();
- }
- }
- return with;
- }
-
- public List getContacts() {
- synchronized (this.contacts) {
- return new ArrayList<>(this.contacts.values());
- }
- }
-
- public void initContact(final Contact contact) {
- if (contact == null) {
- return;
- }
- contact.setAccount(account);
- synchronized (this.contacts) {
- contacts.put(contact.getJid().asBareJid(), contact);
- }
- }
+ List getContacts();
- public void setVersion(String version) {
- this.version = version;
- }
+ List getWithSystemAccounts(Class 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);
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java
index 5c833f603c00117c76af0bc9c08c258d0344b06a..0db1a8bc7a4e9a6bd73ce16d4990e3c5c68a740d 100644
--- a/src/main/java/eu/siacs/conversations/entities/Transferable.java
+++ b/src/main/java/eu/siacs/conversations/entities/Transferable.java
@@ -5,26 +5,27 @@ import java.util.List;
public interface Transferable {
- List VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
- List VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
+ List VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
+ List 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();
}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index 0980159ece0953ce83dfc92492d74350f7d3b441..2a8e69fe34c04a291ca0487d175915d39bb54dc8 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -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 jids = new ArrayList<>();
- jids.add(jid);
- return changeAffiliation(conference, jids, affiliation);
- }
-
- public Iq changeAffiliation(Conversation conference, List 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);
}
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index b686c54beddb100051876488dba11d38f0fe07b7..f5b11dcf00befeb0d439d1120889c08c659a3355 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -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;
}
}
diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
index 0ceddff4efca7d821059459fdd24e1396631b578..041293a421a053b9aa6ceaa5e4763a5de9492457 100644
--- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -1,11 +1,8 @@
package eu.siacs.conversations.generator;
-import android.text.TextUtils;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
@@ -15,50 +12,6 @@ public class PresenceGenerator extends AbstractGenerator {
super(service);
}
- private im.conversations.android.xmpp.model.stanza.Presence subscription(
- String type, Contact contact) {
- im.conversations.android.xmpp.model.stanza.Presence packet =
- new im.conversations.android.xmpp.model.stanza.Presence();
- packet.setAttribute("type", type);
- packet.setTo(contact.getJid());
- packet.setFrom(contact.getAccount().getJid().asBareJid());
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
- final Contact contact) {
- return requestPresenceUpdatesFrom(contact, null);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
- final Contact contact, final String preAuth) {
- im.conversations.android.xmpp.model.stanza.Presence packet =
- subscription("subscribe", contact);
- String displayName = contact.getAccount().getDisplayName();
- if (!TextUtils.isEmpty(displayName)) {
- packet.addChild("nick", Namespace.NICK).setContent(displayName);
- }
- if (preAuth != null) {
- packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
- }
- return packet;
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(
- Contact contact) {
- return subscription("unsubscribe", contact);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(
- Contact contact) {
- return subscription("unsubscribed", contact);
- }
-
- public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(
- Contact contact) {
- return subscription("subscribed", contact);
- }
-
public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Availability status) {
return selfPresence(account, status, true, null);
}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
index 238cc5a0130f267bb9eebd25e4de06470d16f0d2..671e3a30302b297867ee728371daa17842b8a854 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
@@ -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);
}
-
}
}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
index 2ef2a3368c2f5dce4cbc0320e49db90688612295..c8586624921df79c516fb7f257337039b597be99 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
@@ -17,6 +17,7 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.manager.HttpUploadManager;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -39,12 +40,12 @@ public class HttpUploadConnection
private boolean delayed = false;
private DownloadableFile file;
private final Message message;
- private SlotRequester.Slot slot;
+ private HttpUploadManager.Slot slot;
private byte[] key = null;
private long transmitted = 0;
private Call mostRecentCall;
- private ListenableFuture slotFuture;
+ private ListenableFuture slotFuture;
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 slotFuture = this.slotFuture;
+ final ListenableFuture 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 slotFuture = this.slotFuture;
+ final Future 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();
diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java
deleted file mode 100644
index 5bb5d7772b253857e7cc8eab81c60c4aedb9d1c2..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.http;
-
-import android.util.Log;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import im.conversations.android.xmpp.model.upload.Header;
-import im.conversations.android.xmpp.model.upload.Slot;
-import java.util.Map;
-import okhttp3.Headers;
-import okhttp3.HttpUrl;
-
-public class SlotRequester {
-
- private final XmppConnectionService service;
-
- public SlotRequester(XmppConnectionService service) {
- this.service = service;
- }
-
- public ListenableFuture request(
- final Account account, final DownloadableFile file, final String mime) {
- final var result =
- account.getXmppConnection()
- .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
- if (result == null) {
- return Futures.immediateFailedFuture(
- new IllegalStateException("No HTTP upload host found"));
- }
- return requestHttpUpload(account, result.getKey(), file, mime);
- }
-
- private ListenableFuture requestHttpUpload(
- final Account account, final Jid host, final DownloadableFile file, final String mime) {
- final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
- final var iqFuture = service.sendIqPacket(account, request);
- return Futures.transform(
- iqFuture,
- response -> {
- final var slot =
- response.getExtension(
- im.conversations.android.xmpp.model.upload.Slot.class);
- if (slot == null) {
- Log.d(Config.LOGTAG, "-->" + response);
- throw new IllegalStateException("Slot not found in IQ response");
- }
- final var getUrl = slot.getGetUrl();
- final var put = slot.getPut();
- if (getUrl == null || put == null) {
- throw new IllegalStateException("Missing get or put in slot response");
- }
- final var putUrl = put.getUrl();
- if (putUrl == null) {
- throw new IllegalStateException("Missing put url");
- }
- final var headers = new ImmutableMap.Builder();
- for (final Header header : put.getHeaders()) {
- final String name = header.getHeaderName();
- final String value = header.getContent();
- if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
- continue;
- }
- headers.put(name, value.trim());
- }
- headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
- return new Slot(putUrl, getUrl, headers.buildKeepingLast());
- },
- MoreExecutors.directExecutor());
- }
-
- public static class Slot {
- public final HttpUrl put;
- public final HttpUrl get;
- public final Headers headers;
-
- private Slot(HttpUrl put, HttpUrl get, Headers headers) {
- this.put = put;
- this.get = get;
- this.headers = headers;
- }
-
- private Slot(HttpUrl put, HttpUrl getUrl, Map headers) {
- this.put = put;
- this.get = getUrl;
- this.headers = Headers.of(headers);
- }
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
index a057fc65c1b6614f5785d7c15f53f8bae06f478b..5616790c4c6151ee963a1594521236c91d16b239 100644
--- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -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;
}
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 67b859643aee6865ac0b6ad39c6934b4048ea7eb..f6646c6954caef2f104337a9a7a72b235577c57e 100644
--- a/src/main/java/eu/siacs/conversations/parser/IqParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -7,24 +7,33 @@ import com.google.common.base.CharMatcher;
import com.google.common.io.BaseEncoding;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
+import im.conversations.android.xmpp.model.blocking.Block;
+import im.conversations.android.xmpp.model.blocking.Unblock;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.ibb.InBandByteStream;
+import im.conversations.android.xmpp.model.ping.Ping;
+import im.conversations.android.xmpp.model.roster.Query;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.time.Time;
+import im.conversations.android.xmpp.model.up.Push;
import im.conversations.android.xmpp.model.version.Version;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -58,76 +67,6 @@ public class IqParser extends AbstractParser implements Consumer {
super(service, connection);
}
- public static List items(final Iq packet) {
- ArrayList items = new ArrayList<>();
- final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
- if (query == null) {
- return items;
- }
- for (Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- Jid jid = child.getAttributeAsJid("jid");
- if (jid != null) {
- items.add(jid);
- }
- }
- }
- return items;
- }
-
- private void rosterItems(final Account account, final Element query) {
- final String version = query.getAttribute("ver");
- if (version != null) {
- account.getRoster().setVersion(version);
- }
- for (final Element item : query.getChildren()) {
- if (item.getName().equals("item")) {
- final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid == null) {
- continue;
- }
- final String name = item.getAttribute("name");
- final String subscription = item.getAttribute("subscription");
- final Contact contact = account.getRoster().getContact(jid);
- boolean bothPre =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
- contact.setServerName(name);
- contact.parseGroupsFromElement(item);
- }
- if ("remove".equals(subscription)) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_DELETE);
- contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
- } else {
- contact.setOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_PUSH);
- contact.parseSubscriptionFromElement(item);
- }
- boolean both =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if ((both != bothPre) && both) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": gained mutual presence subscription with "
- + contact.getJid());
- AxolotlService axolotlService = account.getAxolotlService();
- if (axolotlService != null) {
- axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
- }
- }
- mXmppConnectionService.getAvatarService().clear(contact);
- }
- }
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateRosterUi(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 {
@Override
public void accept(final Iq packet) {
- final var account = getAccount();
- final boolean isGet = packet.getType() == Iq.Type.GET;
- if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
- return;
+ final var type = packet.getType();
+ switch (type) {
+ case SET -> acceptPush(packet);
+ case GET -> acceptRequest(packet);
+ default ->
+ throw new AssertionError(
+ "IQ results and errors should are handled in callbacks");
}
- if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
- final Element query = packet.findChild("query");
- // If this is in response to a query for the whole roster:
- if (packet.getType() == Iq.Type.RESULT) {
- account.getRoster().markAllAsNotInRoster();
- }
- this.rosterItems(account, query);
- } else if ((packet.hasChild("block", Namespace.BLOCKING)
- || packet.hasChild("blocklist", Namespace.BLOCKING))
- && packet.fromServer(account)) {
- // Block list or block push.
- Log.d(Config.LOGTAG, "Received blocklist update from server");
- final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING);
- final Element block = packet.findChild("block", Namespace.BLOCKING);
- final Collection items =
- blocklist != null
- ? blocklist.getChildren()
- : (block != null ? block.getChildren() : null);
- // If this is a response to a blocklist query, clear the block list and replace with the
- // new one.
- // Otherwise, just update the existing blocklist.
- if (packet.getType() == Iq.Type.RESULT) {
- account.clearBlocklist();
- connection.getFeatures().setBlockListRequested(true);
- }
- if (items != null) {
- final Collection jids = new ArrayList<>(items.size());
- // Create a collection of Jids from the packet
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().addAll(jids);
- if (packet.getType() == Iq.Type.SET) {
- boolean removed = false;
- for (Jid jid : jids) {
- removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
- }
- if (removed) {
- mXmppConnectionService.updateConversationUi();
- }
- }
- }
- // Update the UI
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
- if (packet.getType() == Iq.Type.SET) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- }
- } else if (packet.hasChild("unblock", Namespace.BLOCKING)
- && packet.fromServer(account)
- && packet.getType() == Iq.Type.SET) {
- Log.d(Config.LOGTAG, "Received unblock update from server");
- final Collection items =
- packet.findChild("unblock", Namespace.BLOCKING).getChildren();
- if (items.isEmpty()) {
- // No children to unblock == unblock all
- account.getBlocklist().clear();
- } else {
- final Collection jids = new ArrayList<>(items.size());
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().removeAll(jids);
- }
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
- || packet.hasChild("data", "http://jabber.org/protocol/ibb")
- || packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
- mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet);
- } else if (packet.hasExtension(InfoQuery.class) && isGet) {
+ }
+
+ private void acceptPush(final Iq packet) {
+ if (packet.hasExtension(Query.class)) {
+ this.getManager(RosterManager.class).push(packet);
+ } else if (packet.hasExtension(Block.class)) {
+ this.getManager(BlockingManager.class).pushBlock(packet);
+ } else if (packet.hasExtension(Unblock.class)) {
+ this.getManager(BlockingManager.class).pushUnblock(packet);
+ } else if (packet.hasExtension(InBandByteStream.class)) {
+ mXmppConnectionService
+ .getJingleConnectionManager()
+ .deliverIbbPacket(getAccount(), packet);
+ } else if (packet.hasExtension(Push.class)) {
+ this.getManager(UnifiedPushManager.class).push(packet);
+ } else {
+ this.connection.sendErrorFor(
+ packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
+ }
+ }
+
+ private void acceptRequest(final Iq packet) {
+ if (packet.hasExtension(InfoQuery.class)) {
this.getManager(DiscoManager.class).handleInfoQuery(packet);
- } else if (packet.hasExtension(Version.class) && isGet) {
+ } else if (packet.hasExtension(Version.class)) {
this.getManager(DiscoManager.class).handleVersionRequest(packet);
- } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
- final Iq response;
- if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
- } else {
- response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH)
- && packet.getType() == Iq.Type.SET) {
- final Jid transport = packet.getFrom();
- final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
- final boolean success =
- push != null
- && mXmppConnectionService.processUnifiedPushMessage(
- account, transport, push);
- final Iq response;
- if (success) {
- response = packet.generateResponse(Iq.Type.RESULT);
- } else {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.setAttribute("code", "404");
- error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.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());
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 1430c37ca06feeb4ba4f30cc387b31a16b0232b2..ced874bca7157629a8ce2bc954d6eb0aa9d3a03a 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -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 deviceIds = deviceList.getDeviceIds();
- Log.d(
- Config.LOGTAG,
- AxolotlService.getLogprefix(account)
- + "Received PEP device list "
- + deviceIds
- + " update from "
- + from
- + ", processing... ");
- final AxolotlService axolotlService = account.getAxolotlService();
- axolotlService.registerDevices(from, new HashSet<>(deviceIds));
- }
-
- } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
- final var connection = account.getXmppConnection();
- if (connection.getFeatures().bookmarksConversion()) {
- if (connection.getFeatures().bookmarks2()) {
- Log.w(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received storage:bookmark notification even though we"
- + " opted into bookmarks:1");
- }
- final var storage = items.getFirstItem(Storage.class);
- final Map bookmarks = Bookmark.parseFromStorage(storage, account);
- mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": processing bookmark PEP event");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ignoring bookmark PEP event because bookmark conversion was"
- + " not detected");
- }
- } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- final var retractions = items.getRetractions();
- for (final var item : items.getItemMap(Conference.class).entrySet()) {
- final Bookmark bookmark =
- Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
- if (bookmark == null) {
- continue;
- }
- account.putBookmark(bookmark);
- mXmppConnectionService.processModifiedBookmark(bookmark);
- mXmppConnectionService.updateConversationUi();
- }
- for (final var retract : retractions) {
- final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
- if (id != null) {
- account.removeBookmark(id);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": deleted bookmark for " + id);
- mXmppConnectionService.processDeletedBookmark(account, id);
- mXmppConnectionService.updateConversationUi();
- }
- }
- } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
- && Namespace.MDS_DISPLAYED.equals(node)
- && account.getJid().asBareJid().equals(from)) {
- for (final var item :
- items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
- .entrySet()) {
- mXmppConnectionService.processMdsItem(account, item);
- }
- }
- }
-
- private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
- final String node = delete.getNode();
- if (Namespace.NICK.equals(node)) {
- setNick(account, from, null);
- } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
- deleteAllBookmarks(account);
- } else if (Namespace.AVATAR_METADATA.equals(node)) {
- final boolean isAccount = account.getJid().asBareJid().equals(from);
- if (isAccount) {
- account.setAvatar(null);
- mXmppConnectionService.databaseBackend.updateAccount(account);
- mXmppConnectionService.getAvatarService().clear(account);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": deleted avatar metadata node");
- }
- }
- }
-
- private void parsePurgeEvent(
- @NonNull final Purge purge, final Jid from, final Account account) {
- final String node = purge.getNode();
- if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
- deleteAllBookmarks(account);
- }
- }
-
- private void deleteAllBookmarks(final Account account) {
- final var previous = account.getBookmarkedJids();
- account.setBookmarks(Collections.emptyMap());
- mXmppConnectionService.processDeletedBookmarks(account, previous);
- }
-
- private void setNick(final Account account, final Jid user, final String nick) {
- if (user.asBareJid().equals(account.getJid().asBareJid())) {
- account.setDisplayName(nick);
- if (QuickConversationsService.isQuicksy()) {
- mXmppConnectionService.getAvatarService().clear(account);
- }
- mXmppConnectionService.checkMucRequiresRename();
- } else {
- Contact contact = account.getRoster().getContact(user);
- if (contact.setPresenceName(nick)) {
- 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 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;
}
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
index daf13632241f9fb04486c13e6e48dfed27141800..41dc4840b775a0fa81881c5f2a3e6c83e19745b7 100644
--- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -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 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 =
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index bf1412faa267f72c40906898c500945864d24dd1..f8d1a090e863c1314a5a9abbe3442a024275cf9c 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -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 readRoster(final Account account) {
+ final var builder = new ImmutableMap.Builder();
final SQLiteDatabase db = this.getReadableDatabase();
- final String[] args = {roster.getAccount().getUuid()};
+ final String[] args = {account.getUuid()};
try (final Cursor cursor =
db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
while (cursor.moveToNext()) {
- roster.initContact(Contact.fromCursor(cursor));
+ final var contact = Contact.fromCursor(cursor);
+ if (contact != null) {
+ contact.setAccount(account);
+ builder.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 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(
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 2a681837fc09f318686c2ac5cb9243265073262a..cc2c5604815fa95bf03b67fe6e5543b3c1301c4a 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -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 attachments, long max) {
+ Context context, List 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 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 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 =
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
index 037b31dfadf8ccfe858f6b145ca3e90553e1cf3f..487b53326e35ea8f30a63a9bdd7ced1d9a53e682 100644
--- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -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;
}
diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java
index 8f9662e6a19ba5cff4946182c461a7919971c56b..8898658a763505f3144bbe6d6b5b07d5c4a7ae25 100644
--- a/src/main/java/eu/siacs/conversations/services/AvatarService.java
+++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java
@@ -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();
}
diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java
index 419dca3dfc66720c2326b358996493a89699f810..3197bfb0efad711dc0a066ecfed1a3e5f8d56bc7 100644
--- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java
+++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java
@@ -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);
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
index db05a7a877e7a746551b6d0d72a09bcfe887e6b6..3b52eb7a8f9a65c77a4daa5ef72db1c63a629db8 100644
--- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
+++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
@@ -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 pending = new ArrayList<>();
synchronized (this.pendingQueries) {
for (Iterator 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;
+ }
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 485f22e8fa935309ad411b84e2d1c4bc4e1cf402..62c498266124eac55de7d4f70bf0f2d944966063 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -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;
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
index 24aaf1f4279b04abd32fbf3da615283c0dda3a32..41d3ca7cbc1bf5c9134c3f7186ea3b2c2bde275f 100644
--- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
@@ -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)) {
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 87e261b2dc3759dff039500023544fed138de84d..451e1550547426d88a439cc83bb6fe35af121b21 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -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 conversations = new CopyOnWriteArrayList<>();
private final IqGenerator mIqGenerator = new IqGenerator(this);
private final Set mInProgressAvatarFetches = new HashSet<>();
private final Set mOmittedPepAvatarFetches = new HashSet<>();
- private final HashSet mLowPingTimeoutMode = new HashSet<>();
- private final Consumer mDefaultIqHandler =
- (packet) -> {
- if (packet.getType() != Iq.Type.RESULT) {
- final var error = packet.getError();
- String text = error != null ? error.findChildContent("text") : null;
- if (text != null) {
- Log.d(Config.LOGTAG, "received iq error: " + text);
- }
- }
- };
+ public final HashSet mLowPingTimeoutMode = new HashSet<>();
public DatabaseBackend databaseBackend;
private Multimap 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 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 conversations = getConversations();
- for (Conversation conversation : conversations) {
- final boolean inProgressJoin;
- synchronized (account.inProgressConferenceJoins) {
- inProgressJoin =
- account.inProgressConferenceJoins.contains(conversation);
- }
- final boolean pendingJoin;
- synchronized (account.pendingConferenceJoins) {
- pendingJoin = account.pendingConferenceJoins.contains(conversation);
- }
- if (conversation.getAccount() == account
- && !pendingJoin
- && !inProgressJoin) {
- sendUnsentMessages(conversation);
- }
- }
- final List pendingLeaves;
- synchronized (account.pendingConferenceLeaves) {
- pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
- account.pendingConferenceLeaves.clear();
- }
- for (Conversation conversation : pendingLeaves) {
- leaveMuc(conversation);
- }
- final List pendingJoins;
- synchronized (account.pendingConferenceJoins) {
- pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
- account.pendingConferenceJoins.clear();
- }
- for (Conversation conversation : pendingJoins) {
- joinMuc(conversation);
- }
- scheduleWakeUpCall(
- Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
- } else if (account.getStatus() == Account.State.OFFLINE
- || account.getStatus() == Account.State.DISABLED
- || account.getStatus() == Account.State.LOGGED_OUT) {
- resetSendingToWaiting(account);
- if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": went into offline state during low ping mode."
- + " reconnecting now");
- reconnectAccount(account, true, false);
- } else {
- final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
- scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
- }
- } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
- databaseBackend.updateAccount(account);
- reconnectAccount(account, true, false);
- } else if (account.getStatus() != Account.State.CONNECTING
- && account.getStatus() != Account.State.NO_INTERNET) {
- resetSendingToWaiting(account);
- if (connection != null && account.getStatus().isAttemptReconnect()) {
- final boolean aggressive =
- account.getStatus() == Account.State.SEE_OTHER_HOST
- || hasJingleRtpConnection(account);
- final int next = connection.getTimeToNextAttempt(aggressive);
- final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
- if (next <= 0) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error connecting account. reconnecting now."
- + " lowPingTimeout="
- + lowPingTimeoutMode);
- reconnectAccount(account, true, false);
- } else {
- final int attempt = connection.getAttempt() + 1;
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error connecting account. try again in "
- + next
- + "s for the "
- + attempt
- + " time. lowPingTimeout="
- + lowPingTimeoutMode
- + ", aggressive="
- + aggressive);
- scheduleWakeUpCall(next, account.getUuid().hashCode());
- if (aggressive) {
- internalPingExecutor.schedule(
- XmppConnectionService.this
- ::manageAccountConnectionStatesInternal,
- (next * 1000L) + 50,
- TimeUnit.MILLISECONDS);
- }
- }
- }
- }
- getNotificationService().updateErrorNotification();
- }
- };
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 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 callback =
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final var privateStorage = response.getExtension(PrivateStorage.class);
- if (privateStorage == null) {
- return;
- }
- final var bookmarkStorage = privateStorage.getExtension(Storage.class);
- Map bookmarks =
- Bookmark.parseFromStorage(bookmarkStorage, account);
- processBookmarksInitial(account, bookmarks, false);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": could not fetch bookmarks");
- }
- };
- sendIqPacket(account, iqPacket, callback);
- }
-
- public void fetchBookmarks2(final Account account) {
- final Iq retrieve = mIqGenerator.retrieveBookmarks();
- sendIqPacket(
- account,
- retrieve,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final var pubsub = response.getExtension(PubSub.class);
- final Map bookmarks =
- Bookmark.parseFromPubSub(pubsub, account);
- processBookmarksInitial(account, bookmarks, true);
- }
- });
- }
-
- public void fetchMessageDisplayedSynchronization(final Account account) {
- Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
- final var retrieve = mIqGenerator.retrieveMds();
- sendIqPacket(
- account,
- retrieve,
- (response) -> {
- if (response.getType() != Iq.Type.RESULT) {
- return;
- }
- final var pubsub = response.getExtension(PubSub.class);
- if (pubsub == null) {
- return;
- }
- final var items = pubsub.getItems();
- if (items == null) {
- return;
- }
- if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
- for (final var item :
- items.getItemMap(
- im.conversations.android.xmpp.model.mds.Displayed
- .class)
- .entrySet()) {
- processMdsItem(account, item);
- }
- }
- });
- }
-
- public void processMdsItem(final Account account, final Map.Entry item) {
- final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
- if (jid == null) {
- return;
- }
- final var displayed = item.getValue();
- final var stanzaId = displayed.getStanzaId();
- final String id = stanzaId == null ? null : stanzaId.getId();
- final Conversation conversation = find(account, jid);
- if (id != null && conversation != null) {
- conversation.setDisplayState(id);
- markReadUpToStanzaId(conversation, id);
- }
- }
-
public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
final Message message = conversation.findMessageWithServerMsgId(stanzaId);
if (message == null) { // do we want to check if isRead?
@@ -2739,250 +2351,15 @@ public class XmppConnectionService extends Service {
return true;
}
- public void processBookmarksInitial(
- final Account account, final Map bookmarks, final boolean pep) {
- final Set previousBookmarks = account.getBookmarkedJids();
- for (final Bookmark bookmark : bookmarks.values()) {
- previousBookmarks.remove(bookmark.getJid().asBareJid());
- processModifiedBookmark(bookmark, pep);
- }
- if (pep) {
- processDeletedBookmarks(account, previousBookmarks);
- }
- account.setBookmarks(bookmarks);
- }
-
- public void processDeletedBookmarks(final Account account, final Collection bookmarks) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": "
- + bookmarks.size()
- + " bookmarks have been removed");
- for (final Jid bookmark : bookmarks) {
- processDeletedBookmark(account, bookmark);
- }
- }
-
- public void processDeletedBookmark(final Account account, final Jid jid) {
- final Conversation conversation = find(account, jid);
- if (conversation == null) {
- return;
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
- archiveConversation(conversation, false);
- }
-
- private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
- 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() {
@@ -3004,54 +2381,71 @@ public class XmppConnectionService extends Service {
conversations.remove(conversation);
}
}
- long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
- Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
- Runnable runnable = () -> {
- if (DatabaseBackend.requiresMessageIndexRebuild()) {
- DatabaseBackend.getInstance(this).rebuildMessagesIndex();
- }
- mutedMucUsers = databaseBackend.loadMutedMucUsers();
- final long deletionDate = getAutomaticMessageDeletionDate();
- mLastExpiryRun.set(SystemClock.elapsedRealtime());
- if (deletionDate > 0) {
- Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
- databaseBackend.expireOldMessages(deletionDate);
- }
- Log.d(Config.LOGTAG, "restoring roster...");
- for (final Account account : accounts) {
- databaseBackend.readRoster(account.getRoster());
- account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
- }
- getDrawableCache().evictAll();
- loadPhoneContacts();
- Log.d(Config.LOGTAG, "restoring messages...");
- final long startMessageRestore = SystemClock.elapsedRealtime();
- final Conversation quickLoad = QuickLoader.get(this.conversations);
- if (quickLoad != null) {
- restoreMessages(quickLoad);
- updateConversationUi();
- final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
- Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
- }
- for (Conversation conversation : this.conversations) {
- if (quickLoad != conversation) {
- restoreMessages(conversation);
- }
- }
- mNotificationService.finishBacklog();
- restoredFromDatabaseLatch.countDown();
- final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
- Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
- updateConversationUi();
- };
- mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
- }
- }
-
- private void restoreMessages(Conversation conversation) {
- conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
- conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
- conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
+ long diffConversationsRestore =
+ SystemClock.elapsedRealtime() - startTimeConversationsRestore;
+ Log.d(
+ Config.LOGTAG,
+ "finished restoring conversations in " + diffConversationsRestore + "ms");
+ Runnable runnable =
+ () -> {
+ if (DatabaseBackend.requiresMessageIndexRebuild()) {
+ DatabaseBackend.getInstance(this).rebuildMessagesIndex();
+ }
+ mutedMucUsers = databaseBackend.loadMutedMucUsers();
+ final long deletionDate = getAutomaticMessageDeletionDate();
+ mLastExpiryRun.set(SystemClock.elapsedRealtime());
+ if (deletionDate > 0) {
+ Log.d(
+ Config.LOGTAG,
+ "deleting messages that are older than "
+ + AbstractGenerator.getTimestamp(deletionDate));
+ databaseBackend.expireOldMessages(deletionDate);
+ }
+ Log.d(Config.LOGTAG, "restoring roster...");
+ for (final Account account : accounts) {
+ account.getXmppConnection().getManager(RosterManager.class).restore();
+ }
+ getDrawableCache().evictAll();
+ loadPhoneContacts();
+ Log.d(Config.LOGTAG, "restoring messages...");
+ final long startMessageRestore = SystemClock.elapsedRealtime();
+ final Conversation quickLoad = QuickLoader.get(this.conversations);
+ if (quickLoad != null) {
+ restoreMessages(quickLoad);
+ updateConversationUi();
+ final long diffMessageRestore =
+ SystemClock.elapsedRealtime() - startMessageRestore;
+ Log.d(
+ Config.LOGTAG,
+ "quickly restored "
+ + quickLoad.getName()
+ + " after "
+ + diffMessageRestore
+ + "ms");
+ }
+ for (Conversation conversation : this.conversations) {
+ if (quickLoad != conversation) {
+ restoreMessages(conversation);
+ }
+ }
+ mNotificationService.finishBacklog();
+ restoredFromDatabaseLatch.countDown();
+ final long diffMessageRestore =
+ SystemClock.elapsedRealtime() - startMessageRestore;
+ Log.d(
+ Config.LOGTAG,
+ "finished restoring messages in " + diffMessageRestore + "ms");
+ updateConversationUi();
+ };
+ mDatabaseReaderExecutor.execute(
+ runnable); // will contain one write command (expiry) but that's fine
+ }
+ }
+
+ private void restoreMessages(Conversation conversation) {
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
+ conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
}
public void loadPhoneContacts() {
@@ -3059,9 +2453,13 @@ public class XmppConnectionService extends Service {
() -> {
final Map contacts = JabberIdContact.load(this);
Log.d(Config.LOGTAG, "start merging phone contacts with roster");
+ // TODO if we do this merge this only on enabled accounts we need to trigger
+ // this upon enable
for (final Account account : accounts) {
- final List withSystemAccounts =
- account.getRoster().getWithSystemAccounts(JabberIdContact.class);
+ final var remaining =
+ new ArrayList<>(
+ account.getRoster()
+ .getWithSystemAccounts(JabberIdContact.class));
for (final JabberIdContact jidContact : contacts.values()) {
final Contact contact =
account.getRoster().getContact(jidContact.getJid());
@@ -3069,9 +2467,9 @@ public class XmppConnectionService extends Service {
if (needsCacheClean) {
getAvatarService().clear(contact);
}
- withSystemAccounts.remove(contact);
+ remaining.remove(contact);
}
- for (final Contact contact : withSystemAccounts) {
+ for (final Contact contact : remaining) {
boolean needsCacheClean =
contact.unsetPhoneContact(JabberIdContact.class);
if (needsCacheClean) {
@@ -3087,14 +2485,6 @@ public class XmppConnectionService extends Service {
});
}
- public void syncRoster(final Account account) {
- mRosterSyncTaskManager.execute(account, () -> {
- unregisterPhoneAccounts(account);
- databaseBackend.writeRoster(account.getRoster());
- try { Thread.sleep(500); } catch (InterruptedException e) { }
- });
- }
-
public List getConversations() {
return this.conversations;
}
@@ -3317,11 +2707,13 @@ public class XmppConnectionService extends Service {
public void maybeRegisterWithMuc(Conversation c, String nickArg) {
final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
final var register = new Iq(Iq.Type.GET);
- register.query(Namespace.REGISTER);
+ final var query0 = register.addChild("query");
+ query0.setAttribute("xmlns", Namespace.REGISTER);
register.setTo(c.getJid().asBareJid());
sendIqPacket(c.getAccount(), register, (response) -> {
if (response.getType() == Iq.Type.RESULT) {
- final Element query = response.query(Namespace.REGISTER);
+ final Element query = response.addChild("query");
+ query.setAttribute("xmlns", Namespace.REGISTER);
String username = query.findChildContent("username", Namespace.REGISTER);
if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
if (username != null && username.equals(nick)) {
@@ -3346,7 +2738,9 @@ public class XmppConnectionService extends Service {
form.put("muc#register_roomnick", nick);
form.submit();
final var finish = new Iq(Iq.Type.SET);
- finish.query(Namespace.REGISTER).addChild(form);
+ final var query2 = finish.addChild("query");
+ query2.setAttribute("xmlns", Namespace.REGISTER);
+ query2.addChild(form);
finish.setTo(c.getJid().asBareJid());
sendIqPacket(c.getAccount(), finish, (response2) -> {
if (response.getType() == Iq.Type.RESULT) {
@@ -3368,7 +2762,9 @@ public class XmppConnectionService extends Service {
public void deregisterWithMuc(Conversation c) {
final Iq register = new Iq(Iq.Type.GET);
- register.query(Namespace.REGISTER).addChild("remove");
+ final var query = register.addChild("query");
+ query.setAttribute("xmlns", Namespace.REGISTER);
+ query.addChild("remove");
register.setTo(c.getJid().asBareJid());
sendIqPacket(c.getAccount(), register, (response) -> {
if (response.getType() == Iq.Type.RESULT) {
@@ -3483,7 +2879,9 @@ public class XmppConnectionService extends Service {
null));
this.conversations.add(existing);
if (existing.getMode() == Conversational.MODE_MULTI) {
- ensureBookmarkIsAutoJoin(existing);
+ account.getXmppConnection()
+ .getManager(BookmarkManager.class)
+ .ensureBookmarkIsAutoJoin(existing);
}
updateConversationUi();
return existing;
@@ -3540,21 +2938,24 @@ public class XmppConnectionService extends Service {
archiveConversation(conversation, true);
}
- private void archiveConversation(
+ public void archiveConversation(
Conversation conversation, final boolean maySynchronizeWithBookmarks) {
if (isOnboarding()) return;
+ final var account = conversation.getAccount();
+ final var connection = account.getXmppConnection();
getNotificationService().clear(conversation);
conversation.setStatus(Conversation.STATUS_ARCHIVED);
conversation.setNextMessage(null);
synchronized (this.conversations) {
getMessageArchiveService().kill(conversation);
if (conversation.getMode() == Conversation.MODE_MULTI) {
+ // TODO always clean up bookmarks no matter if we are currently connected
+ // TODO always delete reference to conversation in bookmark
if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
final Bookmark bookmark = conversation.getBookmark();
if (maySynchronizeWithBookmarks && bookmark != null) {
if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
- Account account = bookmark.getAccount();
bookmark.setConversation(null);
deleteBookmark(account, bookmark);
} else if (bookmark.autojoin()) {
@@ -3564,7 +2965,7 @@ public class XmppConnectionService extends Service {
}
}
deregisterWithMuc(conversation);
- leaveMuc(conversation);
+ connection.getManager(MultiUserChatManager.class).leave(conversation);
} else {
if (conversation
.getContact()
@@ -3578,14 +2979,17 @@ public class XmppConnectionService extends Service {
}
}
- public void stopPresenceUpdatesTo(Contact contact) {
+ public void stopPresenceUpdatesTo(final Contact contact) {
Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
- sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+ contact.getAccount()
+ .getXmppConnection()
+ .getManager(PresenceManager.class)
+ .unsubscribed(contact.getJid().asBareJid());
}
public void createAccount(final Account account) {
- account.initAccountServices(this);
+ account.setXmppConnection(createConnection(account));
databaseBackend.createAccount(account);
if (CallIntegration.hasSystemFeature(this)) {
CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
@@ -3733,7 +3137,8 @@ public class XmppConnectionService extends Service {
getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
}
account.setShowErrorNotification(true);
- this.statusListener.onStatusChanged(account);
+ // TODO what was the purpose of that? will likely be triggered by reconnect anyway?
+ // this.statusListener.onStatusChanged(account);
databaseBackend.updateAccount(account);
reconnectAccountInBackground(account);
updateAccountUi();
@@ -3750,41 +3155,10 @@ public class XmppConnectionService extends Service {
}
}
- public void updateAccountPasswordOnServer(
- final Account account,
- final String newPassword,
- final OnAccountPasswordChanged callback) {
- final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
- sendIqPacket(
- account,
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- account.setPassword(newPassword);
- account.setOption(Account.OPTION_MAGIC_CREATE, false);
- databaseBackend.updateAccount(account);
- callback.onPasswordChangeSucceeded();
- } else {
- callback.onPasswordChangeFailed();
- }
- });
- }
-
- public void unregisterAccount(final Account account, final Consumer callback) {
- final Iq iqPacket = new Iq(Iq.Type.SET);
- final Element query = iqPacket.addChild("query", Namespace.REGISTER);
- query.addChild("remove");
- sendIqPacket(
- account,
- iqPacket,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- deleteAccount(account);
- callback.accept(true);
- } else {
- callback.accept(false);
- }
- });
+ public ListenableFuture updateAccountPasswordOnServer(
+ final Account account, final String newPassword) {
+ final var connection = account.getXmppConnection();
+ return connection.getManager(RegistrationManager.class).setPassword(newPassword);
}
public void deleteAccount(final Account account) {
@@ -3798,7 +3172,9 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (connected) {
- leaveMuc(conversation);
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .unavailable(conversation);
}
}
conversations.remove(conversation);
@@ -3826,7 +3202,6 @@ public class XmppConnectionService extends Service {
if (CallIntegration.hasSystemFeature(this)) {
CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
}
- this.mRosterSyncTaskManager.clear(account);
updateAccountUi();
mNotificationService.updateErrorNotification();
syncEnabledAccountSetting();
@@ -4093,7 +3468,7 @@ public class XmppConnectionService extends Service {
private void switchToForeground() {
toggleSoftDisabled(false);
- final boolean broadcastLastActivity = broadcastLastActivity();
+ final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
for (Conversation conversation : getConversations()) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
conversation.getMucOptions().resetChatState();
@@ -4101,45 +3476,41 @@ public class XmppConnectionService extends Service {
conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
}
}
- for (Account account : getAccounts()) {
- if (account.getStatus() == Account.State.ONLINE) {
- account.deactivateGracePeriod();
- final XmppConnection connection = account.getXmppConnection();
- if (connection != null) {
- if (connection.getFeatures().csi()) {
- connection.sendActive();
- }
- if (broadcastLastActivity) {
- sendPresence(
- account,
- false); // send new presence but don't include idle because we are
- // not
- }
- }
+ for (final var account : getAccounts()) {
+ if (account.getStatus() != Account.State.ONLINE) {
+ continue;
+ }
+ account.deactivateGracePeriod();
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection.getFeatures().csi()) {
+ connection.sendActive();
+ }
+ if (broadcastLastActivity) {
+ // send new presence but don't include idle because we are not
+ connection.getManager(PresenceManager.class).available(false);
}
}
Log.d(Config.LOGTAG, "app switched into foreground");
}
private void switchToBackground() {
- final boolean broadcastLastActivity = broadcastLastActivity();
+ final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
if (broadcastLastActivity) {
mLastActivity = System.currentTimeMillis();
final SharedPreferences.Editor editor = getPreferences().edit();
editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
editor.apply();
}
- for (Account account : getAccounts()) {
- if (account.getStatus() == Account.State.ONLINE) {
- XmppConnection connection = account.getXmppConnection();
- if (connection != null) {
- if (broadcastLastActivity) {
- sendPresence(account, true);
- }
- if (connection.getFeatures().csi()) {
- connection.sendInactive();
- }
- }
+ for (final var account : getAccounts()) {
+ if (account.getStatus() != Account.State.ONLINE) {
+ continue;
+ }
+ final var connection = account.getXmppConnection();
+ if (broadcastLastActivity) {
+ connection.getManager(PresenceManager.class).available(true);
+ }
+ if (connection.getFeatures().csi()) {
+ connection.sendInactive();
}
}
this.mNotificationService.setIsInForeground(false);
@@ -4156,439 +3527,75 @@ public class XmppConnectionService extends Service {
}
}
- public void mucSelfPingAndRejoin(final Conversation conversation) {
- final Account account = conversation.getAccount();
- synchronized (account.inProgressConferenceJoins) {
- if (account.inProgressConferenceJoins.contains(conversation)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": canceling muc self ping because join is already under way");
- return;
- }
- }
- synchronized (account.inProgressConferencePings) {
- if (!account.inProgressConferencePings.add(conversation)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": canceling muc self ping because ping is already under way");
- return;
- }
- }
- // TODO use PingManager
- final Jid self = conversation.getMucOptions().getSelf().getFullJid();
- final Iq ping = new Iq(Iq.Type.GET);
- ping.setTo(self);
- ping.addChild("ping", Namespace.PING);
- sendIqPacket(
- conversation.getAccount(),
- ping,
- (response) -> {
- if (response.getType() == Iq.Type.ERROR) {
- final var error = response.getError();
- if (error == null
- || error.hasChild("service-unavailable")
- || error.hasChild("feature-not-implemented")
- || error.hasChild("item-not-found")) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " came back as ignorable error");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " failed. attempting rejoin");
- joinMuc(conversation);
- }
- } else if (response.getType() == Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": ping to "
- + self
- + " came back fine");
- }
- synchronized (account.inProgressConferencePings) {
- account.inProgressConferencePings.remove(conversation);
- }
- });
- }
-
- public void joinMuc(Conversation conversation) {
- joinMuc(conversation, null, false);
- }
-
- public void joinMuc(Conversation conversation, boolean followedInvite) {
- joinMuc(conversation, null, followedInvite);
+ public void joinMuc(final Conversation conversation) {
+ final var account = conversation.getAccount();
+ account.getXmppConnection().getManager(MultiUserChatManager.class).join(conversation);
}
- private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
- joinMuc(conversation, onConferenceJoined, false);
+ public void providePasswordForMuc(final Conversation conversation, final String password) {
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .setPassword(conversation, password);
}
- private void joinMuc(
- final Conversation conversation,
- final OnConferenceJoined onConferenceJoined,
- final boolean followedInvite) {
- final Account account = conversation.getAccount();
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.remove(conversation);
- }
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.remove(conversation);
- }
- if (account.getStatus() == Account.State.ONLINE) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.add(conversation);
- }
- if (Config.MUC_LEAVE_BEFORE_JOIN) {
- sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
- }
- conversation.resetMucOptions();
- if (onConferenceJoined != null) {
- conversation.getMucOptions().flagNoAutoPushConfiguration();
- }
- conversation.setHasMessagesLeftOnServer(false);
- fetchConferenceConfiguration(
- conversation,
- new OnConferenceConfigurationFetched() {
-
- private void join(Conversation conversation) {
- Account account = conversation.getAccount();
- final MucOptions mucOptions = conversation.getMucOptions();
-
- if (mucOptions.nonanonymous()
- && !mucOptions.membersOnly()
- && !conversation.getBooleanAttribute(
- "accept_non_anonymous", false)) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- }
- mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
- updateConversationUi();
- if (onConferenceJoined != null) {
- onConferenceJoined.onConferenceJoined(conversation);
- }
- return;
- }
-
- final Jid joinJid = mucOptions.getSelf().getFullJid();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": joining conversation "
- + joinJid.toString());
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence
- .Availability.ONLINE,
- mucOptions.nonanonymous()
- || onConferenceJoined != null,
- mucOptions.getSelf().getNick());
- packet.setTo(joinJid);
- Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
- if (conversation.getMucOptions().getPassword() != null) {
- x.addChild("password").setContent(mucOptions.getPassword());
- }
-
- if (mucOptions.mamSupport()) {
- // Use MAM instead of the limited muc history to get history
- x.addChild("history").setAttribute("maxchars", "0");
- } else {
- // Fallback to muc history
- x.addChild("history")
- .setAttribute(
- "since",
- PresenceGenerator.getTimestamp(
- conversation
- .getLastMessageTransmitted()
- .getTimestamp()));
- }
- sendPresencePacket(account, packet);
- if (onConferenceJoined != null) {
- onConferenceJoined.onConferenceJoined(conversation);
- }
- if (!joinJid.equals(conversation.getJid())) {
- conversation.setContactJid(joinJid);
- databaseBackend.updateConversation(conversation);
- }
-
- maybeRegisterWithMuc(conversation, null);
-
- if (mucOptions.mamSupport()) {
- getMessageArchiveService().catchupMUC(conversation);
- }
- fetchConferenceMembers(conversation);
- if (mucOptions.isPrivateAndNonAnonymous()) {
- if (followedInvite) {
- final Bookmark bookmark = conversation.getBookmark();
- if (bookmark != null) {
- if (!bookmark.autojoin()) {
- bookmark.setAutojoin(true);
- createBookmark(account, bookmark);
- }
- } else {
- saveConversationAsBookmark(conversation, null);
- }
- }
- }
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- sendUnsentMessages(conversation);
- }
- }
-
- @Override
- public void onConferenceConfigurationFetched(Conversation conversation) {
- if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": conversation ("
- + conversation.getJid()
- + ") got archived before IQ result");
- return;
- }
- join(conversation);
- }
-
- @Override
- public void onFetchFailed(
- final Conversation conversation, final String errorCondition) {
- if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": conversation ("
- + conversation.getJid()
- + ") got archived before IQ result");
- return;
- }
- if ("remote-server-not-found".equals(errorCondition)) {
- synchronized (account.inProgressConferenceJoins) {
- account.inProgressConferenceJoins.remove(conversation);
- }
- conversation
- .getMucOptions()
- .setError(MucOptions.Error.SERVER_NOT_FOUND);
- updateConversationUi();
- } else {
- join(conversation);
- fetchConferenceConfiguration(conversation);
- }
- }
- });
- updateConversationUi();
- } else {
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.add(conversation);
- }
- conversation.resetMucOptions();
- conversation.setHasMessagesLeftOnServer(false);
- updateConversationUi();
- }
- }
+ public void deleteAvatar(final Account account) {
+ final var connection = account.getXmppConnection();
- private void fetchConferenceMembers(final Conversation conversation) {
- final Account account = conversation.getAccount();
- final AxolotlService axolotlService = account.getAxolotlService();
- final var affiliations = new ArrayList();
- affiliations.add("outcast");
- if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
- final Consumer callback =
- new Consumer() {
+ final var vCardPhotoDeletionFuture =
+ connection.getManager(VCardManager.class).deletePhoto();
+ final var pepDeletionFuture = connection.getManager(AvatarManager.class).delete();
- private int i = 0;
- private boolean success = true;
+ final var deletionFuture = Futures.allAsList(vCardPhotoDeletionFuture, pepDeletionFuture);
+ Futures.addCallback(
+ deletionFuture,
+ new FutureCallback<>() {
@Override
- public void accept(Iq response) {
- final boolean omemoEnabled =
- conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
- Element query = response.query("http://jabber.org/protocol/muc#admin");
- if (response.getType() == Iq.Type.RESULT && query != null) {
- for (Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- MucOptions.User user =
- AbstractParser.parseItem(conversation, child);
- user.setOnline(false);
- if (!user.realJidMatchesAccount()) {
- boolean isNew =
- conversation.getMucOptions().updateUser(user);
- Contact contact = user.getContact();
- if (omemoEnabled
- && isNew
- && user.getRealJid() != null
- && (contact == null
- || !contact.mutualPresenceSubscription())
- && axolotlService.hasEmptyDeviceList(
- user.getRealJid())) {
- axolotlService.fetchDeviceIds(user.getRealJid());
- }
- }
- }
- }
- } else {
- success = false;
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not request affiliation "
- + affiliations.get(i)
- + " in "
- + conversation.getJid().asBareJid());
- }
- ++i;
- if (i >= affiliations.size()) {
- final var mucOptions = conversation.getMucOptions();
- List members = mucOptions.getMembers(true);
- if (success) {
- List cryptoTargets = conversation.getAcceptedCryptoTargets();
- boolean changed = false;
- for (ListIterator iterator = cryptoTargets.listIterator();
- iterator.hasNext(); ) {
- Jid jid = iterator.next();
- if (!members.contains(jid)
- && !members.contains(jid.getDomain())) {
- iterator.remove();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removed "
- + jid
- + " from crypto targets of "
- + conversation.getName());
- changed = true;
- }
- }
- if (changed) {
- conversation.setAcceptedCryptoTargets(cryptoTargets);
- updateConversation(conversation);
- }
- }
- getAvatarService().clear(mucOptions);
- updateMucRosterUi();
- updateConversationUi();
- }
- }
- };
- for (String affiliation : affiliations) {
- sendIqPacket(
- account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
- }
-
- public void providePasswordForMuc(final Conversation conversation, final String password) {
- if (conversation.getMode() == Conversation.MODE_MULTI) {
- conversation.getMucOptions().setPassword(password);
- if (conversation.getBookmark() != null) {
- final Bookmark bookmark = conversation.getBookmark();
- bookmark.setAutojoin(true);
- createBookmark(conversation.getAccount(), bookmark);
- }
- updateConversation(conversation);
- joinMuc(conversation);
- }
- }
-
- public void deleteAvatar(final Account account) {
- final AtomicBoolean executed = new AtomicBoolean(false);
- final Runnable onDeleted =
- () -> {
- if (executed.compareAndSet(false, true)) {
+ public void onSuccess(List result) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": deleted avatar from server");
account.setAvatar(null);
databaseBackend.updateAccount(account);
getAvatarService().clear(account);
updateAccountUi();
}
- };
- deleteVcardAvatar(account, onDeleted);
- deletePepNode(account, Namespace.AVATAR_DATA);
- deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
- }
- public void deletePepNode(final Account account, final String node) {
- deletePepNode(account, node, null);
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not delete avatar",
+ t);
+ }
+ },
+ MoreExecutors.directExecutor());
}
- private void deletePepNode(final Account account, final String node, final Runnable runnable) {
- final Iq request = mIqGenerator.deleteNode(node);
- sendIqPacket(
- account,
- request,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
+ public void deletePepNode(final Account account, final String node) {
+ final var future = account.getXmppConnection().getManager(PepManager.class).delete(node);
+ Futures.addCallback(
+ future,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(Void result) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": successfully deleted pep node "
+ node);
- if (runnable != null) {
- runnable.run();
- }
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": failed to delete " + packet);
}
- });
- }
- private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
- final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
- sendIqPacket(
- account,
- retrieveVcard,
- (response) -> {
- if (response.getType() != Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": no vCard set. nothing to do");
- return;
- }
- final Element vcard = response.findChild("vCard", "vcard-temp");
- if (vcard == null) {
+ @Override
+ public void onFailure(@NonNull Throwable t) {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid() + ": no vCard set. nothing to do");
- return;
- }
- Element photo = vcard.findChild("PHOTO");
- if (photo == null) {
- photo = vcard.addChild("PHOTO");
+ account.getJid().asBareJid() + ": failed to delete node " + node,
+ t);
}
- photo.clearChildren();
- final Iq publication = new Iq(Iq.Type.SET);
- publication.setTo(account.getJid().asBareJid());
- publication.addChild(vcard);
- sendIqPacket(
- account,
- publication,
- (publicationResponse) -> {
- if (publicationResponse.getType() == Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": successfully deleted vcard avatar");
- runnable.run();
- } else {
- Log.d(
- Config.LOGTAG,
- "failed to publish vcard "
- + publicationResponse.getErrorCondition());
- }
- });
- });
+ },
+ MoreExecutors.directExecutor());
}
private boolean hasEnabledAccounts() {
@@ -4661,297 +3668,45 @@ public class XmppConnectionService extends Service {
createBookmark(bookmark.getAccount(), bookmark);
}
- public void presenceToMuc(final Conversation conversation) {
- final MucOptions options = conversation.getMucOptions();
- if (options.online()) {
- Account account = conversation.getAccount();
- final Jid joinJid = options.getSelf().getFullJid();
- final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), options.getSelf().getNick());
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
+ public void checkMucRequiresRename() {
+ synchronized (this.conversations) {
+ for (final Conversation conversation : this.conversations) {
+ if (conversation.getMode() == Conversational.MODE_MULTI) {
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .checkMucRequiresRename(conversation);
+ }
+ }
}
}
- public boolean renameInMuc(
- final Conversation conversation,
- final String nick,
+ public void createPublicChannel(
+ final Account account,
+ final String name,
+ final Jid address,
final UiCallback callback) {
- final Account account = conversation.getAccount();
- final Bookmark bookmark = conversation.getBookmark();
- final MucOptions options = conversation.getMucOptions();
- final Jid joinJid = options.createJoinJid(nick);
- if (joinJid == null) {
- return false;
- }
- if (options.online()) {
- maybeRegisterWithMuc(conversation, nick);
- options.setOnRenameListener(
- new OnRenameListener() {
-
- @Override
- public void onSuccess() {
- final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), nick);
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
- callback.success(conversation);
- }
+ final var future =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .createPublicChannel(address, name);
- @Override
- public void onFailure() {
- callback.error(R.string.nick_in_use, conversation);
- }
- });
-
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
- options.nonanonymous(), nick);
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
- if (nick.equals(MucOptions.defaultNick(account))
- && bookmark != null
- && bookmark.getNick() != null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": removing nick from bookmark for "
- + bookmark.getJid());
- bookmark.setNick(null);
- createBookmark(account, bookmark);
- }
- } else {
- conversation.setContactJid(joinJid);
- databaseBackend.updateConversation(conversation);
- if (account.getStatus() == Account.State.ONLINE) {
- if (bookmark != null) {
- bookmark.setNick(nick);
- createBookmark(account, bookmark);
- }
- joinMuc(conversation);
- }
- }
- return true;
- }
-
- public void checkMucRequiresRename() {
- synchronized (this.conversations) {
- for (final Conversation conversation : this.conversations) {
- if (conversation.getMode() == Conversational.MODE_MULTI) {
- checkMucRequiresRename(conversation);
- }
- }
- }
- }
-
- private void checkMucRequiresRename(final Conversation conversation) {
- final var options = conversation.getMucOptions();
- if (!options.online()) {
- return;
- }
- final var account = conversation.getAccount();
- final String current = options.getActualNick();
- final String proposed = options.getProposedNickPure();
- if (current == null || current.equals(proposed)) {
- return;
- }
- final Jid joinJid = options.createJoinJid(proposed);
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: muc rename required %s (was: %s)",
- account.getJid().asBareJid(), joinJid, current));
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
- options.nonanonymous(), proposed);
- packet.setTo(joinJid);
- sendPresencePacket(account, packet);
- }
-
- public void leaveMuc(Conversation conversation) {
- leaveMuc(conversation, false);
- }
-
- private void leaveMuc(Conversation conversation, boolean now) {
- final Account account = conversation.getAccount();
- synchronized (account.pendingConferenceJoins) {
- account.pendingConferenceJoins.remove(conversation);
- }
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.remove(conversation);
- }
- if (account.getStatus() == Account.State.ONLINE || now) {
- sendPresencePacket(
- conversation.getAccount(),
- mPresenceGenerator.leave(conversation.getMucOptions()));
- conversation.getMucOptions().setOffline();
- Bookmark bookmark = conversation.getBookmark();
- if (bookmark != null) {
- bookmark.setConversation(null);
- }
- Log.d(
- Config.LOGTAG,
- conversation.getAccount().getJid().asBareJid()
- + ": leaving muc "
- + conversation.getJid());
- final var connection = account.getXmppConnection();
- if (connection != null) {
- connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
- }
- } else {
- synchronized (account.pendingConferenceLeaves) {
- account.pendingConferenceLeaves.add(conversation);
- }
- }
- }
-
- public String findConferenceServer(final Account account) {
- String server;
- if (account.getXmppConnection() != null) {
- server = account.getXmppConnection().getMucServer();
- if (server != null) {
- return server;
- }
- }
- for (Account other : getAccounts()) {
- if (other != account && other.getXmppConnection() != null) {
- server = other.getXmppConnection().getMucServer();
- if (server != null) {
- return server;
- }
- }
- }
- return null;
- }
-
- public void createPublicChannel(
- final Account account,
- final String name,
- final Jid address,
- final UiCallback callback) {
- joinMuc(
- findOrCreateConversation(account, address, true, false, true),
- conversation -> {
- final Bundle configuration = IqGenerator.defaultChannelConfiguration();
- if (!TextUtils.isEmpty(name)) {
- configuration.putString("muc#roomconfig_roomname", name);
+ Futures.addCallback(
+ future,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(Conversation result) {
+ callback.success(result);
}
- pushConferenceConfiguration(
- conversation,
- configuration,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- saveConversationAsBookmark(conversation, name);
- callback.success(conversation);
- }
- @Override
- public void onPushFailed() {
- if (conversation
- .getMucOptions()
- .getSelf()
- .getAffiliation()
- .ranks(MucOptions.Affiliation.OWNER)) {
- callback.error(
- R.string.unable_to_set_channel_configuration,
- conversation);
- } else {
- callback.error(
- R.string.joined_an_existing_channel, conversation);
- }
- }
- });
- });
- }
-
- public boolean createAdhocConference(
- final Account account,
- final String name,
- final Iterable jids,
- final UiCallback callback) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": creating adhoc conference with "
- + jids.toString());
- if (account.getStatus() == Account.State.ONLINE) {
- try {
- String server = findConferenceServer(account);
- if (server == null) {
- if (callback != null) {
- callback.error(R.string.no_conference_server_found, null);
+ @Override
+ public void onFailure(Throwable t) {
+ Log.d(Config.LOGTAG, "could not create public channel", t);
+ // TODO I guess it’s better to just not use callbacks here
+ callback.error(R.string.unable_to_set_channel_configuration, null);
}
- return false;
- }
- final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
- final Conversation conversation =
- findOrCreateConversation(account, jid, true, false, true);
- joinMuc(
- conversation,
- new OnConferenceJoined() {
- @Override
- public void onConferenceJoined(final Conversation conversation) {
- final Bundle configuration =
- IqGenerator.defaultGroupChatConfiguration();
- if (!TextUtils.isEmpty(name)) {
- configuration.putString("muc#roomconfig_roomname", name);
- }
- pushConferenceConfiguration(
- conversation,
- configuration,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- for (Jid invite : jids) {
- invite(conversation, invite);
- }
- for (String resource :
- account.getSelfContact()
- .getPresences()
- .toResourceArray()) {
- Jid other =
- account.getJid().withResource(resource);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": sending direct invite to "
- + other);
- directInvite(conversation, other);
- }
- saveConversationAsBookmark(conversation, name);
- if (callback != null) {
- callback.success(conversation);
- }
- }
-
- @Override
- public void onPushFailed() {
- archiveConversation(conversation);
- if (callback != null) {
- callback.error(
- R.string.conference_creation_failed,
- conversation);
- }
- }
- });
- }
- });
- return true;
- } catch (IllegalArgumentException e) {
- if (callback != null) {
- callback.error(R.string.conference_creation_failed, null);
- }
- return false;
- }
- } else {
- if (callback != null) {
- callback.error(R.string.not_connected_try_again, null);
- }
- return false;
- }
+ },
+ MoreExecutors.directExecutor());
}
public void checkIfMuc(final Account account, final Jid jid, Consumer cb) {
@@ -4992,97 +3747,34 @@ public class XmppConnectionService extends Service {
);
}
- public void fetchConferenceConfiguration(final Conversation conversation) {
- fetchConferenceConfiguration(conversation, null);
- }
-
- public void fetchConferenceConfiguration(
- final Conversation conversation, final OnConferenceConfigurationFetched callback) {
- final var account = conversation.getAccount();
- final var connection = account.getXmppConnection();
- if (connection == null) {
- return;
+ public boolean createAdhocConference(
+ final Account account,
+ final String name,
+ final Collection addresses,
+ final UiCallback callback) {
+ final var manager = account.getXmppConnection().getManager(MultiUserChatManager.class);
+ if (manager.getServices().isEmpty()) {
+ return false;
}
- final var future =
- connection
- .getManager(DiscoManager.class)
- .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
+
+ final var future = manager.createPrivateGroupChat(name, addresses);
+
Futures.addCallback(
future,
new FutureCallback<>() {
@Override
- public void onSuccess(InfoQuery result) {
- final MucOptions mucOptions = conversation.getMucOptions();
- final Bookmark bookmark = conversation.getBookmark();
- final boolean sameBefore =
- StringUtils.equals(
- bookmark == null ? null : bookmark.getBookmarkName(),
- mucOptions.getName());
-
- final var hadOccupantId = mucOptions.occupantId();
- if (mucOptions.updateConfiguration(result)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": muc configuration changed for "
- + conversation.getJid().asBareJid());
- updateConversation(conversation);
- }
-
- final var hasOccupantId = mucOptions.occupantId();
-
- if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
- final var me = mucOptions.getSelf().getFullJid();
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": gained support for occupant-id in "
- + me
- + ". resending presence");
- final var packet =
- mPresenceGenerator.selfPresence(
- account,
- im.conversations.android.xmpp.model.stanza.Presence
- .Availability.ONLINE,
- mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
- packet.setTo(me);
- sendPresencePacket(account, packet);
- }
-
- if (bookmark != null
- && (sameBefore || bookmark.getBookmarkName() == null)) {
- if (bookmark.setBookmarkName(
- StringUtils.nullOnEmpty(mucOptions.getName()))) {
- createBookmark(account, bookmark);
- }
- }
-
- if (callback != null) {
- callback.onConferenceConfigurationFetched(conversation);
- }
-
- updateConversationUi();
+ public void onSuccess(Conversation result) {
+ callback.success(result);
}
@Override
- public void onFailure(@NonNull Throwable throwable) {
- if (throwable instanceof TimeoutException) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received timeout waiting for conference"
- + " configuration fetch");
- } else if (throwable
- instanceof IqErrorResponseException errorResponseException) {
- if (callback != null) {
- callback.onFetchFailed(
- conversation,
- errorResponseException.getResponse().getErrorCondition());
- }
- }
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not create private group chat", t);
+ callback.error(R.string.conference_creation_failed, null);
}
},
MoreExecutors.directExecutor());
+ return true;
}
public void pushNodeConfiguration(
@@ -5144,65 +3836,11 @@ public class XmppConnectionService extends Service {
});
}
- public void pushConferenceConfiguration(
- final Conversation conversation,
- final Bundle options,
- final OnConfigurationPushed callback) {
- if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
- conversation.setAttribute("accept_non_anonymous", true);
- updateConversation(conversation);
- }
- if (options.containsKey("muc#roomconfig_moderatedroom")) {
- final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
- options.putString("members_by_default", moderated ? "0" : "1");
- }
- if (options.containsKey("muc#roomconfig_allowpm")) {
- // ejabberd :-/
- final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
- options.putString("allow_private_messages", allow ? "1" : "0");
- options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
- }
- final var account = conversation.getAccount();
- final Iq request = new Iq(Iq.Type.GET);
- request.setTo(conversation.getJid().asBareJid());
- request.query("http://jabber.org/protocol/muc#owner");
- sendIqPacket(
- account,
- request,
- response -> {
- if (response.getType() == Iq.Type.RESULT) {
- final Data data =
- Data.parse(response.query().findChild("x", Namespace.DATA));
- data.submit(options);
- final Iq set = new Iq(Iq.Type.SET);
- set.setTo(conversation.getJid().asBareJid());
- set.query("http://jabber.org/protocol/muc#owner").addChild(data);
- sendIqPacket(
- account,
- set,
- packet -> {
- if (callback != null) {
- if (packet.getType() == Iq.Type.RESULT) {
- callback.onPushSucceeded();
- } else {
- Log.d(Config.LOGTAG, "failed: " + packet);
- callback.onPushFailed();
- }
- }
- });
- } else {
- if (callback != null) {
- callback.onPushFailed();
- }
- }
- });
- }
-
public void pushSubjectToConference(final Conversation conference, final String subject) {
- final var packet =
- this.getMessageGenerator()
- .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
- this.sendMessagePacket(conference.getAccount(), packet);
+ final var account = conference.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .setSubject(conference, subject);
}
public void requestVoice(final Account account, final Jid jid) {
@@ -5213,49 +3851,46 @@ public class XmppConnectionService extends Service {
public void changeAffiliationInConference(
final Conversation conference,
Jid user,
- final MucOptions.Affiliation affiliation,
+ final Affiliation affiliation,
final OnAffiliationChanged callback) {
- final Jid jid = user.asBareJid();
- final Iq request =
- this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
- sendIqPacket(
- conference.getAccount(),
- request,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final var mucOptions = conference.getMucOptions();
- mucOptions.changeAffiliation(jid, affiliation);
- getAvatarService().clear(mucOptions);
+ final var account = conference.getAccount();
+ final var future =
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .setAffiliation(conference, affiliation, user);
+ Futures.addCallback(
+ future,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(Void result) {
if (callback != null) {
- callback.onAffiliationChangedSuccessful(jid);
+ callback.onAffiliationChangedSuccessful(user);
} else {
Log.d(
Config.LOGTAG,
"changed affiliation of " + user + " to " + affiliation);
}
- } else if (callback != null) {
- callback.onAffiliationChangeFailed(
- jid, R.string.could_not_change_affiliation);
- } else {
- Log.d(Config.LOGTAG, "unable to change affiliation");
}
- });
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (callback != null) {
+ callback.onAffiliationChangeFailed(
+ user, R.string.could_not_change_affiliation);
+ } else {
+ Log.d(Config.LOGTAG, "could not change affiliation", t);
+ }
+ }
+ },
+ MoreExecutors.directExecutor());
}
public void changeRoleInConference(
- final Conversation conference, final String nick, MucOptions.Role role) {
+ final Conversation conference, final String nick, Role role) {
final var account = conference.getAccount();
- final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
- sendIqPacket(
- account,
- request,
- (packet) -> {
- if (packet.getType() != Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + " unable to change role of " + nick);
- }
- });
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .setRole(conference.getJid().asBareJid(), role, nick);
}
public void moderateMessage(final Account account, final Message m, final String reason) {
@@ -5268,689 +3903,164 @@ public class XmppConnectionService extends Service {
});
}
- public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
- final Iq request = new Iq(Iq.Type.SET);
- request.setTo(conversation.getJid().asBareJid());
- request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
- sendIqPacket(
- conversation.getAccount(),
- request,
- response -> {
- if (response.getType() == Iq.Type.RESULT) {
- if (callback != null) {
- callback.onRoomDestroySucceeded();
- }
- } else if (response.getType() == Iq.Type.ERROR) {
- if (callback != null) {
- callback.onRoomDestroyFailed();
- }
- }
- });
- }
-
- private void disconnect(final Account account, boolean force) {
- final XmppConnection connection = account.getXmppConnection();
- if (connection == null) {
- return;
- }
- if (!force) {
- final List conversations = getConversations();
- for (Conversation conversation : conversations) {
- if (conversation.getAccount() == account) {
- if (conversation.getMode() == Conversation.MODE_MULTI) {
- leaveMuc(conversation, true);
- }
- }
- }
- sendOfflinePresence(account);
- }
- connection.disconnect(force);
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mBinder;
- }
-
- public void deleteMessage(Message message) {
- mScheduledMessages.remove(message.getUuid());
- databaseBackend.deleteMessage(message.getUuid());
- ((Conversation) message.getConversation()).remove(message);
- updateConversationUi();
- }
-
- public void updateMessage(Message message) {
- updateMessage(message, true);
- }
-
- public void updateMessage(Message message, boolean includeBody) {
- databaseBackend.updateMessage(message, includeBody);
- updateConversationUi();
- }
-
- public void createMessageAsync(final Message message) {
- mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
- }
-
- public void updateMessage(Message message, String uuid) {
- if (!databaseBackend.updateMessage(message, uuid)) {
- Log.e(Config.LOGTAG, "error updated message in DB after edit");
- }
- updateConversationUi();
- }
-
- public void syncDirtyContacts(Account account) {
- for (Contact contact : account.getRoster().getContacts()) {
- if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
- pushContactToServer(contact);
- }
- if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
- deleteContactOnServer(contact);
- }
- }
- }
-
- protected void unregisterPhoneAccounts(final Account account) {
- for (final Contact contact : account.getRoster().getContacts()) {
- if (!contact.showInRoster()) {
- contact.unregisterAsPhoneAccount(this);
- }
- }
- }
-
- public void createContact(final Contact contact, final boolean autoGrant) {
- createContact(contact, autoGrant, null);
- }
-
- public void createContact(
- final Contact contact, final boolean autoGrant, final String preAuth) {
- if (autoGrant) {
- contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
- contact.setOption(Contact.Options.ASKING);
- }
- pushContactToServer(contact, preAuth);
- }
-
- public void pushContactToServer(final Contact contact) {
- pushContactToServer(contact, null);
- }
-
- private void pushContactToServer(final Contact contact, final String preAuth) {
- contact.resetOption(Contact.Options.DIRTY_DELETE);
- contact.setOption(Contact.Options.DIRTY_PUSH);
- final Account account = contact.getAccount();
- if (account.getStatus() == Account.State.ONLINE) {
- final boolean ask = contact.getOption(Contact.Options.ASKING);
- final boolean sendUpdates =
- contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
- && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
- final Iq iq = new Iq(Iq.Type.SET);
- iq.query(Namespace.ROSTER).addChild(contact.asElement());
- account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
- if (sendUpdates) {
- sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
- }
- if (ask) {
- sendPresencePacket(
- account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
- }
- } else {
- syncRoster(contact.getAccount());
- }
- }
-
- public void publishMucAvatar(
- final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
- new Thread(
- () -> {
- final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
- final int size = Config.AVATAR_SIZE;
- final Avatar avatar =
- getFileBackend().getPepAvatar(image, size, format);
- if (avatar != null) {
- if (!getFileBackend().save(avatar)) {
- callback.onAvatarPublicationFailed(
- R.string.error_saving_avatar);
- return;
- }
- avatar.owner = conversation.getJid().asBareJid();
- publishMucAvatar(conversation, avatar, callback);
- } else {
- callback.onAvatarPublicationFailed(
- R.string.error_publish_avatar_converting);
- }
- })
- .start();
- }
-
- public void publishAvatarAsync(
- final Account account,
- final Uri image,
- final boolean open,
- final OnAvatarPublication callback) {
- new Thread(() -> publishAvatar(account, image, open, callback)).start();
- }
-
- private void publishAvatar(
- final Account account,
- final Uri image,
- final boolean open,
- final OnAvatarPublication callback) {
- final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
- final int size = Config.AVATAR_SIZE;
- final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
- if (avatar != null) {
- if (!getFileBackend().save(avatar)) {
- Log.d(Config.LOGTAG, "unable to save vcard");
- callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
- return;
- }
- publishAvatar(account, avatar, open, callback);
- } else {
- callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
- }
- }
-
- private void publishMucAvatar(
- Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
+ public ListenableFuture destroyRoom(final Conversation conversation) {
final var account = conversation.getAccount();
- final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
- sendIqPacket(
- account,
- retrieve,
- (response) -> {
- boolean itemNotFound =
- response.getType() == Iq.Type.ERROR
- && response.hasChild("error")
- && response.findChild("error").hasChild("item-not-found");
- if (response.getType() == Iq.Type.RESULT || itemNotFound) {
- Element vcard = response.findChild("vCard", "vcard-temp");
- if (vcard == null) {
- vcard = new Element("vCard", "vcard-temp");
- }
- Element photo = vcard.findChild("PHOTO");
- if (photo == null) {
- photo = vcard.addChild("PHOTO");
- }
- photo.clearChildren();
- photo.addChild("TYPE").setContent(avatar.type);
- photo.addChild("BINVAL").setContent(avatar.image);
- final Iq publication = new Iq(Iq.Type.SET);
- publication.setTo(conversation.getJid().asBareJid());
- publication.addChild(vcard);
- sendIqPacket(
- account,
- publication,
- (publicationResponse) -> {
- if (publicationResponse.getType() == Iq.Type.RESULT) {
- callback.onAvatarPublicationSucceeded();
- } else {
- Log.d(
- Config.LOGTAG,
- "failed to publish vcard "
- + publicationResponse.getErrorCondition());
- callback.onAvatarPublicationFailed(
- R.string.error_publish_avatar_server_reject);
- }
- });
- } else {
- Log.d(Config.LOGTAG, "failed to request vcard " + response);
- callback.onAvatarPublicationFailed(
- R.string.error_publish_avatar_no_server_support);
- }
- });
- }
-
- public void publishAvatar(
- final Account account,
- final Avatar avatar,
- final boolean open,
- final OnAvatarPublication callback) {
- final Bundle options;
- if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
- options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
- } else {
- options = null;
- }
- publishAvatar(account, avatar, options, true, callback);
- }
-
- public void publishAvatar(
- Account account,
- final Avatar avatar,
- final Bundle options,
- final boolean retry,
- final OnAvatarPublication callback) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": publishing avatar. options=" + options);
- final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
- this.sendIqPacket(
- account,
- packet,
- result -> {
- if (result.getType() == Iq.Type.RESULT) {
- publishAvatarMetadata(account, avatar, options, true, callback);
- } else if (retry && PublishOptions.preconditionNotMet(result)) {
- pushNodeConfiguration(
- account,
- Namespace.AVATAR_DATA,
- options,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": changed node configuration for avatar"
- + " node");
- publishAvatar(account, avatar, options, false, callback);
- }
-
- @Override
- public void onPushFailed() {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to change node configuration"
- + " for avatar node");
- publishAvatar(account, avatar, null, false, callback);
- }
- });
- } else {
- Element error = result.findChild("error");
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": server rejected avatar "
- + (avatar.size / 1024)
- + "KiB "
- + (error != null ? error.toString() : ""));
- if (callback != null) {
- callback.onAvatarPublicationFailed(
- R.string.error_publish_avatar_server_reject);
- }
- }
- });
- }
-
- public void publishAvatarMetadata(
- Account account,
- final Avatar avatar,
- final Bundle options,
- final boolean retry,
- final OnAvatarPublication callback) {
- final Iq packet =
- XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
- sendIqPacket(
- account,
- packet,
- result -> {
- if (result.getType() == Iq.Type.RESULT) {
- if (account.setAvatar(avatar.getFilename())) {
- getAvatarService().clear(account);
- databaseBackend.updateAccount(account);
- notifyAccountAvatarHasChanged(account);
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": published avatar "
- + (avatar.size / 1024)
- + "KiB");
- if (callback != null) {
- callback.onAvatarPublicationSucceeded();
- }
- } else if (retry && PublishOptions.preconditionNotMet(result)) {
- pushNodeConfiguration(
- account,
- Namespace.AVATAR_METADATA,
- options,
- new OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": changed node configuration for avatar"
- + " meta data node");
- publishAvatarMetadata(
- account, avatar, options, false, callback);
- }
-
- @Override
- public void onPushFailed() {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to change node configuration"
- + " for avatar meta data node");
- publishAvatarMetadata(
- account, avatar, null, false, callback);
- }
- });
- } else {
- if (callback != null) {
- callback.onAvatarPublicationFailed(
- R.string.error_publish_avatar_server_reject);
- }
- }
- });
+ return account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .destroy(conversation.getJid().asBareJid());
}
- public void republishAvatarIfNeeded(final Account account) {
- if (account.getAxolotlService().isPepBroken()) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": skipping republication of avatar because pep is broken");
+ private void disconnect(final Account account, boolean force) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection == null) {
return;
}
- final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
- this.sendIqPacket(
- account,
- packet,
- new Consumer() {
-
- private Avatar parseAvatar(final Iq packet) {
- final var pubsub = packet.getExtension(PubSub.class);
- if (pubsub == null) {
- return null;
- }
- final var items = pubsub.getItems();
- if (items == null) {
- return null;
- }
- final var item = items.getFirstItemWithId(Metadata.class);
- if (item == null) {
- return null;
- }
- return Avatar.parseMetadata(item.getKey(), item.getValue());
+ if (!force) {
+ final List conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .unavailable(conversation);
}
+ }
+ }
+ connection.getManager(PresenceManager.class).unavailable();
+ }
+ connection.disconnect(force);
+ }
- private boolean errorIsItemNotFound(Iq packet) {
- Element error = packet.findChild("error");
- return packet.getType() == Iq.Type.ERROR
- && error != null
- && error.hasChild("item-not-found");
- }
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
- @Override
- public void accept(final Iq packet) {
- if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
- final Avatar serverAvatar = parseAvatar(packet);
- if (serverAvatar == null && account.getAvatar() != null) {
- final Avatar avatar =
- fileBackend.getStoredPepAvatar(account.getAvatar());
- if (avatar != null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": avatar on server was null. republishing");
- // publishing as 'open' - old server (that requires
- // republication) likely doesn't support access models anyway
- publishAvatar(
- account,
- fileBackend.getStoredPepAvatar(account.getAvatar()),
- true,
- null);
- } else {
- Log.e(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": error rereading avatar");
- }
- }
- }
- }
- });
+ public void deleteMessage(Message message) {
+ mScheduledMessages.remove(message.getUuid());
+ databaseBackend.deleteMessage(message.getUuid());
+ ((Conversation) message.getConversation()).remove(message);
+ updateConversationUi();
}
- public void cancelAvatarFetches(final Account account) {
- synchronized (mInProgressAvatarFetches) {
- for (final Iterator iterator = mInProgressAvatarFetches.iterator();
- iterator.hasNext(); ) {
- final String KEY = iterator.next();
- if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
- iterator.remove();
- }
- }
- }
+ public void updateMessage(Message message) {
+ updateMessage(message, true);
+ }
+
+ public void updateMessage(Message message, boolean includeBody) {
+ databaseBackend.updateMessage(message, includeBody);
+ updateConversationUi();
}
- public void fetchAvatar(Account account, Avatar avatar) {
- fetchAvatar(account, avatar, null);
+ public void createMessageAsync(final Message message) {
+ mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
}
- public void fetchAvatar(
- Account account, final Avatar avatar, final UiCallback callback) {
- if (databaseBackend.isBlockedMedia(avatar.cid())) {
- if (callback != null) callback.error(0, null);
- return;
+ public void updateMessage(Message message, String uuid) {
+ if (!databaseBackend.updateMessage(message, uuid)) {
+ Log.e(Config.LOGTAG, "error updated message in DB after edit");
}
+ updateConversationUi();
+ }
- final String KEY = generateFetchKey(account, avatar);
- synchronized (this.mInProgressAvatarFetches) {
- if (mInProgressAvatarFetches.add(KEY)) {
- switch (avatar.origin) {
- case PEP:
- this.mInProgressAvatarFetches.add(KEY);
- fetchAvatarPep(account, avatar, callback);
- break;
- case VCARD:
- this.mInProgressAvatarFetches.add(KEY);
- fetchAvatarVcard(account, avatar, callback);
- break;
- }
- } else if (avatar.origin == Avatar.Origin.PEP) {
- mOmittedPepAvatarFetches.add(KEY);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": already fetching "
- + avatar.origin
- + " avatar for "
- + avatar.owner);
+ public void createContact(final Contact contact) {
+ createContact(contact, null);
+ }
+
+ public void unregisterPhoneAccounts(final Account account) {
+ for (final Contact contact : account.getRoster().getContacts()) {
+ if (!contact.showInRoster()) {
+ contact.unregisterAsPhoneAccount(this);
}
}
}
- private void fetchAvatarPep(
- final Account account, final Avatar avatar, final UiCallback callback) {
- final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
- sendIqPacket(
- account,
- packet,
- (result) -> {
- synchronized (mInProgressAvatarFetches) {
- mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
- }
- final String ERROR =
- account.getJid().asBareJid()
- + ": fetching avatar for "
- + avatar.owner
- + " failed ";
- if (result.getType() == Iq.Type.RESULT) {
- avatar.image = IqParser.avatarData(result);
- if (avatar.image != null) {
- if (getFileBackend().save(avatar)) {
- if (account.getJid().asBareJid().equals(avatar.owner)) {
- if (account.setAvatar(avatar.getFilename())) {
- databaseBackend.updateAccount(account);
- }
- getAvatarService().clear(account);
- updateConversationUi();
- updateAccountUi();
- } else {
- final Contact contact =
- account.getRoster().getContact(avatar.owner);
- contact.setAvatar(avatar);
- syncRoster(account);
- getAvatarService().clear(contact);
- updateConversationUi();
- updateRosterUi(UpdateRosterReason.AVATAR);
- }
- if (callback != null) {
- callback.success(avatar);
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": successfully fetched pep avatar for "
- + avatar.owner);
- return;
- }
- } else {
+ public void createContact(final Contact contact, final String preAuth) {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.setOption(Contact.Options.ASKING);
+ final var connection = contact.getAccount().getXmppConnection();
+ connection.getManager(RosterManager.class).addRosterItem(contact, preAuth);
+ }
- Log.d(Config.LOGTAG, ERROR + "(parsing error)");
- }
- } else {
- Element error = result.findChild("error");
- if (error == null) {
- Log.d(Config.LOGTAG, ERROR + "(server error)");
- } else {
- Log.d(Config.LOGTAG, ERROR + error);
- }
- }
- if (callback != null) {
- callback.error(0, null);
- }
- });
+ public void deleteContactOnServer(final Contact contact) {
+ final var connection = contact.getAccount().getXmppConnection();
+ connection.getManager(RosterManager.class).deleteRosterItem(contact);
}
- private void fetchAvatarVcard(
- final Account account, final Avatar avatar, final UiCallback callback) {
- final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
- this.sendIqPacket(
- account,
- packet,
- response -> {
- final boolean previouslyOmittedPepFetch;
- synchronized (mInProgressAvatarFetches) {
- final String KEY = generateFetchKey(account, avatar);
- mInProgressAvatarFetches.remove(KEY);
- previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
+ public void publishMucAvatar(
+ final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
+ final var connection = conversation.getAccount().getXmppConnection();
+ final var future =
+ connection
+ .getManager(AvatarManager.class)
+ .publishVCard(conversation.getJid().asBareJid(), image);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ callback.onAvatarPublicationSucceeded();
}
- if (response.getType() == Iq.Type.RESULT) {
- Element vCard = response.findChild("vCard", "vcard-temp");
- Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
- String image = photo != null ? photo.findChildContent("BINVAL") : null;
- if (image != null) {
- avatar.image = image;
- if (getFileBackend().save(avatar)) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": successfully fetched vCard avatar for "
- + avatar.owner
- + " omittedPep="
- + previouslyOmittedPepFetch);
- if (avatar.owner.isBareJid()) {
- if (account.getJid().asBareJid().equals(avatar.owner)
- && account.getAvatar() == null) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": had no avatar. replacing with vcard");
- account.setAvatar(avatar.getFilename());
- databaseBackend.updateAccount(account);
- getAvatarService().clear(account);
- updateAccountUi();
- } else {
- final Contact contact =
- account.getRoster().getContact(avatar.owner);
- contact.setAvatar(avatar, previouslyOmittedPepFetch);
- syncRoster(account);
- getAvatarService().clear(contact);
- updateRosterUi(UpdateRosterReason.AVATAR);
- }
- updateConversationUi();
- } else {
- Conversation conversation =
- find(account, avatar.owner.asBareJid());
- if (conversation != null
- && conversation.getMode() == Conversation.MODE_MULTI) {
- MucOptions.User user =
- conversation
- .getMucOptions()
- .findUserByFullJid(avatar.owner);
- if (user != null) {
- if (user.setAvatar(avatar)) {
- getAvatarService().clear(user);
- updateConversationUi();
- updateMucRosterUi();
- }
- if (user.getRealJid() != null) {
- Contact contact =
- account.getRoster()
- .getContact(user.getRealJid());
- contact.setAvatar(avatar);
- syncRoster(account);
- getAvatarService().clear(contact);
- updateRosterUi(UpdateRosterReason.AVATAR);
- }
- }
- }
- }
- }
- }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not publish MUC avatar", t);
+ callback.onAvatarPublicationFailed(
+ R.string.error_publish_avatar_server_reject);
}
- });
+ },
+ MoreExecutors.directExecutor());
}
- public void checkForAvatar(final Account account, final UiCallback callback) {
- final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
- this.sendIqPacket(
- account,
- packet,
- response -> {
- if (response.getType() != Iq.Type.RESULT) {
- callback.error(0, null);
- }
- final var pubsub = packet.getExtension(PubSub.class);
- if (pubsub == null) {
- callback.error(0, null);
- return;
- }
- final var items = pubsub.getItems();
- if (items == null) {
- callback.error(0, null);
- return;
- }
- final var item = items.getFirstItemWithId(Metadata.class);
- if (item == null) {
- callback.error(0, null);
- return;
- }
- final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
- if (avatar == null) {
- callback.error(0, null);
- return;
+ public void publishAvatar(
+ final Account account,
+ final Uri image,
+ final boolean open,
+ final OnAvatarPublication callback) {
+
+ final var connection = account.getXmppConnection();
+ final var publicationFuture =
+ connection.getManager(AvatarManager.class).uploadAndPublish(image, open);
+
+ Futures.addCallback(
+ publicationFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Void result) {
+ Log.d(Config.LOGTAG, "published avatar");
+ callback.onAvatarPublicationSucceeded();
}
- avatar.owner = account.getJid().asBareJid();
- if (fileBackend.isAvatarCached(avatar)) {
- if (account.setAvatar(avatar.getFilename())) {
- databaseBackend.updateAccount(account);
- }
- getAvatarService().clear(account);
- callback.success(avatar);
- } else {
- fetchAvatarPep(account, avatar, callback);
+
+ @Override
+ public void onFailure(@NonNull final Throwable t) {
+ Log.d(Config.LOGTAG, "avatar upload failed", t);
+ // TODO actually figure out what went wrong
+ callback.onAvatarPublicationFailed(
+ R.string.error_publish_avatar_server_reject);
}
- });
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture checkForAvatar(final Account account) {
+ final var connection = account.getXmppConnection();
+ return connection
+ .getManager(AvatarManager.class)
+ .fetchAndStore(account.getJid().asBareJid());
}
public void notifyAccountAvatarHasChanged(final Account account) {
final XmppConnection connection = account.getXmppConnection();
- if (connection != null && connection.getFeatures().bookmarksConversion()) {
+ // this was bookmark conversion for a bit which doesn't make sense
+ if (connection.getManager(AvatarManager.class).hasPepToVCardConversion()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": avatar changed. resending presence to online group chats");
for (Conversation conversation : conversations) {
- if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
- presenceToMuc(conversation);
+ if (conversation.getAccount() == account
+ && conversation.getMode() == Conversational.MODE_MULTI) {
+ connection.getManager(MultiUserChatManager.class).resendPresence(conversation);
}
}
}
@@ -5985,37 +4095,14 @@ public class XmppConnectionService extends Service {
});
}
- public void deleteContactOnServer(Contact contact) {
- contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
- contact.resetOption(Contact.Options.DIRTY_PUSH);
- contact.setOption(Contact.Options.DIRTY_DELETE);
- Account account = contact.getAccount();
- if (account.getStatus() == Account.State.ONLINE) {
- final Iq iq = new Iq(Iq.Type.SET);
- Element item = iq.query(Namespace.ROSTER).addChild("item");
- item.setAttribute("jid", contact.getJid());
- item.setAttribute("subscription", "remove");
- account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
- }
- }
-
public void updateConversation(final Conversation conversation) {
mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
}
- private void reconnectAccount(
+ public void reconnectAccount(
final Account account, final boolean force, final boolean interactive) {
synchronized (account) {
- final XmppConnection existingConnection = account.getXmppConnection();
- final XmppConnection connection;
- if (existingConnection != null) {
- connection = existingConnection;
- } else if (account.isConnectionEnabled()) {
- connection = createConnection(account);
- account.setXmppConnection(connection);
- } else {
- return;
- }
+ final XmppConnection connection = account.getXmppConnection();
final boolean hasInternet = hasInternetConnection();
if (account.isConnectionEnabled() && hasInternet) {
if (!force) {
@@ -6029,13 +4116,14 @@ public class XmppConnectionService extends Service {
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else {
disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
- account.getRoster().clearPresences();
+ connection.getManager(RosterManager.class).clearPresences();
connection.resetEverything();
final AxolotlService axolotlService = account.getAxolotlService();
if (axolotlService != null) {
axolotlService.resetBrokenness();
}
if (!hasInternet) {
+ // TODO should this go via XmppConnection.setStatusAndTriggerProcessor()?
account.setStatus(Account.State.NO_INTERNET);
}
}
@@ -6047,25 +4135,17 @@ public class XmppConnectionService extends Service {
}
public void invite(final Conversation conversation, final Jid contact) {
- Log.d(
- Config.LOGTAG,
- conversation.getAccount().getJid().asBareJid()
- + ": inviting "
- + contact
- + " to "
- + conversation.getJid().asBareJid());
- final MucOptions.User user =
- conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
- if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
- changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
- }
- final var packet = mMessageGenerator.invite(conversation, contact);
- sendMessagePacket(conversation.getAccount(), packet);
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .invite(conversation, contact);
}
public void directInvite(Conversation conversation, Jid jid) {
- final var packet = mMessageGenerator.directInvite(conversation, jid);
- sendMessagePacket(conversation.getAccount(), packet);
+ final var account = conversation.getAccount();
+ account.getXmppConnection()
+ .getManager(MultiUserChatManager.class)
+ .directInvite(conversation, jid);
}
public void resetSendingToWaiting(Account account) {
@@ -6196,11 +4276,11 @@ public class XmppConnectionService extends Service {
}
}
- public boolean getBooleanPreference(String name, @BoolRes int res) {
+ public boolean getBooleanPreference(String name, int res) {
return getPreferences().getBoolean(name, getResources().getBoolean(res));
}
- public String getStringPreference(String name, @BoolRes int res) {
+ public String getStringPreference(String name, int res) {
return getPreferences().getString(name, getResources().getString(res));
}
@@ -6212,18 +4292,10 @@ public class XmppConnectionService extends Service {
return appSettings.isAllowMessageCorrection();
}
- public boolean sendChatStates() {
- return getBooleanPreference("chat_states", R.bool.chat_states);
- }
-
public boolean useTorToConnect() {
return appSettings.isUseTor();
}
- public boolean broadcastLastActivity() {
- return appSettings.isBroadcastLastActivity();
- }
-
public int unreadCount() {
int count = 0;
for (Conversation conversation : getConversations()) {
@@ -6291,21 +4363,24 @@ public class XmppConnectionService extends Service {
}
}
- public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
- if (mOnCaptchaRequested.size() > 0) {
- DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
- Bitmap scaled =
- Bitmap.createScaledBitmap(
- captcha,
- (int) (captcha.getWidth() * metrics.scaledDensity),
- (int) (captcha.getHeight() * metrics.scaledDensity),
- false);
- for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
- listener.onCaptchaRequested(account, id, data, scaled);
- }
- return true;
+ public boolean displayCaptchaRequest(
+ final Account account,
+ final im.conversations.android.xmpp.model.data.Data data,
+ final Bitmap captcha) {
+ if (mOnCaptchaRequested.isEmpty()) {
+ return false;
}
- return false;
+ final var metrics = getApplicationContext().getResources().getDisplayMetrics();
+ Bitmap scaled =
+ Bitmap.createScaledBitmap(
+ captcha,
+ (int) (captcha.getWidth() * metrics.scaledDensity),
+ (int) (captcha.getHeight() * metrics.scaledDensity),
+ false);
+ for (final OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
+ listener.onCaptchaRequested(account, data, scaled);
+ }
+ return true;
}
public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
@@ -6453,7 +4528,8 @@ public class XmppConnectionService extends Service {
final String stanzaId = last.getServerMsgId();
if (sendDisplayedMarker && serverAssist) {
- final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
+ final var mdsDisplayed =
+ MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
final var packet = mMessageGenerator.confirm(last);
packet.addChild(mdsDisplayed);
if (!last.isPrivateMessage()) {
@@ -6500,21 +4576,11 @@ public class XmppConnectionService extends Service {
itemId = conversation.getJid().asBareJid();
}
Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
- publishMds(account, itemId, stanzaId, conversation);
- }
-
- private void publishMds(
- final Account account,
- final Jid itemId,
- final String stanzaId,
- final Conversation conversation) {
- final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
- pushNodeAndEnforcePublishOptions(
- account,
- Namespace.MDS_DISPLAYED,
- item,
- itemId.toString(),
- PublishOptions.persistentWhitelistAccessMaxItems());
+ final var displayed =
+ MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
+ connection
+ .getManager(MessageDisplayedSynchronizationManager.class)
+ .publish(itemId, displayed);
}
public boolean sendReactions(final Message message, final Collection reactions) {
@@ -6678,20 +4744,20 @@ public class XmppConnectionService extends Service {
}
public Collection getKnownConferenceHosts() {
- final Set mucServers = new HashSet<>();
+ final var builder = new ImmutableSet.Builder();
for (final Account account : accounts) {
- if (account.getXmppConnection() != null) {
- mucServers.addAll(account.getXmppConnection().getMucServers());
- for (final Bookmark bookmark : account.getBookmarks()) {
- final Jid jid = bookmark.getJid();
- final String s = jid == null ? null : jid.getDomain().toString();
- if (s != null) {
- mucServers.add(s);
- }
+ final var connection = account.getXmppConnection();
+ builder.addAll(connection.getManager(MultiUserChatManager.class).getServices());
+ for (final var bookmark : account.getBookmarks()) {
+ final Jid jid = bookmark.getJid();
+ final Jid domain = jid == null ? null : jid.getDomain();
+ if (domain == null) {
+ continue;
}
+ builder.add(domain);
}
}
- return mucServers;
+ return Collections2.transform(builder.build(), Jid::toString);
}
public void sendMessagePacket(
@@ -6703,23 +4769,6 @@ public class XmppConnectionService extends Service {
}
}
- public void sendPresencePacket(
- final Account account,
- final im.conversations.android.xmpp.model.stanza.Presence packet) {
- final XmppConnection connection = account.getXmppConnection();
- if (connection != null) {
- connection.sendPresencePacket(packet);
- }
- }
-
- public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
- final XmppConnection connection = account.getXmppConnection();
- if (connection == null) {
- return;
- }
- connection.sendCreateAccountWithCaptchaPacket(id, data);
- }
-
public ListenableFuture sendIqPacket(final Account account, final Iq request) {
final XmppConnection connection = account.getXmppConnection();
if (connection == null) {
@@ -6741,27 +4790,6 @@ public class XmppConnectionService extends Service {
}
}
- public void sendPresence(final Account account) {
- sendPresence(account, checkListeners() && broadcastLastActivity());
- }
-
- private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
- final im.conversations.android.xmpp.model.stanza.Presence.Availability status;
- if (manuallyChangePresence()) {
- status = account.getPresenceStatus();
- } else {
- status = getTargetPresence();
- }
- final var packet = mPresenceGenerator.selfPresence(account, status);
- if (mLastActivity > 0 && includeIdleTimestamp) {
- long since =
- Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
- packet.addChild("idle", Namespace.IDLE)
- .setAttribute("since", AbstractGenerator.getTimestamp(since));
- }
- sendPresencePacket(account, packet);
- }
-
private void deactivateGracePeriod() {
for (Account account : getAccounts()) {
account.deactivateGracePeriod();
@@ -6769,10 +4797,13 @@ public class XmppConnectionService extends Service {
}
public void refreshAllPresences() {
- boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
- for (Account account : getAccounts()) {
+ final boolean includeIdleTimestamp =
+ checkListeners() && appSettings.isBroadcastLastActivity();
+ for (final var account : getAccounts()) {
if (account.isConnectionEnabled()) {
- sendPresence(account, includeIdleTimestamp);
+ account.getXmppConnection()
+ .getManager(PresenceManager.class)
+ .available(includeIdleTimestamp);
}
}
}
@@ -6785,11 +4816,6 @@ public class XmppConnectionService extends Service {
}
}
- private void sendOfflinePresence(final Account account) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
- sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
- }
-
public MessageGenerator getMessageGenerator() {
return this.mMessageGenerator;
}
@@ -6806,7 +4832,7 @@ public class XmppConnectionService extends Service {
return this.mJingleConnectionManager;
}
- private boolean hasJingleRtpConnection(final Account account) {
+ public boolean hasJingleRtpConnection(final Account account) {
return this.mJingleConnectionManager.hasJingleRtpConnection(account);
}
@@ -6890,29 +4916,11 @@ public class XmppConnectionService extends Service {
public boolean sendBlockRequest(
final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
- if (blockable != null && blockable.getBlockedJid() != null) {
- final var account = blockable.getAccount();
- final Jid jid = blockable.getBlockedJid();
- this.sendIqPacket(
- account,
- getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- account.getBlocklist().add(jid);
- updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
- }
- });
- if (blockable.getBlockedJid().isFullJid()) {
- return false;
- } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
- updateConversationUi();
- return true;
- } else {
- return false;
- }
- } else {
- return false;
- }
+ final var account = blockable.getAccount();
+ final var connection = account.getXmppConnection();
+ return connection
+ .getManager(BlockingManager.class)
+ .block(blockable, reportSpam, serverMsgId);
}
public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
@@ -6947,55 +4955,46 @@ public class XmppConnectionService extends Service {
}
public void sendUnblockRequest(final Blockable blockable) {
- if (blockable != null && blockable.getJid() != null) {
- final var account = blockable.getAccount();
- final Jid jid = blockable.getBlockedJid();
- this.sendIqPacket(
- account,
- getIqGenerator().generateSetUnblockRequest(jid),
- response -> {
- if (response.getType() == Iq.Type.RESULT) {
- account.getBlocklist().remove(jid);
- updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
- }
- });
- }
+ final var account = blockable.getAccount();
+ final var connection = account.getXmppConnection();
+ connection.getManager(BlockingManager.class).unblock(blockable);
}
public void publishDisplayName(final Account account) {
- String displayName = account.getDisplayName();
- final Iq request;
- if (TextUtils.isEmpty(displayName)) {
- request = mIqGenerator.deleteNode(Namespace.NICK);
- } else {
- request = mIqGenerator.publishNick(displayName);
- }
+ final var connection = account.getXmppConnection();
+ final String displayName = account.getDisplayName();
mAvatarService.clear(account);
- sendIqPacket(
- account,
- request,
- (packet) -> {
- if (packet.getType() == Iq.Type.ERROR) {
+ final var future = connection.getManager(NickManager.class).publish(displayName);
+ Futures.addCallback(
+ future,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(Void result) {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to modify nick name "
- + packet);
+ account.getJid().asBareJid() + ": published User Nick");
}
- });
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not publish User Nick", t);
+ }
+ },
+ MoreExecutors.directExecutor());
}
public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
request.setTo(jid);
- Element query = request.query("jabber:iq:gateway");
+ Element query = request.addChild("query");
+ query.setAttribute("xmlns", "jabber:iq:gateway");
if (input != null) {
Element prompt = query.addChild("prompt");
prompt.setContent(input);
}
sendIqPacket(account, request, packet -> {
if (packet.getType() == Iq.Type.RESULT) {
- callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
+ callback.onGatewayResult(packet.findChild("query").findChildContent(input == null ? "prompt" : "jid"), null);
} else {
Element error = packet.findChild("error");
callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
@@ -7013,6 +5012,7 @@ public class XmppConnectionService extends Service {
(packet) -> {
final Element prefs = packet.findChild("prefs", version.namespace);
if (packet.getType() == Iq.Type.RESULT && prefs != null) {
+ account.setMamPrefs(prefs);
callback.onPreferencesFetched(prefs);
} else {
callback.onPreferencesFetchFailed();
@@ -7024,7 +5024,8 @@ public class XmppConnectionService extends Service {
return mPushManagementService;
}
- public void changeStatus(Account account, PresenceTemplate template, String signature) {
+ public void changeStatus(
+ final Account account, final PresenceTemplate template, final String signature) {
if (!template.getStatusMessage().isEmpty()) {
databaseBackend.insertPresenceTemplate(template);
}
@@ -7032,7 +5033,7 @@ public class XmppConnectionService extends Service {
account.setPresenceStatus(template.getStatus());
account.setPresenceStatusMessage(template.getStatusMessage());
databaseBackend.updateAccount(account);
- sendPresence(account);
+ account.getXmppConnection().getManager(PresenceManager.class).available();
}
public List getPresenceTemplates(Account account) {
@@ -7045,22 +5046,6 @@ public class XmppConnectionService extends Service {
return templates;
}
- public void saveConversationAsBookmark(final Conversation conversation, final String name) {
- final Account account = conversation.getAccount();
- final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
- String nick = conversation.getMucOptions().getActualNick();
- if (nick == null) 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);
- createBookmark(account, bookmark);
- bookmark.setConversation(conversation);
- }
-
public boolean verifyFingerprints(Contact contact, List fingerprints) {
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
@@ -7132,6 +5117,10 @@ public class XmppConnectionService extends Service {
}
}
+ public long getLastActivity() {
+ return this.mLastActivity;
+ }
+
public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs);
@@ -7150,18 +5139,6 @@ public class XmppConnectionService extends Service {
void informUser(int r);
}
- public interface OnAccountPasswordChanged {
- void onPasswordChangeSucceeded();
-
- void onPasswordChangeFailed();
- }
-
- public interface OnRoomDestroy {
- void onRoomDestroySucceeded();
-
- void onRoomDestroyFailed();
- }
-
public interface OnAffiliationChanged {
void onAffiliationChangedSuccessful(Jid jid);
@@ -7190,7 +5167,10 @@ public class XmppConnectionService extends Service {
}
public interface OnCaptchaRequested {
- void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
+ void onCaptchaRequested(
+ Account account,
+ im.conversations.android.xmpp.model.data.Data data,
+ Bitmap captcha);
}
public interface OnRosterUpdate {
diff --git a/src/main/java/eu/siacs/conversations/ui/Activities.java b/src/main/java/eu/siacs/conversations/ui/Activities.java
index d95d6b4acd04e9aba716480294210202ab1d6f23..a0a118afb9fdc08aae679983abc230650540055e 100644
--- a/src/main/java/eu/siacs/conversations/ui/Activities.java
+++ b/src/main/java/eu/siacs/conversations/ui/Activities.java
@@ -17,7 +17,14 @@ public final class Activities {
public static void setStatusAndNavigationBarColors(
final Activity activity, final View view, final boolean raisedStatusBar) {
- final var isLightMode = isLightMode(activity);
+ setStatusAndNavigationBarColors(activity, view, isLightMode(activity), raisedStatusBar);
+ }
+
+ public static void setStatusAndNavigationBarColors(
+ final Activity activity,
+ final View view,
+ final boolean isLightMode,
+ final boolean raisedStatusBar) {
final var window = activity.getWindow();
final var flags = view.getSystemUiVisibility();
// an elevation of 4 matches the MaterialToolbar elevation
diff --git a/src/main/java/eu/siacs/conversations/ui/BaseActivity.java b/src/main/java/eu/siacs/conversations/ui/BaseActivity.java
index cea58c15ecef8e1fbff19b263b4b9980c243fde0..259809f9995a0764425bccae6d7b42405eb1b626 100644
--- a/src/main/java/eu/siacs/conversations/ui/BaseActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/BaseActivity.java
@@ -1,10 +1,8 @@
package eu.siacs.conversations.ui;
import android.util.Log;
-
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
-
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.ui.util.SettingsUtils;
@@ -23,7 +21,7 @@ public abstract class BaseActivity extends AppCompatActivity {
}
@Override
- protected void onResume(){
+ protected void onResume() {
super.onResume();
SettingsUtils.applyScreenshotSetting(this);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
index 263bc64205f1a7fa5255732d6a3c5a8715970f12..e6444915ff3ecd9a4b5f4aab4626d99d6e3852d9 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
@@ -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() {}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java
index 45f54d0e36ef7e830d33ef91f9041ac2f1896073..cfd5fc1e53b6dfaefaaba2a01d2e3f526285a48f 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java
@@ -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);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index 64247cc9627aafafcd24c9fc08a43f7653585ada..130936aa5d7f229d3a2f171cffaec86903b3d4bd 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -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 renameCallback =
- new UiCallback() {
+ private FutureCallback renameCallback =
+ new FutureCallback() {
@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 onConfigurationPushed =
+ new FutureCallback() {
+
+ @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()
+ .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() {
+
+ @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(
() -> {
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index f061897fa5c983abeb241ab5366fda635db6d6a0..49d50627247441aa7c0080e1d24c6b77481d61b5 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -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) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index c7e2a59c22fff8fd2eb646ca3fe4227ef99641c0..9480f3c1c8a84b61b1c1270b3ab1e6c8a388bce2 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -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() {
diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java
index 7351823f6708e2e87ad6073f092e64da84b04f8e..461d0780150b2f6b2cc76808e9c219b60e825b17 100644
--- a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java
+++ b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java
@@ -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 hosts =
- ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
+ if (activity instanceof XmppActivity xmppActivity) {
+ Collection hosts = xmppActivity.xmppConnectionService.getKnownConferenceHosts();
this.knownHostsAdapter.refresh(hosts);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index 9ccbd703424e9641fd4fe41dfab5d3fb26b512f3..7ceb823bad95c290860b832eeb914fba0c8b9a9c 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -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 mAvatarFetchCallback =
- new UiCallback() {
+ private final FutureCallback mAvatarFetchCallback =
+ new FutureCallback<>() {
@Override
- public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
- finishInitialSetup(avatar);
+ public void onSuccess(Void result) {
+ Log.d(Config.LOGTAG, "found pre-existing avatar");
+ finishInitialSetup(true);
}
@Override
- public void success(final Avatar avatar) {
- finishInitialSetup(avatar);
- }
-
- @Override
- public void error(final int errorCode, final Avatar avatar) {
- finishInitialSetup(avatar);
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "failed to fetch avatar", t);
+ finishInitialSetup(false);
}
};
private final OnClickListener mAvatarClickListener =
@@ -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();
diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
index 7b8aaa2c0d386c3e017d3cb222bfe91da6820048..e2bc3b7af06f2b7896d19f1c73a7a34c4ece6a1f 100644
--- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
@@ -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);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
index 1549ddbef90e866ff30bdaffec9720806ed4093f..e228d97b603fa882971fc7a5c9e12e6f8da5d5ff 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -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 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() {
diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
index b1656cb5bb5b870942e6c3c3f8e51b0806ef9262..2ee65d2eb76cf1376dfc720af4dfd4ec71e9dcbd 100644
--- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
@@ -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 AAC_SENSITIVE_DEVICES =
- new ImmutableSet.Builder()
- .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);
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index 3b53e7f3a0c17ea16de68493fc1e87da6e0b6230..d0f641856c8471fd77b0f4245415d8fad6de09a7 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -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 {
diff --git a/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..30705664c9f33d4e0053d564b443c1b6157299e8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ViewProfilePictureActivity.java
@@ -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);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index f8278558ca67d8c9d2bf2aed08f3c3fc960a2c75..a193809bc9407d2d9c28bc6ea2fb88715176f273 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -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();
}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
index c68f8c8c0bea2e1e36e3a7d0746d705660515bea..8b925cb689c5f35c7908dcda026ae6cff922a517 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
@@ -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 implements View.OnCreateContextMenuListener {
-
- static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
- @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
+ implements View.OnCreateContextMenuListener {
+
+ static final DiffUtil.ItemCallback DIFF =
+ new DiffUtil.ItemCallback() {
+ @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 {
- 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 {
- 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
implements View.OnCreateContextMenuListener {
@@ -52,7 +51,7 @@ public class UserPreviewAdapter extends ListAdapter toBundle(boolean[] values) {
+ final var builder = new ImmutableMap.Builder();
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) {
diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java
index 1d0fd3fdb85e9683cdb1eaf38a9e60bce2b6f518..ebcee8410e283dffaad185fec62cc2a414922a6c 100644
--- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java
+++ b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java
@@ -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 getPermissionsChoices(Activity activity, Conversation conversation, User user) {
+ public static Pair getPermissionsChoices(Activity activity, Conversation conversation, MucOptions.User user) {
ArrayList items = new ArrayList<>();
ArrayList 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(
diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
index 8b7a2e56f100124834a4cda7cffbfff9e6e6a6b2..fbefc239b00adf58ef007271557fa451e12756cb 100644
--- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java
+++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
@@ -37,6 +37,10 @@ public class Compatibility {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
}
+ public static boolean thirtyFour() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+ }
+
public static void startService(final Context context, final Intent intent) {
try {
if (Compatibility.twentySix()) {
diff --git a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java
index c787f8e7eeed0d7cd6849ad4b3f13efd2edba3d0..3279ac14fd3470dca555208b703e2fb82f150976 100644
--- a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java
+++ b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java
@@ -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 CREATOR = new Creator() {
- @Override
- public EasyOnboardingInvite createFromParcel(Parcel in) {
- return new EasyOnboardingInvite(in);
- }
+ public static final Creator CREATOR =
+ new Creator() {
+ @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 getSupportingAccounts(final XmppConnectionService service) {
- final ImmutableList.Builder supportingAccountsBuilder = new ImmutableList.Builder<>();
- final List 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 supportingAccountsBuilder =
+ new ImmutableList.Builder<>();
+ final List 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);
}
}
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
index 98924a26214c2ec03a33f43c2987e08f155237ae..e1189d813cfb957be009f201fb501baad90c8628 100644
--- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -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");
- }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java b/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java
deleted file mode 100644
index 396bfa3ece0259adfba748a3dd3b85e511b1406b..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.utils;
-
-import java.util.HashMap;
-
-import eu.siacs.conversations.entities.Account;
-
-public class ReplacingTaskManager {
-
- private final HashMap executors = new HashMap<>();
-
- public void execute(final Account account, Runnable runnable) {
- ReplacingSerialSingleThreadExecutor executor;
- synchronized (this.executors) {
- executor = this.executors.get(account);
- if (executor == null) {
- executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName());
- this.executors.put(account, executor);
- }
- executor.execute(runnable);
- }
- }
-
- public void clear(Account account) {
- synchronized (this.executors) {
- this.executors.remove(account);
- }
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java
index 91df1954ad714e44a4c374591b329e8d21420a5d..7a19f7ed67f2ae080b3f0aaff835ace89c4724f0 100644
--- a/src/main/java/eu/siacs/conversations/utils/Resolver.java
+++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java
@@ -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>> futures = futuresBuilder.build();
- return merge(futures);
+ return FutureMerger.successfulAsList(futures);
}
private static ListenableFuture> 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>> futures = futuresBuilder.build();
- final var noSrvFallbacks = merge(futures);
+ final var noSrvFallbacks = FutureMerger.successfulAsList(futures);
return Futures.transform(
noSrvFallbacks,
results -> {
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 09b4ac2e9039eade6d72102c079c07d74252e1d3..eb01cbf1d00c1b5fd5c2b449012f1a8ef5961577 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -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;
diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java
index c5fb190723b4dce38393589ee91c41b4bd4824f0..d9ccbfe8a7ecc5d1bc1628adf28768bbe8c2d0ba 100644
--- a/src/main/java/eu/siacs/conversations/xml/Element.java
+++ b/src/main/java/eu/siacs/conversations/xml/Element.java
@@ -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;
}
diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java
index 851b8e78c2fdc162d0f35dc0d0ca876961dabd31..c15383c380b2b5f4b06de4dbff2a36ca6c5164a5 100644
--- a/src/main/java/eu/siacs/conversations/xml/Namespace.java
+++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java
@@ -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";
diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea4ed3a4f5db012e68470dc44e653dc1486b5ff0
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/IqErrorException.java
@@ -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;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java b/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java
deleted file mode 100644
index fb8d8e730cd9d4e4b37d3321a084e9d4abc25f93..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package eu.siacs.conversations.xmpp;
-
-import im.conversations.android.xmpp.model.stanza.Iq;
-
-public class IqErrorResponseException extends Exception {
-
- private final Iq response;
-
- public IqErrorResponseException(final Iq response) {
- super(message(response));
- this.response = response;
- }
-
- public Iq getResponse() {
- return this.response;
- }
-
- public static String message(final Iq iq) {
- final var error = iq.getError();
- if (error == null) {
- return "missing error element in response";
- }
- final var text = error.getTextAsString();
- if (text != null) {
- return text;
- }
- final var condition = error.getCondition();
- if (condition != null) {
- return condition.getName();
- }
- return "no condition attached to error";
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/Jid.java
index a83c762872b18f9b6f9a17a2662016ecd64c4297..9faff3a0080cf601d971b397d86143723f593c15 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/Jid.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/Jid.java
@@ -15,7 +15,7 @@ public abstract class Jid implements Comparable, Serializable, CharSequence
private static final Pattern HOSTNAME_PATTERN =
Pattern.compile(
- "^(?=.{1,253}$)(?=.{1,253}$)(?!-)(?!.*--)(?!.*-$)[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$");
+ "^(?=.{1,253}$)(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?(?:\\.(?!-)[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?)*\\.?$");
public static Jid of(
final CharSequence local, final CharSequence domain, final CharSequence resource) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java
index f4bf31f422714e3b1923257646645443728eb8ed..93a89db2a7835e43137004f35f2537eaac04e71a 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java
@@ -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 get(
final XmppConnectionService context, final XmppConnection connection) {
return new ImmutableClassToInstanceMap.Builder()
+ .put(AvatarManager.class, new AvatarManager(context, connection))
+ .put(AxolotlManager.class, new AxolotlManager(context, connection))
+ .put(BlockingManager.class, new BlockingManager(context, connection))
+ .put(BookmarkManager.class, new BookmarkManager(context, connection))
.put(CarbonsManager.class, new CarbonsManager(context, connection))
.put(DiscoManager.class, new DiscoManager(context, connection))
+ .put(EntityTimeManager.class, new EntityTimeManager(context, connection))
+ .put(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();
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2777667734c76e46af46c9eccedc334957a7c9f
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/PreconditionNotMetException.java
@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp;
+
+import im.conversations.android.xmpp.model.pubsub.error.PubSubError;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class PreconditionNotMetException extends PubSubErrorException {
+
+ public PreconditionNotMetException(final Iq response) {
+ super(response);
+ if (this.pubSubError instanceof PubSubError.PreconditionNotMet) {
+ return;
+ }
+ throw new AssertionError(
+ "This exception should only be constructed for PreconditionNotMet errors");
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b6e079214c672913a1aa0c47d7d5e7102159aad
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/PubSubErrorException.java
@@ -0,0 +1,19 @@
+package im.conversations.android.xmpp;
+
+import im.conversations.android.xmpp.model.pubsub.error.PubSubError;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class PubSubErrorException extends IqErrorException {
+
+ protected final PubSubError pubSubError;
+
+ public PubSubErrorException(Iq response) {
+ super(response);
+ final var error = response.getError();
+ final var pubSubError = error == null ? null : error.getExtension(PubSubError.class);
+ if (pubSubError == null) {
+ throw new AssertionError("This exception should only be constructed for PubSubErrors");
+ }
+ this.pubSubError = pubSubError;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index edbd5a5754579c4bae0b5651e1794354273fad7e..3ef7259eac9504e4a28eece1cbf3c6f07706ac47 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -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 commands = new HashMap<>();
private final SparseArray mStanzaQueue = new SparseArray<>();
private final Hashtable, ScheduledFuture>>> packetCallbacks = new Hashtable<>();
private final Set advancedStreamFeaturesLoadedListeners =
@@ -246,9 +243,11 @@ public class XmppConnection implements Runnable {
private final Consumer presenceListener;
private final Consumer unregisteredIqListener;
private final Consumer messageListener;
- private OnStatusChanged statusListener = null;
- private final Runnable bindListener;
- private OnMessageAcknowledged acknowledgedListener = null;
+ private final Consumer accountStateProcessor;
+ private final BiFunction messageAcknowledgedProcessor;
+ private AxolotlService axolotlService;
+ private final PgpDecryptionService pgpDecryptionService;
+ private final Runnable bindProcessor;
private final PendingItem 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 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 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 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 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 sendIqPacket(final Iq request) {
+ return sendIqPacket(request, false);
+ }
+
+ public ListenableFuture sendIqPacket(final Iq request, final boolean allowUnbound) {
final SettableFuture 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> findDiscoItemsByFeature(final String feature) {
- final List> items = new ArrayList<>();
- for (final Entry cursor :
- getManager(DiscoManager.class).getServerItems().entrySet()) {
- if (cursor.getValue().getFeatureStrings().contains(feature)) {
- items.add(cursor);
- }
- }
- return items;
- }
-
- public Entry getServiceDiscoveryResultByFeature(final String feature) {
- return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
- }
-
- public Jid findDiscoItemByFeature(final String feature) {
- final var items = findDiscoItemsByFeature(feature);
- if (items.isEmpty()) {
- return null;
- }
- return Iterables.getFirst(items, null).getKey();
- }
-
- public boolean r() {
- if (getFeatures().sm()) {
- this.tagWriter.writeStanzaAsync(new Request());
- return true;
- } else {
- return false;
- }
- }
-
- public List getMucServersWithholdAccount() {
- final List servers = getMucServers();
- servers.remove(account.getDomain().toString());
- return servers;
- }
-
- public List getMucServers() {
- List servers = new ArrayList<>();
- for (final Entry 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 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);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
index b95d330401b0f92342ee3d8bf8598451fb43936d..655ed495b6052a3487cc196db2c7ca07ee1b7896 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -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;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
index d8dd240c3c2c38d592cee78c3c2477e8aa560eb2..ec73b11acdbe13ea0276903fd587df2bc1711a25 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
@@ -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(
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java
index 8cebf080fd14835dde8d5a36002b929a63514f98..64f2d4f4bb8cb09e8fc26e40a2d65905c92c1511 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java
@@ -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 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 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 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 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;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..93c42dfff666b31c649c4fe9a138dc95f3649dd1
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractBookmarkManager.java
@@ -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 bookmarks, final boolean pep) {
+ final var account = getAccount();
+ // TODO we can internalize this getBookmarkedJid
+ final Set previousBookmarks = account.getBookmarkedJids();
+ for (final Bookmark bookmark : bookmarks.values()) {
+ previousBookmarks.remove(bookmark.getJid().asBareJid());
+ getManager(BookmarkManager.class).processModifiedBookmark(bookmark, pep);
+ }
+ if (pep) {
+ this.processDeletedBookmarks(previousBookmarks);
+ }
+ account.setBookmarks(bookmarks);
+ }
+
+ protected void processDeletedBookmarks(final Collection 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);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..509bbf472d1a4c0ac5872d575c175ccaf314cfc2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java
@@ -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 SUPPORTED_CONTENT_TYPES;
+
+ private static final Ordering AVATAR_ORDERING =
+ new Ordering<>() {
+ @Override
+ public int compare(Info left, Info right) {
+ return ComparisonChain.start()
+ .compare(
+ right.getWidth() * right.getHeight(),
+ left.getWidth() * left.getHeight())
+ .compare(
+ ImageFormat.formatPriority(right.getType()),
+ ImageFormat.formatPriority(left.getType()))
+ .result();
+ }
+ };
+
+ static {
+ final ImmutableList.Builder builder = new ImmutableList.Builder<>();
+ builder.add(ImageFormat.JPEG, ImageFormat.PNG, ImageFormat.WEBP, 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 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 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 fetchAndStoreWithFallback(
+ final Jid address, final Info picked, final Info fallback) {
+ Preconditions.checkArgument(fallback.getUrl() == null, "fallback avatar must be in-band");
+ final var url = picked.getUrl();
+ if (url != null) {
+ final var httpDownloadFuture = fetchAndStoreHttp(url, picked);
+ return Futures.catchingAsync(
+ httpDownloadFuture,
+ Exception.class,
+ ex -> {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not download avatar for "
+ + address
+ + " from "
+ + url,
+ ex);
+ return fetchAndStoreInBand(address, fallback);
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ return fetchAndStoreInBand(address, picked);
+ }
+ }
+
+ private ListenableFuture fetchAndStoreInBand(final Jid address, final Info avatar) {
+ final var future = fetch(address, avatar.getId());
+ return Futures.transformAsync(
+ future,
+ data -> {
+ 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 write(final File destination, byte[] bytes) {
+ return Futures.submit(
+ () -> {
+ final var randomFile =
+ new File(context.getCacheDir(), UUID.randomUUID().toString());
+ Files.write(bytes, randomFile);
+ if (moveAvatarIntoCache(randomFile, destination)) {
+ return null;
+ }
+ throw new IllegalStateException(
+ String.format(
+ "Could not move file to %s", destination.getAbsolutePath()));
+ },
+ AVATAR_COMPRESSION_EXECUTOR);
+ }
+
+ private ListenableFuture fetchAndStoreHttp(final HttpUrl url, final Info avatar) {
+ final SettableFuture settableFuture = SettableFuture.create();
+ final OkHttpClient client =
+ service.getHttpConnectionManager().buildHttpClient(url, getAccount(), 30, false);
+ final var request = new Request.Builder().url(url).get().build();
+ client.newCall(request)
+ .enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ settableFuture.setException(e);
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()) {
+ try {
+ write(avatar, response);
+ } catch (final Exception e) {
+ settableFuture.setException(e);
+ return;
+ }
+ settableFuture.set(avatar);
+ } else {
+ settableFuture.setException(
+ new IOException("HTTP call was not successful"));
+ }
+ }
+ });
+ return settableFuture;
+ }
+
+ private void write(final Info avatar, Response response) throws IOException {
+ final var body = response.body();
+ if (body == null) {
+ throw new IOException("Body was null");
+ }
+ final long bytes = avatar.getBytes();
+ final long actualBytes;
+ final var inputStream = ByteStreams.limit(body.byteStream(), avatar.getBytes());
+ final var randomFile = new File(context.getCacheDir(), UUID.randomUUID().toString());
+ final String actualHash;
+ try (final var fileOutputStream = new FileOutputStream(randomFile);
+ var hashingOutputStream =
+ new HashingOutputStream(Hashing.sha1(), fileOutputStream)) {
+ actualBytes = ByteStreams.copy(inputStream, hashingOutputStream);
+ actualHash = hashingOutputStream.hash().toString();
+ }
+ if (actualBytes != bytes) {
+ throw new IllegalStateException("File size did not meet expected size");
+ }
+ if (!actualHash.equals(avatar.getId())) {
+ throw new IllegalStateException("File hash did not match");
+ }
+ final var avatarFile = FileBackend.getAvatarFile(context, avatar.getId());
+ if (moveAvatarIntoCache(randomFile, avatarFile)) {
+ return;
+ }
+ throw new IOException("Could not move avatar to avatar location");
+ }
+
+ private void 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 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() {
+ @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