diff --git a/build.gradle b/build.gradle index 3088587934..0147d25e33 100644 --- a/build.gradle +++ b/build.gradle @@ -44,14 +44,16 @@ configurations { dependencies { androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test:rules:1.3.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - implementation "androidx.core:core:1.10.1" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + implementation "androidx.core:core:1.13.1" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + implementation project(':libs:annotation') + annotationProcessor project(':libs:annotation-processor') implementation 'androidx.viewpager:viewpager:1.0.0' @@ -65,17 +67,17 @@ dependencies { conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' - implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") + implementation("com.github.CanHub:Android-Image-Cropper:2.2.0") implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.exifinterface:exifinterface:1.3.7' implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.preference:preference:1.2.1" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.work:work-runtime:2.9.0' + implementation 'androidx.work:work-runtime:2.9.1' - implementation "androidx.emoji2:emoji2:1.4.0" - freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0" + implementation "androidx.emoji2:emoji2:1.5.0" + freeImplementation "androidx.emoji2:emoji2-bundled:1.5.0" implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1' implementation 'com.google.zxing:core:3.5.3' @@ -92,7 +94,7 @@ dependencies { implementation 'org.jxmpp:jxmpp-jid:1.0.3' implementation 'org.jxmpp:jxmpp-stringprep-libidn:1.0.3' - implementation 'org.osmdroid:osmdroid-android:6.1.11' + implementation 'org.osmdroid:osmdroid-android:6.1.16' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'me.drakeet.support:toastcompat:1.1.0' @@ -102,11 +104,11 @@ dependencies { implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation "com.squareup.okhttp3:okhttp:4.12.0" - implementation 'com.google.guava:guava:32.1.3-android' + implementation 'com.google.guava:guava:33.0.0-android' implementation 'io.michaelrocks:libphonenumber-android:8.13.35' implementation 'im.conversations.webrtc:webrtc-android:119.0.1' implementation 'io.github.nishkarsh:android-permissions:2.1.6' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.browser:browser:1.8.0' implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0' diff --git a/libs/annotation-processor/build.gradle b/libs/annotation-processor/build.gradle new file mode 100644 index 0000000000..6232f33c6e --- /dev/null +++ b/libs/annotation-processor/build.gradle @@ -0,0 +1,20 @@ +apply plugin: "java-library" + +repositories { + google() + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +dependencies { + + implementation project(':libs:annotation') + + annotationProcessor 'com.google.auto.service:auto-service:1.0.1' + api 'com.google.auto.service:auto-service-annotations:1.0.1' + implementation 'com.google.guava:guava:31.1-jre' + +} \ No newline at end of file diff --git a/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java new file mode 100644 index 0000000000..c42cc53405 --- /dev/null +++ b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java @@ -0,0 +1,185 @@ +package im.conversations.android.annotation.processor; + +import com.google.auto.service.AutoService; +import com.google.common.base.CaseFormat; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.annotation.XmlPackage; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; +import javax.tools.JavaFileObject; + +@AutoService(Processor.class) +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedAnnotationTypes("im.conversations.android.annotation.XmlElement") +public class XmlElementProcessor extends AbstractProcessor { + + @Override + public boolean process(Set set, RoundEnvironment roundEnvironment) { + final Set elements = + roundEnvironment.getElementsAnnotatedWith(XmlElement.class); + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (final Element element : elements) { + if (element instanceof final TypeElement typeElement) { + final Id id = of(typeElement); + builder.put(id, typeElement.getQualifiedName().toString()); + } + } + final ImmutableMap maps = builder.build(); + if (maps.isEmpty()) { + return false; + } + final JavaFileObject extensionFile; + try { + extensionFile = + processingEnv + .getFiler() + .createSourceFile("im.conversations.android.xmpp.Extensions"); + } catch (final IOException e) { + throw new RuntimeException(e); + } + try (final PrintWriter out = new PrintWriter(extensionFile.openWriter())) { + out.println("package im.conversations.android.xmpp;"); + out.println("import com.google.common.collect.BiMap;"); + out.println("import com.google.common.collect.ImmutableBiMap;"); + out.println("import im.conversations.android.xmpp.ExtensionFactory;"); + out.println("import im.conversations.android.xmpp.model.Extension;"); + out.print("\n"); + out.println("public final class Extensions {"); + out.println( + "public static final BiMap>" + + " EXTENSION_CLASS_MAP;"); + out.println("static {"); + out.println( + "final var builder = new ImmutableBiMap.Builder>();"); + for (final Map.Entry entry : maps.entrySet()) { + Id id = entry.getKey(); + String clazz = entry.getValue(); + out.format( + "builder.put(new ExtensionFactory.Id(\"%s\",\"%s\"),%s.class);", + id.name, id.namespace, clazz); + out.print("\n"); + } + out.println("EXTENSION_CLASS_MAP = builder.build();"); + out.println("}"); + out.println(" private Extensions() {}"); + out.println("}"); + // writing generated file to out … + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + + private static Id of(final TypeElement typeElement) { + final XmlElement xmlElement = typeElement.getAnnotation(XmlElement.class); + final PackageElement packageElement = getPackageElement(typeElement); + final XmlPackage xmlPackage = + packageElement == null ? null : packageElement.getAnnotation(XmlPackage.class); + if (xmlElement == null) { + throw new IllegalStateException( + String.format( + "%s is not annotated as @XmlElement", + typeElement.getQualifiedName().toString())); + } + final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace(); + final String elementName = xmlElement.name(); + final String elementNamespace = xmlElement.namespace(); + final String namespace; + if (!Strings.isNullOrEmpty(elementNamespace)) { + namespace = elementNamespace; + } else if (!Strings.isNullOrEmpty(packageNamespace)) { + namespace = packageNamespace; + } else { + throw new IllegalStateException( + String.format( + "%s does not declare a namespace", + typeElement.getQualifiedName().toString())); + } + if (!hasEmptyDefaultConstructor(typeElement)) { + throw new IllegalStateException( + String.format( + "%s does not have an empty default constructor", + typeElement.getQualifiedName().toString())); + } + final String name; + if (Strings.isNullOrEmpty(elementName)) { + name = + CaseFormat.UPPER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, typeElement.getSimpleName().toString()); + } else { + name = elementName; + } + return new Id(name, namespace); + } + + private static PackageElement getPackageElement(final TypeElement typeElement) { + final Element parent = typeElement.getEnclosingElement(); + if (parent instanceof PackageElement) { + return (PackageElement) parent; + } else { + final Element nextParent = parent.getEnclosingElement(); + if (nextParent instanceof PackageElement) { + return (PackageElement) nextParent; + } else { + return null; + } + } + } + + private static boolean hasEmptyDefaultConstructor(final TypeElement typeElement) { + final List constructors = + ElementFilter.constructorsIn(typeElement.getEnclosedElements()); + for (final ExecutableElement constructor : constructors) { + if (constructor.getParameters().isEmpty() + && constructor.getModifiers().contains(Modifier.PUBLIC)) { + return true; + } + } + return false; + } + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + } +} diff --git a/libs/annotation/build.gradle b/libs/annotation/build.gradle new file mode 100644 index 0000000000..13a27e90c8 --- /dev/null +++ b/libs/annotation/build.gradle @@ -0,0 +1,6 @@ +apply plugin: "java-library" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} \ No newline at end of file diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java new file mode 100644 index 0000000000..68ff736352 --- /dev/null +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java @@ -0,0 +1,15 @@ +package im.conversations.android.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +public @interface XmlElement { + + String name() default ""; + + String namespace() default ""; +} diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java new file mode 100644 index 0000000000..462fc6965b --- /dev/null +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java @@ -0,0 +1,12 @@ +package im.conversations.android.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PACKAGE) +public @interface XmlPackage { + String namespace(); +} diff --git a/proguard-rules.pro b/proguard-rules.pro index 7098945c6a..0f28bdddfd 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -1,6 +1,7 @@ -dontobfuscate -keep class eu.siacs.conversations.** +-keep class im.conversations.** -keep class org.whispersystems.** diff --git a/settings.gradle b/settings.gradle index 4193570fa7..3cecbc8894 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ +include ':libs:annotation', ':libs:annotation-processor:' + rootProject.name = 'Conversations' diff --git a/src/cheogram/res/xml/cache_paths.xml b/src/cheogram/res/xml/cache_paths.xml new file mode 100644 index 0000000000..a02c3fdfde --- /dev/null +++ b/src/cheogram/res/xml/cache_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 99f0f40187..323e82d276 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -142,10 +142,6 @@ - - diff --git a/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java new file mode 100644 index 0000000000..e5e406ca7a --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; + +import java.io.Serial; + +public class ResolutionUnsuccessfulException extends MiniDnsException { + + /** + * + */ + @Serial + private static final long serialVersionUID = 1L; + + public final Question question; + public final RESPONSE_CODE responseCode; + + public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) { + super("Asking for " + question + " yielded an error response " + responseCode); + this.question = question; + this.responseCode = responseCode; + } +} diff --git a/src/main/java/de/gultsch/minidns/ResolverResult.java b/src/main/java/de/gultsch/minidns/ResolverResult.java new file mode 100644 index 0000000000..3d80fb8df4 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolverResult.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnssec.DnssecResultNotAuthenticException; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; + +public class ResolverResult { + + protected final Question question; + private final RESPONSE_CODE responseCode; + private final Set data; + private final boolean isAuthenticData; + protected final Set unverifiedReasons; + protected final DnsMessage answer; + protected final DnsQueryResult result; + + public ResolverResult(Question question, DnsQueryResult result, Set unverifiedReasons) throws NullResultException { + // TODO: Is this null check still needed? + if (result == null) { + throw new MiniDnsException.NullResultException(question.asMessageBuilder().build()); + } + + this.result = result; + + DnsMessage answer = result.response; + this.question = question; + this.responseCode = answer.responseCode; + this.answer = answer; + + Set r = answer.getAnswersFor(question); + if (r == null) { + this.data = Collections.emptySet(); + } else { + this.data = Collections.unmodifiableSet(r); + } + + if (unverifiedReasons == null) { + this.unverifiedReasons = null; + isAuthenticData = false; + } else { + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + isAuthenticData = this.unverifiedReasons.isEmpty(); + } + } + + public boolean wasSuccessful() { + return responseCode == RESPONSE_CODE.NO_ERROR; + } + + public Set getAnswers() { + throwIseIfErrorResponse(); + return data; + } + + public Set getAnswersOrEmptySet() { + return data; + } + + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + public boolean isAuthenticData() { + throwIseIfErrorResponse(); + return isAuthenticData; + } + + /** + * Get the reasons the result could not be verified if any exists. + * + * @return The reasons the result could not be verified or null. + */ + public Set getUnverifiedReasons() { + throwIseIfErrorResponse(); + return unverifiedReasons; + } + + public Question getQuestion() { + return question; + } + + public void throwIfErrorResponse() throws ResolutionUnsuccessfulException { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException; + } + + private ResolutionUnsuccessfulException resolutionUnsuccessfulException; + + public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() { + if (wasSuccessful()) return null; + + if (resolutionUnsuccessfulException == null) { + resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + } + + return resolutionUnsuccessfulException; + } + + private DnssecResultNotAuthenticException dnssecResultNotAuthenticException; + + public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() { + if (!wasSuccessful()) + return null; + if (isAuthenticData) + return null; + + if (dnssecResultNotAuthenticException == null) { + dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons()); + } + + return dnssecResultNotAuthenticException; + } + + /** + * Get the raw answer DNS message we received. This is likely not what you want, try {@link #getAnswers()} instead. + * + * @return the raw answer DNS Message. + * @see #getAnswers() + */ + public DnsMessage getRawAnswer() { + return answer; + } + + public DnsQueryResult getDnsQueryResult() { + return result; + } + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(getClass().getName()).append('\n') + .append("Question: ").append(question).append('\n') + .append("Response Code: ").append(responseCode).append('\n'); + + if (responseCode == RESPONSE_CODE.NO_ERROR) { + if (isAuthenticData) { + sb.append("Results verified via DNSSEC\n"); + } + if (hasUnverifiedReasons()) { + sb.append(unverifiedReasons).append('\n'); + } + sb.append(answer.answerSection); + } + + return sb.toString(); + } + + boolean hasUnverifiedReasons() { + return unverifiedReasons != null && !unverifiedReasons.isEmpty(); + } + + protected void throwIseIfErrorResponse() { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) + throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful", + resolutionUnsuccessfulException); + } +} diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index 0be3605cb4..080cbaf3f3 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -10,6 +10,8 @@ import androidx.preference.PreferenceManager; import com.google.common.base.Strings; +import java.security.SecureRandom; + public class AppSettings { public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; @@ -46,6 +48,9 @@ public class AppSettings { public static final String LARGE_FONT = "large_font"; public static final String SHOW_LINK_PREVIEWS = "show_link_previews"; + private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers"; + private static final String INSTALLATION_ID = "im.conversations.android.install_id"; + private final Context context; public AppSettings(final Context context) { @@ -143,4 +148,25 @@ public class AppSettings { public boolean isRequireChannelBinding() { return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding); } + + public synchronized long getInstallationId() { + final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + final long existing = sharedPreferences.getLong(INSTALLATION_ID, 0); + if (existing != 0) { + return existing; + } + final var secureRandom = new SecureRandom(); + final var installationId = secureRandom.nextLong(); + sharedPreferences.edit().putLong(INSTALLATION_ID, installationId).apply(); + return installationId; + } + + public synchronized void resetInstallationId() { + final var secureRandom = new SecureRandom(); + final var installationId = secureRandom.nextLong(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putLong(INSTALLATION_ID, installationId) + .apply(); + } } diff --git a/src/main/java/eu/siacs/conversations/Conversations.java b/src/main/java/eu/siacs/conversations/Conversations.java index 02e7791b50..36d2ea8dd8 100644 --- a/src/main/java/eu/siacs/conversations/Conversations.java +++ b/src/main/java/eu/siacs/conversations/Conversations.java @@ -1,5 +1,6 @@ package eu.siacs.conversations; +import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; @@ -15,9 +16,17 @@ import eu.siacs.conversations.utils.ThemeHelper; public class Conversations extends Application { + @SuppressLint("StaticFieldLeak") + private static Context CONTEXT; + + public static Context getContext() { + return Conversations.CONTEXT; + } + @Override public void onCreate() { super.onCreate(); + CONTEXT = this.getApplicationContext(); ExceptionHelper.init(getApplicationContext()); applyThemeSettings(); } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 5d45fbf207..7a0e9457c9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -61,7 +61,6 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.DescriptionTransport; import eu.siacs.conversations.xmpp.jingle.OmemoVerification; import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap; @@ -70,8 +69,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.pep.PublishOptions; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @@ -392,20 +390,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); return; } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); - } else { - //TODO consider calling registerDevices only after item-not-found to account for broken PEPs - Element item = mXmppConnectionService.getIqParser().getItem(packet); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); - registerDevices(account.getJid().asBareJid(), deviceIds); - } + Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, response -> { + if (response.getType() == Iq.Type.TIMEOUT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); + } else { + //TODO consider calling registerDevices only after item-not-found to account for broken PEPs + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); + registerDevices(account.getJid().asBareJid(), deviceIds); } + }); } @@ -455,40 +451,37 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); - mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - - @Override - public void onPushFailed() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - }); - } else { - if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); - mXmppConnectionService.databaseBackend.updateAccount(account); + final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); + mXmppConnectionService.sendIqPacket(account, publish, response -> { + final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; + final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response); + if (firstAttempt && preConditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); + mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceIdsAndRefineAccessModel(ids, false); } - if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt"); - } else if (error != null) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); - } + @Override + public void onPushFailed() { + publishDeviceIdsAndRefineAccessModel(ids, false); } + }); + } else { + if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); + account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (response.getType() == Iq.Type.ERROR) { + if (preConditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt"); + } else if (error != null) { + pepBroken = true; + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error")); + } + } } }); @@ -506,26 +499,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); - IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); + final Iq packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } + mXmppConnectionService.sendIqPacket(account, packet, response -> { + String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - }); - } + @Override + public void onPushFailed() { + Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } + }); }); } catch (Exception e) { e.printStackTrace(); @@ -549,109 +539,106 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (this.changeAccessMode.get()) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; //ignore timeout. do nothing + if (response.getType() == Iq.Type.TIMEOUT) { + return; //ignore timeout. do nothing + } + + if (response.getType() == Iq.Type.ERROR) { + Element error = response.findChild("error"); + if (error == null || !error.hasChild("item-not-found")) { + pepBroken = true; + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response); + return; + } + } + + PreKeyBundle bundle = IqParser.bundle(response); + final Map keys = IqParser.preKeyPublics(response); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; } - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.findChild("error"); - if (error == null || !error.hasChild("item-not-found")) { - pepBroken = true; - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); - return; - } - } - - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); - } + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - changed = true; - } - - // Validate signedPreKeyRecord + ID - SignedPreKeyRecord signedPreKeyRecord; - int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); - try { - signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if (flush - || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - } catch (InvalidKeyIdException e) { + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; } - - // Validate PreKeys - Set preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); - } - } catch (InvalidKeyIdException ignored) { - } - } - } - int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); - if (newKeys > 0) { - List newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId() + 1, newKeys); - preKeyRecords.addAll(newRecords); - for (PreKeyRecord record : newRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); - } - - - if (changed || changeAccessMode.get()) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } catch (InvalidKeyIdException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; } + + // Validate PreKeys + Set preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); + } + + + if (changed || changeAccessMode.get()) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } else { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); + if (wipe) { + wipeOtherPepDevices(); + } else if (announce) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); } }); } @@ -669,44 +656,41 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final boolean wipe, final boolean firstAttempt) { final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + final Iq publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, getOwnDeviceId(), publishOptions); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing..."); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preconditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } + mXmppConnectionService.sendIqPacket(account, publish, response -> { + final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response); + if (firstAttempt && preconditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); + final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); + } - @Override - public void onPushFailed() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - }); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); + @Override + public void onPushFailed() { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preconditionNotMet) { - Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt"); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.toString()); - } - pepBroken = true; + }); + } else if (response.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); } + } else if (response.getType() == Iq.Type.ERROR) { + if (preconditionNotMet) { + Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt"); + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString()); + } + pepBroken = true; } }); } @@ -759,9 +743,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } final SettableFuture future = SettableFuture.create(); - final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - Pair verification = mXmppConnectionService.getIqParser().verification(response); + final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, response -> { + Pair verification = IqParser.verification(response); if (verification != null) { try { Signature verifier = Signature.getInstance("sha256WithRSA"); @@ -846,7 +830,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { - IqPacket packet; + final Iq packet; synchronized (this.fetchDeviceIdsMap) { List callbacks = this.fetchDeviceIdsMap.get(jid); if (callbacks != null) { @@ -866,11 +850,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + mXmppConnectionService.sendIqPacket(account, packet, response -> { + if (response.getType() == Iq.Type.RESULT) { fetchDeviceListStatus.put(jid, true); - Element item = mXmppConnectionService.getIqParser().getItem(response); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); registerDevices(jid, deviceIds); final List callbacks; synchronized (fetchDeviceIdsMap) { @@ -882,7 +866,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } } else { - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { fetchDeviceListStatus.remove(jid); } else { fetchDeviceListStatus.put(jid, false); @@ -929,16 +913,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } final Jid jid = Jid.of(address.getName()); final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, packet -> { + if (packet.getType() == Iq.Type.TIMEOUT) { fetchStatusMap.put(address, FetchStatus.TIMEOUT); sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { + } else if (packet.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); + final List preKeyBundleList = IqParser.preKeys(packet); + final PreKeyBundle bundle = IqParser.bundle(packet); if (preKeyBundleList.isEmpty() || bundle == null) { Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); @@ -1544,7 +1527,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { axolotlMessage.addDevice(session, true); try { final Jid jid = Jid.of(session.getRemoteAddress().getName()); - MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); mXmppConnectionService.sendMessagePacket(account, packet); } catch (IllegalArgumentException e) { throw new Error("Remote addresses are created from jid and should convert back to jid", e); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index fd27785de5..f99c2cf735 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -146,10 +146,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Option; import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; import static eu.siacs.conversations.entities.Bookmark.printableValue; +import im.conversations.android.xmpp.model.stanza.Iq; public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; @@ -1600,7 +1600,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl show(); CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService); - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(command.getAttributeAsJid("jid")); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", command.getAttribute("node")); @@ -1618,7 +1618,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } }, 1000); } else { - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { session.updateWithResponse(iq); }, 120L); } @@ -1645,7 +1645,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void startMucConfig(XmppConnectionService xmppConnectionService) { MucConfigSession session = new MucConfigSession(xmppConnectionService); - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + final var packet = new Iq(Iq.Type.GET); packet.setTo(Conversation.this.getJid().asBareJid()); packet.addChild("query", "http://jabber.org/protocol/muc#owner"); @@ -1661,7 +1661,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } }, 1000); } else { - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { session.updateWithResponse(iq); }, 120L); } @@ -2782,7 +2782,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected Item mkItem(Element el, int pos) { int viewType = TYPE_ERROR; - if (response != null && response.getType() == IqPacket.TYPE.RESULT) { + if (response != null && response.getType() == Iq.Type.RESULT) { if (el.getName().equals("note")) { viewType = TYPE_NOTE; } else if (el.getNamespace().equals("jabber:x:oob")) { @@ -2883,7 +2883,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected String mTitle; protected String mNode; protected CommandPageBinding mBinding = null; - protected IqPacket response = null; + protected Iq response = null; protected Element responseElement = null; protected boolean expectingRemoval = false; protected List reported = null; @@ -2893,7 +2893,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected GridLayoutManager layoutManager; protected WebView actionToWebview = null; protected int fillableFieldCount = 0; - protected IqPacket pendingResponsePacket = null; + protected Iq pendingResponsePacket = null; protected boolean waitingForRefresh = false; CommandSession(String title, String node, XmppConnectionService xmppConnectionService) { @@ -2912,7 +2912,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return mNode; } - public void updateWithResponse(final IqPacket iq) { + public void updateWithResponse(final Iq iq) { if (getView() != null && getView().isAttachedToWindow()) { getView().post(() -> updateWithResponseUiThread(iq)); } else { @@ -2920,7 +2920,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - protected void updateWithResponseUiThread(final IqPacket iq) { + protected void updateWithResponseUiThread(final Iq iq) { Timer oldTimer = this.loadingTimer; this.loadingTimer = new Timer(); oldTimer.cancel(); @@ -2937,7 +2937,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl boolean actionsCleared = false; Element command = iq.findChild("command", "http://jabber.org/protocol/commands"); - if (iq.getType() == IqPacket.TYPE.RESULT && command != null) { + if (iq.getType() == Iq.Type.RESULT && command != null) { if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) { xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true); } @@ -3101,7 +3101,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int getItemCount() { if (loading) return 1; if (response == null) return 0; - if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) { + if (response.getType() == Iq.Type.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) { int i = 0; for (Element el : responseElement.getChildren()) { if (!el.getNamespace().equals("jabber:x:data")) continue; @@ -3134,7 +3134,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (items.get(position) != null) return items.get(position); if (response == null) return null; - if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) { + if (response.getType() == Iq.Type.RESULT && responseElement != null) { if (responseElement.getNamespace().equals("jabber:x:data")) { int i = 0; for (Element el : responseElement.getChildren()) { @@ -3317,7 +3317,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return false; } - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", mNode); @@ -3360,7 +3360,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (c.getAttribute("action") == null) c.setAttribute("action", action); executing = true; - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { updateWithResponse(iq); }, 120L); @@ -3495,7 +3495,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl actionsAdapter.notifyDataSetChanged(); if (pendingResponsePacket != null) { - final IqPacket pending = pendingResponsePacket; + final var pending = pendingResponsePacket; pendingResponsePacket = null; updateWithResponseUiThread(pending); } @@ -3570,7 +3570,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } @Override - protected void updateWithResponseUiThread(final IqPacket iq) { + protected void updateWithResponseUiThread(final Iq iq) { Timer oldTimer = this.loadingTimer; this.loadingTimer = new Timer(); oldTimer.cancel(); @@ -3586,7 +3586,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl layoutManager.setSpanCount(1); final Element query = iq.findChild("query", "http://jabber.org/protocol/muc#owner"); - if (iq.getType() == IqPacket.TYPE.RESULT && query != null) { + if (iq.getType() == Iq.Type.RESULT && query != null) { final Data form = Data.parse(query.findChild("x", "jabber:x:data")); final String title = form.getTitle(); if (title != null) { @@ -3605,7 +3605,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (actionsAdapter.getPosition("cancel") < 0) { actionsAdapter.insert(Pair.create("cancel", "cancel"), 0); } - } else if (iq.getType() == IqPacket.TYPE.RESULT) { + } else if (iq.getType() == Iq.Type.RESULT) { expectingRemoval = true; removeSession(this); return; @@ -3619,7 +3619,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl @Override public synchronized boolean execute(String action) { if ("cancel".equals(action)) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); final Element form = packet .addChild("query", "http://jabber.org/protocol/muc#owner") @@ -3631,7 +3631,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (!"save".equals(action)) return true; - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); String formType = responseElement == null ? null : responseElement.getAttribute("type"); @@ -3647,7 +3647,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } executing = true; - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { updateWithResponse(iq); }, 120L); diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index f8120721f3..324c71c71e 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -29,6 +29,14 @@ import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xml.Element; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + public class MucOptions { public static final String STATUS_CODE_SELF_PRESENCE = "110"; @@ -199,6 +207,11 @@ public class MucOptions { } } + public boolean allowPmRaw() { + final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm"); + return field == null || Arrays.asList("anyone","participants").contains(field.getValue()); + } + public boolean participating() { return self.getRole().ranks(Role.PARTICIPANT) || !moderated(); } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 1168d205e1..e784b3e8cb 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -24,7 +24,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class ServiceDiscoveryResult { public static final String TABLENAME = "discovery_results"; @@ -36,7 +36,7 @@ public class ServiceDiscoveryResult { protected final List features; protected final List forms; private final List identities; - public ServiceDiscoveryResult(final IqPacket packet) { + public ServiceDiscoveryResult(final Iq packet) { this.identities = new ArrayList<>(); this.features = new ArrayList<>(); this.forms = new ArrayList<>(); @@ -279,7 +279,7 @@ public class ServiceDiscoveryResult { return values; } - public static class Identity implements Comparable { + public static class Identity implements Comparable { protected final String type; protected final String lang; protected final String name; @@ -327,8 +327,21 @@ public class ServiceDiscoveryResult { return this.name; } - public int compareTo(@NonNull Object other) { - Identity o = (Identity) other; + JSONObject toJSON() { + try { + JSONObject o = new JSONObject(); + o.put("category", this.getCategory()); + o.put("type", this.getType()); + o.put("lang", this.getLang()); + o.put("name", this.getName()); + return o; + } catch (JSONException e) { + return null; + } + } + + @Override + public int compareTo(final Identity o) { int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); if (r == 0) { r = blankNull(this.getType()).compareTo(blankNull(o.getType())); @@ -342,18 +355,5 @@ public class ServiceDiscoveryResult { return r; } - - JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - o.put("category", this.getCategory()); - o.put("type", this.getType()); - o.put("lang", this.getLang()); - o.put("name", this.getName()); - return o; - } catch (JSONException e) { - return null; - } - } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index f3db999842..137acb5fea 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -45,7 +45,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class IqGenerator extends AbstractGenerator { @@ -53,8 +53,8 @@ public class IqGenerator extends AbstractGenerator { super(service); } - public IqPacket discoResponse(final Account account, final IqPacket request) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); + public Iq discoResponse(final Account account, final Iq request) { + final var packet = new Iq(Iq.Type.RESULT); packet.setId(request.getId()); packet.setTo(request.getFrom()); final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); @@ -69,8 +69,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket versionResponse(final IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + public Iq versionResponse(final Iq request) { + final var packet = request.generateResponse(Iq.Type.RESULT); Element query = packet.query("jabber:iq:version"); query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); query.addChild("version").setContent(getIdentityVersion()); @@ -93,8 +93,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket entityTimeResponse(IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + public Iq entityTimeResponse(final Iq request) { + final Iq packet = request.generateResponse(Iq.Type.RESULT); Element time = packet.addChild("time", "urn:xmpp:time"); final long now = System.currentTimeMillis(); time.addChild("utc").setContent(getTimestamp(now)); @@ -113,14 +113,14 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket purgeOfflineMessages() { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public static Iq purgeOfflineMessages() { + final Iq packet = new Iq(Iq.Type.SET); packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); return packet; } - protected IqPacket publish(final String node, final Element item, final Bundle options) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + protected Iq publish(final String node, final Element item, final Bundle options) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element publish = pubsub.addChild("publish"); publish.setAttribute("node", node); @@ -132,12 +132,12 @@ public class IqGenerator extends AbstractGenerator { return packet; } - protected IqPacket publish(final String node, final Element item) { + protected Iq publish(final String node, final Element item) { return publish(node, item, null); } - private IqPacket retrieve(String node, Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + private Iq retrieve(String node, Element item) { + final var packet = new Iq(Iq.Type.GET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element items = pubsub.addChild("items"); items.setAttribute("node", node); @@ -147,36 +147,36 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket retrieveVcard4(final Jid jid) { - final IqPacket packet = retrieve("urn:xmpp:vcard4", null); + public Iq retrieveVcard4(final Jid jid) { + final var packet = retrieve("urn:xmpp:vcard4", null); packet.setTo(jid); return packet; } - public IqPacket retrieveBookmarks() { + public Iq retrieveBookmarks() { return retrieve(Namespace.BOOKMARKS2, null); } - public IqPacket retrieveMds() { + public Iq retrieveMds() { return retrieve(Namespace.MDS_DISPLAYED, null); } - public IqPacket publishNick(String nick) { + public Iq publishNick(String nick) { final Element item = new Element("item"); item.setAttribute("id", "current"); item.addChild("nick", Namespace.NICK).setContent(nick); return publish(Namespace.NICK, item); } - public IqPacket deleteNode(final String node) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq deleteNode(final String node) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); pubsub.addChild("delete").setAttribute("node", node); return packet; } - public IqPacket deleteItem(final String node, final String id) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq deleteItem(final String node, final String id) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element retract = pubsub.addChild("retract"); retract.setAttribute("node", node); @@ -185,7 +185,7 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket publishAvatar(Avatar avatar, Bundle options) { + public Iq publishAvatar(Avatar avatar, Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element data = item.addChild("data", Namespace.AVATAR_DATA); @@ -193,14 +193,14 @@ public class IqGenerator extends AbstractGenerator { return publish(Namespace.AVATAR_DATA, item, options); } - public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) { + public Iq publishElement(final String namespace, final Element element, String id, final Bundle options) { final Element item = new Element("item"); item.setAttribute("id", id); item.addChild(element); return publish(namespace, item, options); } - public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) { + public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element metadata = item @@ -214,57 +214,57 @@ public class IqGenerator extends AbstractGenerator { return publish(Namespace.AVATAR_METADATA, item, options); } - public IqPacket retrievePepAvatar(final Avatar avatar) { + public Iq retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item); + final var packet = retrieve(Namespace.AVATAR_DATA, item); packet.setTo(avatar.owner); return packet; } - public IqPacket retrieveVcardAvatar(final Avatar avatar) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq retrieveVcardAvatar(final Avatar avatar) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(avatar.owner); packet.addChild("vCard", "vcard-temp"); return packet; } - public IqPacket retrieveVcardAvatar(final Jid to) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq retrieveVcardAvatar(final Jid to) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(to); packet.addChild("vCard", "vcard-temp"); return packet; } - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + public Iq retrieveAvatarMetaData(final Jid to) { + final Iq packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { packet.setTo(to); } return packet; } - public IqPacket retrieveDeviceIds(final Jid to) { - final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + public Iq retrieveDeviceIds(final Jid to) { + final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if (to != null) { packet.setTo(to); } return packet; } - public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); + public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) { + final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); packet.setTo(to); return packet; } - public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); + public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) { + final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); packet.setTo(to); return packet; } - public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { + public Iq publishDeviceIds(final Set ids, final Bundle publishOptions) { final Element item = new Element("item"); item.setAttribute("id", "current"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); @@ -314,7 +314,7 @@ public class IqGenerator extends AbstractGenerator { return displayed; } - public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + public Iq publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, final Set preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); item.setAttribute("id", "current"); @@ -338,7 +338,7 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); } - public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { + public Iq publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { final Element item = new Element("item"); item.setAttribute("id", "current"); final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); @@ -356,8 +356,8 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); } - public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) { + final Iq packet = new Iq(Iq.Type.SET); final Element query = packet.query(mam.version.namespace); query.setAttribute("queryid", mam.getQueryId()); final Data data = new Data(); @@ -387,15 +387,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket generateGetBlockList() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + public Iq generateGetBlockList() { + final Iq iq = new Iq(Iq.Type.GET); iq.addChild("blocklist", Namespace.BLOCKING); return iq; } - public IqPacket generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) { + final Iq iq = new Iq(Iq.Type.SET); final Element block = iq.addChild("block", Namespace.BLOCKING); final Element item = block.addChild("item").setAttribute("jid", jid); if (reportSpam) { @@ -411,15 +411,15 @@ public class IqGenerator extends AbstractGenerator { return iq; } - public IqPacket generateSetUnblockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetUnblockRequest(final Jid jid) { + final Iq iq = new Iq(Iq.Type.SET); final Element block = iq.addChild("unblock", Namespace.BLOCKING); block.addChild("item").setAttribute("jid", jid); return iq; } - public IqPacket generateSetPassword(final Account account, final String newPassword) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetPassword(final Account account, final String newPassword) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(account.getDomain()); final Element query = packet.addChild("query", Namespace.REGISTER); final Jid jid = account.getJid(); @@ -428,14 +428,14 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { + public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) { List jids = new ArrayList<>(); jids.add(jid); return changeAffiliation(conference, jids, affiliation); } - public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + 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"); @@ -447,8 +447,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket changeRole(Conversation conference, String nick, String role) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq changeRole(Conversation conference, String nick, String role) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(conference.getJid().asBareJid()); packet.setFrom(conference.getAccount().getJid()); Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); @@ -457,11 +457,11 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket moderateMessage(Account account, Message m, String reason) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq moderateMessage(Account account, Message m, String reason) { + final var packet = new Iq(Iq.Type.SET); packet.setTo(m.getConversation().getJid().asBareJid()); packet.setFrom(account.getJid()); - Element moderate = + final var moderate = packet.addChild("apply-to", "urn:xmpp:fasten:0") .setAttribute("id", m.getServerMsgId()) .addChild("moderate", "urn:xmpp:message-moderate:0"); @@ -470,8 +470,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(host); Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); request.setAttribute("filename", name == null ? convertFilename(file.getName()) : name); @@ -480,8 +480,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(host); Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY); request.addChild("filename").setContent(convertFilename(file.getName())); @@ -507,8 +507,8 @@ public class IqGenerator extends AbstractGenerator { } } - public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { - final IqPacket register = new IqPacket(IqPacket.TYPE.SET); + public static Iq generateCreateAccountWithCaptcha(final Account account, final String id, final Data data) { + final Iq register = new Iq(Iq.Type.SET); register.setFrom(account.getJid().asBareJid()); register.setTo(account.getDomain()); register.setId(id); @@ -519,12 +519,12 @@ public class IqGenerator extends AbstractGenerator { return register; } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { + public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) { return pushTokenToAppServer(appServer, token, deviceId, null); } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(appServer); final Element command = packet.addChild("command", Namespace.COMMANDS); command.setAttribute("node", "register-push-fcm"); @@ -540,8 +540,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(appServer); final Element command = packet.addChild("command", Namespace.COMMANDS); command.setAttribute("node", "unregister-push-fcm"); @@ -554,8 +554,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket enablePush(final Jid jid, final String node, final String secret) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq enablePush(final Jid jid, final String node, final String secret) { + final Iq packet = new Iq(Iq.Type.SET); Element enable = packet.addChild("enable", Namespace.PUSH); enable.setAttribute("jid", jid); enable.setAttribute("node", node); @@ -569,16 +569,16 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket disablePush(final Jid jid, final String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq disablePush(final Jid jid, final String node) { + Iq packet = new Iq(Iq.Type.SET); Element disable = packet.addChild("disable", Namespace.PUSH); disable.setAttribute("jid", jid); disable.setAttribute("node", node); return packet; } - public IqPacket queryAffiliation(Conversation conversation, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryAffiliation(Conversation conversation, String affiliation) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(conversation.getJid().asBareJid()); packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation); return packet; @@ -611,16 +611,16 @@ public class IqGenerator extends AbstractGenerator { return options; } - public IqPacket requestPubsubConfiguration(Jid jid, String node) { + public Iq requestPubsubConfiguration(Jid jid, String node) { return pubsubConfiguration(jid, node, null); } - public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { + public Iq publishPubsubConfiguration(Jid jid, String node, Data data) { return pubsubConfiguration(jid, node, data); } - private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { - IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); + private Iq pubsubConfiguration(Jid jid, String node, Data data) { + final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET); packet.setTo(jid); Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); Element configure = pubsub.addChild("configure").setAttribute("node", node); @@ -630,43 +630,43 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket queryDiscoItems(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryDiscoItems(final Jid jid) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(jid); packet.query(Namespace.DISCO_ITEMS); return packet; } - public IqPacket queryDiscoItems(Jid jid, String node) { - IqPacket packet = queryDiscoItems(jid); - final Element query = packet.query(Namespace.DISCO_ITEMS); + public Iq queryDiscoItems(Jid jid, String node) { + final var packet = queryDiscoItems(jid); + final var query = packet.query(Namespace.DISCO_ITEMS); query.setAttribute("node", node); return packet; } - public IqPacket queryDiscoInfo(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryDiscoInfo(final Jid jid) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(jid); packet.addChild("query",Namespace.DISCO_INFO); return packet; } - public IqPacket bobResponse(IqPacket request) { + public Iq bobResponse(Iq request) { try { - String bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid"); - Cid cid = BobTransfer.cid(bobCid); - DownloadableFile f = mXmppConnectionService.getFileForCid(cid); + final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid"); + final var cid = BobTransfer.cid(bobCid); + final var f = mXmppConnectionService.getFileForCid(cid); if (f == null || !f.canRead()) { throw new IOException("No such file"); } else if (f.getSize() > 129000) { - final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); + final var response = request.generateResponse(Iq.Type.ERROR); + final var error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas"); return response; } else { - final IqPacket response = request.generateResponse(IqPacket.TYPE.RESULT); - final Element data = response.addChild("data", "urn:xmpp:bob"); + final var response = request.generateResponse(Iq.Type.RESULT); + final var data = response.addChild("data", "urn:xmpp:bob"); data.setAttribute("cid", bobCid); data.setAttribute("type", f.getMimeType()); ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2); @@ -678,8 +678,8 @@ public class IqGenerator extends AbstractGenerator { return response; } } catch (final IOException | IllegalStateException e) { - final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); + final var response = request.generateResponse(Iq.Type.ERROR); + final var error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); return response; diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 8627b15562..463bad4e35 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -23,7 +23,6 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; @@ -33,25 +32,25 @@ public class MessageGenerator extends AbstractGenerator { super(service); } - private MessagePacket preparePacket(Message message, boolean legacyEncryption) { + private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) { Conversation conversation = (Conversation) message.getConversation(); Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); final boolean isWithSelf = conversation.getContact().isSelf(); if (conversation.getMode() == Conversation.MODE_SINGLE) { packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); if (!isWithSelf) { packet.addChild("request", "urn:xmpp:receipts"); } } else if (message.isPrivateMessage()) { packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.addChild("x", "http://jabber.org/protocol/muc#user"); packet.addChild("request", "urn:xmpp:receipts"); } else { packet.setTo(message.getCounterpart().asBareJid()); - packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); } if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) { packet.addChild("markable", "urn:xmpp:chat-markers:0"); @@ -78,7 +77,7 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public void addDelay(MessagePacket packet, long timestamp) { + public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) { final SimpleDateFormat mDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -87,8 +86,8 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = preparePacket(message, true); + public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { + im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true); if (axolotlMessage == null) { return null; } @@ -101,17 +100,18 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); + public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(to); packet.setAxolotlMessage(axolotlMessage.toElement()); packet.addChild("store", "urn:xmpp:hints"); return packet; } - public MessagePacket generateChat(Message message) { - MessagePacket packet = preparePacket(message, false); + public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) { + im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false); + String content; if (message.hasFileOnRemoteHost()) { final Message.FileParams fileParams = message.getFileParams(); @@ -139,8 +139,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generatePgpChat(Message message) { - MessagePacket packet = preparePacket(message, true); + public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) { + final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true); if (message.hasFileOnRemoteHost()) { Message.FileParams fileParams = message.getFileParams(); final String url = fileParams.url; @@ -163,10 +163,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateChatState(Conversation conversation) { + public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) { final Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(account.getJid()); packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); @@ -175,11 +175,11 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket confirm(final Message message) { + public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) { final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI; final Jid to = message.getCounterpart(); - final MessagePacket packet = new MessagePacket(); - packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(groupChat ? to.asBareJid() : to); final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); if (groupChat) { @@ -197,20 +197,20 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket conferenceSubject(Conversation conversation, String subject) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_GROUPCHAT); + public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); packet.setTo(conversation.getJid().asBareJid()); packet.addChild("subject").setContent(subject); packet.setFrom(conversation.getAccount().getJid().asBareJid()); return packet; } - public MessagePacket requestVoice(Jid jid) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); + public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) { + final var packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL); packet.setTo(jid.asBareJid()); - Data form = new Data(); + final var form = new Data(); form.setFormType("http://jabber.org/protocol/muc#request"); form.put("muc#role", "participant"); form.submit(); @@ -218,9 +218,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket directInvite(final Conversation conversation, final Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); + public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL); packet.setTo(contact); packet.setFrom(conversation.getAccount().getJid()); Element x = packet.addChild("x", "jabber:x:conference"); @@ -236,8 +236,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket invite(final Conversation conversation, final Jid contact) { - final MessagePacket packet = new MessagePacket(); + public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) { + final var packet = new im.conversations.android.xmpp.model.stanza.Message(); packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(conversation.getAccount().getJid()); Element x = new Element("x"); @@ -249,8 +249,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { - final MessagePacket receivedPacket = new MessagePacket(); + public im.conversations.android.xmpp.model.stanza.Message received(Account account, final Jid from, final String id, ArrayList namespaces, im.conversations.android.xmpp.model.stanza.Message.Type type) { + final var receivedPacket = + new im.conversations.android.xmpp.model.stanza.Message(); receivedPacket.setType(type); receivedPacket.setTo(from); receivedPacket.setFrom(account.getJid()); @@ -261,8 +262,8 @@ public class MessageGenerator extends AbstractGenerator { return receivedPacket; } - public MessagePacket received(Account account, Jid to, String id) { - MessagePacket packet = new MessagePacket(); + public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); packet.setFrom(account.getJid()); packet.setTo(to); packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); @@ -270,10 +271,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionFinish( + public im.conversations.android.xmpp.model.stanza.Message sessionFinish( final Jid with, final String sessionId, final Reason reason) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(with); final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE); finish.setAttribute("id", sessionId); @@ -283,9 +284,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(proposal.with); packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); @@ -298,9 +299,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(proposal.with); final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); @@ -309,9 +310,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionReject(final Jid with, final String sessionId) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(with); final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", sessionId); diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 4d2d38ce54..ca166be4bc 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -9,7 +9,6 @@ import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceGenerator extends AbstractGenerator { @@ -17,20 +16,20 @@ public class PresenceGenerator extends AbstractGenerator { super(service); } - private PresencePacket subscription(String type, Contact contact) { - PresencePacket packet = new PresencePacket(); + private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) { + im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); packet.setAttribute("type", type); packet.setTo(contact.getJid()); packet.setFrom(contact.getAccount().getJid().asBareJid()); return packet; } - public PresencePacket requestPresenceUpdatesFrom(final Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) { return requestPresenceUpdatesFrom(contact, null); } - public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { - PresencePacket packet = subscription("subscribe", contact); + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { + im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact); String displayName = contact.getAccount().getDisplayName(); if (!TextUtils.isEmpty(displayName)) { packet.addChild("nick", Namespace.NICK).setContent(displayName); @@ -41,24 +40,24 @@ public class PresenceGenerator extends AbstractGenerator { return packet; } - public PresencePacket stopPresenceUpdatesFrom(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(Contact contact) { return subscription("unsubscribe", contact); } - public PresencePacket stopPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(Contact contact) { return subscription("unsubscribed", contact); } - public PresencePacket sendPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(Contact contact) { return subscription("subscribed", contact); } - public PresencePacket selfPresence(Account account, Presence.Status status) { + public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Status status) { return selfPresence(account, status, true, null); } - public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) { - final PresencePacket packet = new PresencePacket(); + public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) { + final im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); if (personal) { final String sig = account.getPgpSignature(); final String message = account.getPresenceStatusMessage(); @@ -87,16 +86,16 @@ public class PresenceGenerator extends AbstractGenerator { return packet; } - public PresencePacket leave(final MucOptions mucOptions) { - PresencePacket presencePacket = new PresencePacket(); - presencePacket.setTo(mucOptions.getSelf().getFullJid()); - presencePacket.setFrom(mucOptions.getAccount().getJid()); - presencePacket.setAttribute("type", "unavailable"); - return presencePacket; + public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) { + im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence(); + presence.setTo(mucOptions.getSelf().getFullJid()); + presence.setFrom(mucOptions.getAccount().getJid()); + presence.setAttribute("type", "unavailable"); + return presence; } - public PresencePacket sendOfflinePresence(Account account) { - PresencePacket packet = new PresencePacket(); + public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) { + im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); packet.setFrom(account.getJid()); packet.setAttribute("type", "unavailable"); return packet; diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 0fe254b82d..5f60adb8d8 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -2,11 +2,27 @@ package eu.siacs.conversations.http; import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; +import android.content.Context; import android.os.Build; import android.util.Log; import androidx.core.util.Consumer; +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.TrustManagers; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.TLSSocketFactory; + +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; + import org.apache.http.conn.ssl.StrictHostnameVerifier; import java.io.IOException; @@ -16,7 +32,9 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.UnknownHostException; import java.security.KeyManagementException; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @@ -26,19 +44,6 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.TLSSocketFactory; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.ResponseBody; - public class HttpConnectionManager extends AbstractConnectionManager { private final List downloadConnections = new ArrayList<>(); @@ -46,7 +51,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { public static final Executor EXECUTOR = Executors.newFixedThreadPool(4); - public static final OkHttpClient OK_HTTP_CLIENT; + private static final OkHttpClient OK_HTTP_CLIENT; static { OK_HTTP_CLIENT = new OkHttpClient.Builder() @@ -209,4 +214,27 @@ public class HttpConnectionManager extends AbstractConnectionManager { return filename; } + + public static OkHttpClient okHttpClient(final Context context) { + final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); + try { + final X509TrustManager trustManager; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); + } else { + trustManager = TrustManagers.createDefaultTrustManager(); + } + final SSLSocketFactory socketFactory = + new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); + builder.sslSocketFactory(socketFactory, trustManager); + } catch (final IOException + | KeyManagementException + | NoSuchAlgorithmException + | KeyStoreException + | CertificateException e) { + Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); + throw new RuntimeException(e); + } + return builder.build(); + } } diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java index d0a39ca5d5..b3af132b7e 100644 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ b/src/main/java/eu/siacs/conversations/http/SlotRequester.java @@ -43,7 +43,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.IqResponseException; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -67,9 +67,9 @@ public class SlotRequester { private ListenableFuture requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) { final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + final Iq request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); + service.sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); if (slotElement != null) { try { @@ -97,9 +97,9 @@ public class SlotRequester { private ListenableFuture requestHttpUpload(Account account, Jid host, DownloadableFile file, String fname, String mime) { final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime); + service.sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); if (slotElement != null) { try { diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 02f1b91c1d..63fd084f4c 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -18,14 +18,16 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import im.conversations.android.xmpp.model.stanza.Stanza; public abstract class AbstractParser { - protected XmppConnectionService mXmppConnectionService; + protected final XmppConnectionService mXmppConnectionService; + protected final Account account; - protected AbstractParser(XmppConnectionService service) { + protected AbstractParser(final XmppConnectionService service, final Account account) { this.mXmppConnectionService = service; + this.account = account; } public static Long parseTimestamp(Element element, Long d) { @@ -36,8 +38,8 @@ public abstract class AbstractParser { long min = Long.MAX_VALUE; boolean returnDefault = true; final Jid to; - if (ignoreCsiAndSm && element instanceof AbstractStanza) { - to = ((AbstractStanza) element).getTo(); + if (ignoreCsiAndSm && element instanceof Stanza stanza) { + to = stanza.getTo(); } else { to = null; } @@ -125,7 +127,7 @@ public abstract class AbstractParser { contact.setLastResource(from.isBareJid() ? "" : from.getResource()); } - protected String avatarData(Element items) { + protected static String avatarData(Element items) { Element item = items.findChild("item"); if (item == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index c401c29d21..95b239802f 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -38,18 +39,17 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; -public class IqParser extends AbstractParser implements OnIqPacketReceived { +public class IqParser extends AbstractParser implements Consumer { - public IqParser(final XmppConnectionService service) { - super(service); + public IqParser(final XmppConnectionService service, final Account account) { + super(service, account); } - public static List items(IqPacket packet) { + public static List items(final Iq packet) { ArrayList items = new ArrayList<>(); final Element query = packet.findChild("query", Namespace.DISCO_ITEMS); if (query == null) { @@ -66,7 +66,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items; } - public static Room parseRoom(IqPacket packet) { + public static Room parseRoom(Iq packet) { final Element query = packet.findChild("query", Namespace.DISCO_INFO); if (query == null) { return null; @@ -144,7 +144,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.syncRoster(account); } - public String avatarData(final IqPacket packet) { + public static String avatarData(final Iq packet) { final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); if (pubsub == null) { return null; @@ -153,10 +153,10 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { if (items == null) { return null; } - return super.avatarData(items); + return AbstractParser.avatarData(items); } - public Element getItem(final IqPacket packet) { + public static Element getItem(final Iq packet) { final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); if (pubsub == null) { return null; @@ -169,7 +169,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } @NonNull - public Set deviceIds(final Element item) { + public static Set deviceIds(final Element item) { Set deviceIds = new HashSet<>(); if (item != null) { final Element list = item.findChild("list"); @@ -190,7 +190,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return deviceIds; } - private Integer signedPreKeyId(final Element bundle) { + private static Integer signedPreKeyId(final Element bundle) { final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); if (signedPreKeyPublic == null) { return null; @@ -202,7 +202,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - private ECPublicKey signedPreKeyPublic(final Element bundle) { + private static ECPublicKey signedPreKeyPublic(final Element bundle) { ECPublicKey publicKey = null; final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic"); if (signedPreKeyPublic == null) { @@ -216,7 +216,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return publicKey; } - private byte[] signedPreKeySignature(final Element bundle) { + private static byte[] signedPreKeySignature(final Element bundle) { final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature"); if (signedPreKeySignature == null) { return null; @@ -229,7 +229,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - private IdentityKey identityKey(final Element bundle) { + private static IdentityKey identityKey(final Element bundle) { final String identityKey = bundle.findChildContent("identityKey"); if (identityKey == null) { return null; @@ -242,7 +242,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - public Map preKeyPublics(final IqPacket packet) { + public static Map preKeyPublics(final Iq packet) { Map preKeyRecords = new HashMap<>(); Element item = getItem(packet); if (item == null) { @@ -285,7 +285,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input)); } - public Pair verification(final IqPacket packet) { + public static Pair verification(final Iq packet) { Element item = getItem(packet); Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null; Element chain = verification != null ? verification.findChild("chain") : null; @@ -313,7 +313,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - public PreKeyBundle bundle(final IqPacket bundle) { + public static PreKeyBundle bundle(final Iq bundle) { final Element bundleItem = getItem(bundle); if (bundleItem == null) { return null; @@ -337,7 +337,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); } - public List preKeys(final IqPacket preKeys) { + public static List preKeys(final Iq preKeys) { List bundles = new ArrayList<>(); Map preKeyPublics = preKeyPublics(preKeys); if (preKeyPublics != null) { @@ -352,15 +352,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final boolean isGet = packet.getType() == IqPacket.TYPE.GET; - if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { + public void accept(final Iq packet) { + final boolean isGet = packet.getType() == Iq.Type.GET; + if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) { return; } if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { final Element query = packet.findChild("query"); // If this is in response to a query for the whole roster: - if (packet.getType() == IqPacket.TYPE.RESULT) { + if (packet.getType() == Iq.Type.RESULT) { account.getRoster().markAllAsNotInRoster(); } this.rosterItems(account, query); @@ -374,7 +374,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { (block != null ? block.getChildren() : null); // If this is a response to a blocklist query, clear the block list and replace with the new one. // Otherwise, just update the existing blocklist. - if (packet.getType() == IqPacket.TYPE.RESULT) { + if (packet.getType() == Iq.Type.RESULT) { account.clearBlocklist(); account.getXmppConnection().getFeatures().setBlockListRequested(true); } @@ -390,7 +390,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } account.getBlocklist().addAll(jids); - if (packet.getType() == IqPacket.TYPE.SET) { + if (packet.getType() == Iq.Type.SET) { boolean removed = false; for (Jid jid : jids) { removed |= mXmppConnectionService.removeBlockedConversations(account, jid); @@ -402,15 +402,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } // Update the UI mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - if (packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + if (packet.getType() == Iq.Type.SET) { + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } } else if (packet.hasChild("unblock", Namespace.BLOCKING) && - packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { + packet.fromServer(account) && packet.getType() == Iq.Type.SET) { Log.d(Config.LOGTAG, "Received unblock update from server"); final Collection items = packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.size() == 0) { + if (items.isEmpty()) { // No children to unblock == unblock all account.getBlocklist().clear(); } else { @@ -426,7 +426,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { account.getBlocklist().removeAll(jids); } mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") || packet.hasChild("data", "http://jabber.org/protocol/ibb") @@ -434,18 +434,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.getJingleConnectionManager() .deliverIbbPacket(account, packet); } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); + final Iq response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { - final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); + final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) { - final IqPacket response; + final Iq response; if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { - response = packet.generateResponse(IqPacket.TYPE.ERROR); + response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas"); @@ -453,18 +453,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); } mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) { + } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == Iq.Type.SET) { final Jid transport = packet.getFrom(); final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH); final boolean success = push != null && mXmppConnectionService.processUnifiedPushMessage( account, transport, push); - final IqPacket response; + final Iq response; if (success) { - response = packet.generateResponse(IqPacket.TYPE.RESULT); + response = packet.generateResponse(Iq.Type.RESULT); } else { - response = packet.generateResponse(IqPacket.TYPE.ERROR); + response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.setAttribute("code", "404"); @@ -476,8 +476,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { final Conversation conversation = mXmppConnectionService.find(account, packet.getFrom()); if (packet.hasChild("data", "urn:xmpp:bob") && isGet && (conversation == null ? contact != null && contact.canInferPresence() : conversation.canInferPresence())) { mXmppConnectionService.sendIqPacket(account, mXmppConnectionService.getIqGenerator().bobResponse(packet), null); - } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + } else if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) { + final var response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index b4a3168047..b931d4374a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import io.ipfs.cid.Cid; @@ -60,17 +61,20 @@ import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.carbons.Received; +import im.conversations.android.xmpp.model.carbons.Sent; +import im.conversations.android.xmpp.model.forward.Forwarded; -public class MessageParser extends AbstractParser implements OnMessagePacketReceived { +public class MessageParser extends AbstractParser implements Consumer { private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish"); - public MessageParser(XmppConnectionService service) { - super(service); + public MessageParser(final XmppConnectionService service, final Account account) { + super(service, account); } private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) { @@ -109,7 +113,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return result != null ? result : fallback; } - private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) { + private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) { ChatState state = ChatState.parse(packet); if (state != null && c != null) { final Account account = c.getAccount(); @@ -251,7 +255,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { Element item = items.findChild("item"); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + final Set deviceIds = IqParser.deviceIds(item); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... "); final AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); @@ -358,10 +362,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.updateAccountUi(); } - private boolean handleErrorMessage(final Account account, final MessagePacket packet) { - if (packet.getType() == MessagePacket.TYPE_ERROR) { + private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) { + if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) { if (packet.fromServer(account)) { - final Pair forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS); + final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS); if (forwarded != null) { return handleErrorMessage(account, forwarded.first); } @@ -404,11 +408,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } @Override - public void onMessagePacketReceived(Account account, MessagePacket original) { + public void accept(final im.conversations.android.xmpp.model.stanza.Message original) { if (handleErrorMessage(account, original)) { return; } - final MessagePacket packet; + final im.conversations.android.xmpp.model.stanza.Message packet; Long timestamp = null; boolean isCarbon = false; String serverMsgId = null; @@ -422,7 +426,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId); final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved(); if (query != null && query.validFrom(original.getFrom())) { - final Pair f = original.getForwardedMessagePacket("result", query.version.namespace); + final var f = getForwardedMessagePacket(original,"result", query.version.namespace); if (f == null) { return; } @@ -442,9 +446,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")"); return; } else if (original.fromServer(account)) { - Pair f; - f = original.getForwardedMessagePacket("received", Namespace.CARBONS); - f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f; + Pair f; + f = getForwardedMessagePacket(original, Received.class); + f = f == null ? getForwardedMessagePacket(original, Sent.class) : f; packet = f != null ? f.first : original; if (handleErrorMessage(account, packet)) { return; @@ -514,7 +518,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return; } - boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; + boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT; if (query != null && !query.muc() && isTypeGroupChat) { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping"); return; @@ -1300,6 +1304,34 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } + private static Pair getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class clazz) { + final var extension = original.getExtension(clazz); + final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class); + if (forwarded == null) { + return null; + } + final Long timestamp = AbstractParser.parseTimestamp(forwarded, null); + final var forwardedMessage = forwarded.getMessage(); + if (forwardedMessage == null) { + return null; + } + return new Pair<>(forwardedMessage,timestamp); + } + + private static Pair getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) { + final Element wrapper = original.findChild(name, namespace); + final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD); + if (forwardedElement instanceof Forwarded forwarded) { + final Long timestamp = AbstractParser.parseTimestamp(forwarded, null); + final var forwardedMessage = forwarded.getMessage(); + if (forwardedMessage == null) { + return null; + } + return new Pair<>(forwardedMessage,timestamp); + } + return null; + } + private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) { final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); if (conversation != null && (query == null || query.isCatchup())) { @@ -1312,7 +1344,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - private void processMessageReceipts(final Account account, final MessagePacket packet, final String remoteMsgId, MessageArchiveService.Query query) { + private void processMessageReceipts(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, MessageArchiveService.Query query) { final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); final boolean request = packet.hasChild("request", "urn:xmpp:receipts"); if (query == null) { @@ -1324,7 +1356,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece receiptsNamespaces.add("urn:xmpp:receipts"); } if (receiptsNamespaces.size() > 0) { - final MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, + final var receipt = mXmppConnectionService.getMessageGenerator().received(account, packet.getFrom(), remoteMsgId, receiptsNamespaces, diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 955cf9d1e1..a254dfe63e 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -19,22 +19,21 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import org.openintents.openpgp.util.OpenPgpUtils; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; -public class PresenceParser extends AbstractParser implements OnPresencePacketReceived { +public class PresenceParser extends AbstractParser implements Consumer { - public PresenceParser(XmppConnectionService service) { - super(service); + public PresenceParser(final XmppConnectionService service, final Account account) { + super(service, account); } - public void parseConferencePresence(PresencePacket packet, Account account) { + public void parseConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) { final Conversation conversation = packet.getFrom() == null ? null @@ -58,9 +57,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe } } - private void processConferencePresence(PresencePacket packet, Conversation conversation) { - - + private void processConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Conversation conversation) { final Account account = conversation.getAccount(); final MucOptions mucOptions = conversation.getMucOptions(); final Jid jid = conversation.getAccount().getJid(); @@ -300,7 +297,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe return codes; } - private void parseContactPresence(final PresencePacket packet, final Account account) { + private void parseContactPresence(final im.conversations.android.xmpp.model.stanza.Presence packet, final Account account) { final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); final Jid from = packet.getFrom(); if (from == null || from.equals(account.getJid())) { @@ -434,7 +431,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe } @Override - public void onPresencePacketReceived(Account account, PresencePacket packet) { + public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 3a600b3473..1247a2c838 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -53,6 +53,21 @@ import com.madebyevan.thumbhash.ThumbHash; import com.wolt.blurhashkt.BlurHashDecoder; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AttachFileToConversationRunnable; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.adapter.MediaAdapter; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.FileUtils; +import eu.siacs.conversations.utils.FileWriterException; +import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.xmpp.pep.Avatar; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -85,22 +100,6 @@ import org.tomlj.TomlTable; import io.ipfs.cid.Cid; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AttachFileToConversationRunnable; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.FileUtils; -import eu.siacs.conversations.utils.FileWriterException; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xml.Element; - public class FileBackend { private static final Object THUMBNAIL_LOCK = new Object(); @@ -784,16 +783,16 @@ public class FileBackend { } private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d( - Config.LOGTAG, - "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); - file.getParentFile().mkdirs(); + final var parentDirectory = file.getParentFile(); + if (parentDirectory != null && parentDirectory.mkdirs()) { + Log.d(Config.LOGTAG,"created directory "+parentDirectory.getAbsolutePath()); + } try { if (!file.createNewFile() && file.length() > 0) { if (file.canRead() && file.getName().startsWith("zb2")) return; // We have this content already throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } - } catch (IOException e) { + } catch (final IOException e) { throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } try (final OutputStream os = new FileOutputStream(file); @@ -803,12 +802,12 @@ public class FileBackend { } try { ByteStreams.copy(is, os); - } catch (IOException e) { + } catch (final IOException e) { throw new FileWriterException(file); } try { os.flush(); - } catch (IOException e) { + } catch (final IOException e) { throw new FileWriterException(file); } } catch (final FileNotFoundException e) { @@ -817,7 +816,7 @@ public class FileBackend { } catch (final FileWriterException e) { cleanup(file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } catch (final SecurityException | IllegalStateException e) { + } catch (final SecurityException | IllegalStateException | IllegalArgumentException e) { cleanup(file); throw new FileCopyException(R.string.error_security_exception); } catch (final IOException e) { @@ -828,7 +827,7 @@ public class FileBackend { public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException { - String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); + final String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); String extension = MimeUtils.guessExtensionFromMimeType(mime); if (extension == null) { @@ -1168,9 +1167,9 @@ public class FileBackend { } public BitmapDrawable getFallbackThumbnail(final Message message, int size, boolean cacheOnly) { - List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; if (thumbs != null && !thumbs.isEmpty()) { - for (Element thumb : thumbs) { + for (final var thumb : thumbs) { final var uriS = thumb.getAttribute("uri"); if (uriS == null) continue; Uri uri = Uri.parse(uriS); @@ -1240,9 +1239,9 @@ public class FileBackend { if ((thumbnail == null) && (!cacheOnly)) { synchronized (THUMBNAIL_LOCK) { - List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; if (thumbs != null && !thumbs.isEmpty()) { - for (Element thumb : thumbs) { + for (final var thumb : thumbs) { final var uriS = thumb.getAttribute("uri"); if (uriS == null) continue; Uri uri = Uri.parse(uriS); diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 4044891495..5b23d1533f 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -42,8 +42,12 @@ public class CallIntegration extends Connection { * *

Samsung Galaxy Tab A claims to have FEATURE_CONNECTION_SERVICE but then throws * SecurityException when invoking placeCall(). Both Stock and LineageOS have this problem. + * + *

Lenovo Yoga Smart Tab YT-X705F claims to have FEATURE_CONNECTION_SERVICE but throws + * SecurityException */ - private static final List BROKEN_DEVICE_MODELS = Arrays.asList("OnePlus6", "gtaxlwifi"); + private static final List BROKEN_DEVICE_MODELS = + Arrays.asList("OnePlus6", "gtaxlwifi", "YT-X705F"); public static final int DEFAULT_TONE_VOLUME = 60; private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90; @@ -393,9 +397,7 @@ public class CallIntegration extends Connection { public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - final var toneGenerator = - new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); } @@ -410,9 +412,7 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); - final var toneGenerator = - new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); } @@ -429,8 +429,7 @@ public class CallIntegration extends Connection { public void busy() { Log.d(Config.LOGTAG, "CallIntegration.busy()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); + startTone(80, ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500); } @@ -458,6 +457,17 @@ public class CallIntegration extends Connection { Log.d(Config.LOGTAG, "destroyed!"); } + private void startTone(final int volume, final int toneType, final int durationMs) { + final ToneGenerator toneGenerator; + try { + toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, volume); + } catch (final RuntimeException e) { + Log.e(Config.LOGTAG, "could not initialize tone generator", e); + return; + } + toneGenerator.startTone(toneType, durationMs); + } + public static Uri address(final Jid contact) { return Uri.parse(String.format("xmpp:%s", contact.toEscapedString())); } @@ -532,6 +542,12 @@ public class CallIntegration extends Connection { && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { return false; } + // we are relatively sure that old Oppo devices are broken too. We get reports of 'number + // not sent' from Oppo R15x (Android 10) + if ("OPPO".equalsIgnoreCase(Build.MANUFACTURER) + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + return false; + } // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being // routed properly) However with those devices being extremely rare it's impossible to gauge // how many might be effected and no Naomi Wu around to clarify with the company directly diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 49169b7d29..623653fbc2 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -36,6 +36,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -126,9 +127,17 @@ public class CallIntegrationConnectionService extends ConnectionService { // actually attempted // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR); } else { - final var proposal = - service.getJingleConnectionManager() - .proposeJingleRtpSession(account, with, media); + final JingleConnectionManager.RtpSessionProposal proposal; + try { + proposal = + service.getJingleConnectionManager() + .proposeJingleRtpSession(account, with, media); + } catch (final IllegalStateException e) { + return Connection.createFailedConnection( + new DisconnectCause( + DisconnectCause.ERROR, + "Phone is busy. Probably race condition. Try again in a moment")); + } if (proposal == null) { // TODO instead of just null checking try to get the sessionID return Connection.createFailedConnection( diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 0a027da2d2..e2a828ac63 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.services; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; -import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -12,17 +10,15 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.utils.TLSSocketFactory; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; @@ -34,10 +30,6 @@ import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -47,9 +39,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - public class ChannelDiscoveryService { private final XmppConnectionService service; @@ -68,25 +57,7 @@ public class ChannelDiscoveryService { this.muclumbusService = null; return; } - final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); - try { - final X509TrustManager trustManager; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - trustManager = TrustManagers.defaultWithBundledLetsEncrypt(service); - } else { - trustManager = TrustManagers.createDefaultTrustManager(); - } - final SSLSocketFactory socketFactory = - new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); - builder.sslSocketFactory(socketFactory, trustManager); - } catch (final IOException - | KeyManagementException - | NoSuchAlgorithmException - | KeyStoreException - | CertificateException e) { - Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); - throw new RuntimeException(e); - } + final OkHttpClient.Builder builder = HttpConnectionManager.okHttpClient(service).newBuilder(); if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } @@ -203,7 +174,7 @@ public class ChannelDiscoveryService { final String query, Map mucServices, final OnChannelSearchResultsFound listener) { final Map localMucService = mucServices == null ? getLocalMucServices() : mucServices; Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); - if (localMucService.size() == 0) { + if (localMucService.isEmpty()) { listener.onChannelSearchResultsFound(Collections.emptyList()); return; } @@ -217,38 +188,37 @@ public class ChannelDiscoveryService { } final AtomicInteger queriesInFlight = new AtomicInteger(); final List rooms = new ArrayList<>(); - for (Map.Entry entry : localMucService.entrySet()) { - IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); + for (final Map.Entry entry : localMucService.entrySet()) { + Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); queriesInFlight.incrementAndGet(); + final var account = entry.getValue(); service.sendIqPacket( - entry.getValue(), + account, itemsRequest, - (account, itemsResponse) -> { - if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { + (itemsResponse) -> { + if (itemsResponse.getType() == Iq.Type.RESULT) { final List items = IqParser.items(itemsResponse); - for (Jid item : items) { + for (final Jid item : items) { if (item.isDomainJid()) continue; // Only looking for MUCs for now, and by spec they have a localpart - IqPacket infoRequest = + final Iq infoRequest = service.getIqGenerator().queryDiscoInfo(item); queriesInFlight.incrementAndGet(); service.sendIqPacket( account, infoRequest, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived( - Account account, IqPacket infoResponse) { - if (infoResponse.getType() - == IqPacket.TYPE.RESULT) { - final Room room = - IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } + infoResponse -> { + if (infoResponse.getType() + == Iq.Type.RESULT) { + final Room room = + IqParser.parseRoom(infoResponse); + if (room != null) { + rooms.add(room); } if (queriesInFlight.decrementAndGet() <= 0) { finishDiscoSearch(rooms, query, mucServices, listener); } + } else { + queriesInFlight.decrementAndGet(); } }, 20L); } diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 3569142308..c9be19551e 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -41,7 +41,6 @@ import android.util.Log; import android.util.SparseArray; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.util.Consumer; import com.google.common.base.Charsets; import com.google.common.base.Joiner; @@ -86,6 +85,7 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Locale; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index dad829baf6..559f49ef91 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -23,8 +23,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Message; public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { @@ -81,7 +81,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return false; } - public static Element findResult(MessagePacket packet) { + public static Element findResult(Message packet) { for (Version version : values()) { Element result = packet.findChild("result", version.namespace); if (result != null) { @@ -233,17 +233,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { throw new IllegalStateException("Attempted to run MAM query for archived conversation"); } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); - final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); - this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { + final Iq packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, (p) -> { final Element fin = p.findChild("fin", query.version.namespace); - if (p.getType() == IqPacket.TYPE.TIMEOUT) { + if (p.getType() == Iq.Type.TIMEOUT) { synchronized (this.queries) { this.queries.remove(query); if (query.hasCallback()) { query.callback(false); } } - } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { + } else if (p.getType() == Iq.Type.RESULT && fin != null) { final boolean running; synchronized (this.queries) { running = this.queries.contains(query); @@ -253,10 +253,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring MAM iq result because query had been killed"); } - } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { + } else if (p.getType() == Iq.Type.RESULT && query.isLegacy()) { //do nothing } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); try { finalizeQuery(query, true); } catch (final IllegalStateException e) { @@ -303,7 +303,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - boolean inCatchup(Account account) { + public boolean inCatchup(Account account) { synchronized (this.queries) { for (Query query : queries) { if (query.account == account && query.isCatchup() && query.getWith() == null) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 14c3b10402..4a5768f94e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -204,9 +204,7 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import im.conversations.android.xmpp.model.stanza.Iq; import me.leolin.shortcutbadger.ShortcutBadger; import okhttp3.HttpUrl; @@ -255,12 +253,12 @@ public class XmppConnectionService extends Service { private final Set mInProgressAvatarFetches = new HashSet<>(); private final Set mOmittedPepAvatarFetches = new HashSet<>(); private final HashSet mLowPingTimeoutMode = new HashSet<>(); - private final OnIqPacketReceived mDefaultIqHandler = (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - Element error = packet.findChild("error"); + 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, account.getJid().asBareJid() + ": received iq error - " + text); + Log.d(Config.LOGTAG, "received iq error: " + text); } } }; @@ -272,6 +270,7 @@ public class XmppConnectionService extends Service { private long mLastMucPing = 0; private Map mScheduledMessages = new HashMap<>(); private long mLastStickerRescan = 0; + private final AppSettings appSettings = new AppSettings(this); private final FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; private final NotificationService mNotificationService = new NotificationService(this); @@ -282,9 +281,6 @@ public class XmppConnectionService extends Service { private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false); private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); private final AtomicReference ongoingCall = new AtomicReference<>(); - private final OnMessagePacketReceived mMessageParser = new MessageParser(this); - private final OnPresencePacketReceived mPresenceParser = new PresenceParser(this); - private final IqParser mIqParser = new IqParser(this); private final MessageGenerator mMessageGenerator = new MessageGenerator(this); public OnContactStatusChanged onContactStatusChanged = (contact, online) -> { Conversation conversation = find(getConversations(), contact); @@ -347,7 +343,7 @@ public class XmppConnectionService extends Service { private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false); public void setDiallerIntegrationActive(boolean active) { - diallerIntegrationActive.set(active); + diallerIntegrationActive.set(active); } private boolean destroyed = false; @@ -371,79 +367,6 @@ public class XmppConnectionService extends Service { public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); - private final OnBindListener mOnBindListener = new OnBindListener() { - - @Override - public void onBind(final Account account) { - synchronized (mInProgressAvatarFetches) { - for (Iterator iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) { - final String KEY = iterator.next(); - if (KEY.startsWith(account.getJid().asBareJid() + "_")) { - iterator.remove(); - } - } - } - boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); - boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0)); - if (loggedInSuccessfully || gainedFeature) { - databaseBackend.updateAccount(account); - } - - if (loggedInSuccessfully) { - if (!TextUtils.isEmpty(account.getDisplayName())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); - publishDisplayName(account); - } - } - - account.getRoster().clearPresences(); - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.clear(); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.clear(); - } - mJingleConnectionManager.notifyRebound(account); - mQuickConversationsService.considerSyncBackground(false); - fetchRosterFromServer(account); - - final XmppConnection connection = account.getXmppConnection(); - - if (connection.getFeatures().bookmarks2()) { - fetchBookmarks2(account); - } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { - fetchBookmarks(account); - } - - if (connection.getFeatures().mds()) { - fetchMessageDisplayedSynchronization(account); - } else { - Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); - } - final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); - final boolean catchup = getMessageArchiveService().inCatchup(account); - final boolean trackOfflineMessageRetrieval; - if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) { - trackOfflineMessageRetrieval = false; - sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages"); - } - }); - } else { - trackOfflineMessageRetrieval = true; - } - sendPresence(account); - account.getXmppConnection().trackOfflineMessageRetrieval(trackOfflineMessageRetrieval); - if (mPushManagementService.available(account)) { - mPushManagementService.registerPushTokenOnServer(account); - } - connectMultiModeConversations(account); - syncDirtyContacts(account); - - unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); - } - }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); @@ -609,6 +532,10 @@ public class XmppConnectionService extends Service { } } + public AppSettings getAppSettings() { + return this.appSettings; + } + public FileBackend getFileBackend() { return this.fileBackend; } @@ -1133,14 +1060,14 @@ public class XmppConnectionService extends Service { boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 - && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); + && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); final HashSet pingCandidates = new HashSet<>(); final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this); for (final Account account : accounts) { final boolean pushWasMeantForThisAccount = androidId != null && CryptoHelper.getAccountFingerprint(account, androidId) - .equals(pushedAccountHash); + .equals(pushedAccountHash); pingNow |= processAccountState( account, @@ -1313,7 +1240,7 @@ public class XmppConnectionService extends Service { final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class); return !Compatibility.isActiveNetworkMetered(connectivityManager) || Compatibility.getRestrictBackgroundStatus(connectivityManager) - == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; + == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) { @@ -1513,13 +1440,11 @@ public class XmppConnectionService extends Service { toggleForegroundService(); this.destroyed = false; OmemoSetting.load(this); - ExceptionHelper.init(getApplicationContext()); try { Security.insertProviderAt(Conscrypt.newProvider(), 1); } catch (Throwable throwable) { Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } - Resolver.init(this); updateMemorizingTrustManager(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 15; @@ -1563,7 +1488,7 @@ public class XmppConnectionService extends Service { if (QuickConversationsService.isContactListIntegration(this) && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) - == PackageManager.PERMISSION_GRANTED) { + == PackageManager.PERMISSION_GRANTED) { startContactObserver(); } FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); @@ -1786,7 +1711,7 @@ public class XmppConnectionService extends Service { final int foregroundServiceType; if (requireMicrophone && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) - == PackageManager.PERMISSION_GRANTED) { + == PackageManager.PERMISSION_GRANTED) { foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; Log.d(Config.LOGTAG, "defaulting to microphone foreground service type"); } else if (getSystemService(PowerManager.class) @@ -1911,12 +1836,8 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); - connection.setOnMessagePacketReceivedListener(this.mMessageParser); connection.setOnStatusChangedListener(this.statusListener); - connection.setOnPresencePacketReceivedListener(this.mPresenceParser); - connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); - connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); @@ -1929,7 +1850,7 @@ public class XmppConnectionService extends Service { public void sendChatState(Conversation conversation) { if (sendChatStates()) { - MessagePacket packet = mMessageGenerator.generateChatState(conversation); + final var packet = mMessageGenerator.generateChatState(conversation); sendMessagePacket(conversation.getAccount(), packet); } } @@ -1967,7 +1888,7 @@ public class XmppConnectionService extends Service { } } - MessagePacket packet = null; + im.conversations.android.xmpp.model.stanza.Message packet = null; final boolean addToConversation = !message.edited() && message.getRawBody() != null; boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); @@ -2020,8 +1941,8 @@ public class XmppConnectionService extends Service { // Part of the real body, not just a fallback Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB); fallback.addChild("body", "urn:xmpp:fallback:0") - .setAttribute("start", "0") - .setAttribute("end", "0"); + .setAttribute("start", "0") + .setAttribute("end", "0"); message.addPayload(fallback); } @@ -2067,9 +1988,9 @@ public class XmppConnectionService extends Service { waiter.release(); } }) - .showNullOnEmpty(true) - .maxBodySize(90000) - .timeout(5000); + .showNullOnEmpty(true) + .maxBodySize(90000) + .timeout(5000); if (useTorToConnect()) { openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118)); } @@ -2287,13 +2208,13 @@ public class XmppConnectionService extends Service { callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites)); return; } - final IqPacket request = new IqPacket(IqPacket.TYPE.SET); + final Iq request = new Iq(Iq.Type.SET); request.setTo(jid); final Element command = request.addChild("command", Namespace.COMMANDS); command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE); command.setAttribute("action", "execute"); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element resultCommand = response.findChild("command", Namespace.COMMANDS); final Element x = resultCommand == null ? null : resultCommand.findChild("x", Namespace.DATA); if (x != null) { @@ -2308,7 +2229,7 @@ public class XmppConnectionService extends Service { } callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite)); Log.d(Config.LOGTAG, response.toString()); - } else if (response.getType() == IqPacket.TYPE.ERROR) { + } else if (response.getType() == Iq.Type.ERROR) { callback.inviteRequestFailed(IqParser.errorMessage(response)); } else { callback.inviteRequestFailed(getString(R.string.remote_server_timeout)); @@ -2317,54 +2238,42 @@ public class XmppConnectionService extends Service { } - public void fetchRosterFromServer(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - if (!"".equals(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": fetching roster version " + account.getRosterVersion()); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster"); - } - iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion()); - sendIqPacket(account, iqPacket, mIqParser); - } - public void fetchBookmarks(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); + final Iq iqPacket = new Iq(Iq.Type.GET); final Element query = iqPacket.query("jabber:iq:private"); query.addChild("storage", Namespace.BOOKMARKS); - final OnIqPacketReceived callback = (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Consumer callback = (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query1 = response.query(); final Element storage = query1.findChild("storage", "storage:bookmarks"); Map bookmarks = Bookmark.parseFromStorage(storage, account); - processBookmarksInitial(a, bookmarks, false); + processBookmarksInitial(account, bookmarks, false); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fetch bookmarks"); } }; sendIqPacket(account, iqPacket, callback); } public void fetchBookmarks2(final Account account) { - final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket(account, retrieve, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq retrieve = mIqGenerator.retrieveBookmarks(); + sendIqPacket(account, retrieve, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); - final Map bookmarks = Bookmark.parseFromPubsub(pubsub, a); - processBookmarksInitial(a, bookmarks, true); + final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); + processBookmarksInitial(account, bookmarks, true); } }); } - private void fetchMessageDisplayedSynchronization(final Account account) { + public void fetchMessageDisplayedSynchronization(final Account account) { Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); final var retrieve = mIqGenerator.retrieveMds(); sendIqPacket( account, retrieve, - (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() != Iq.Type.RESULT) { return; } final var pubSub = response.findChild("pubsub", Namespace.PUBSUB); @@ -2514,7 +2423,7 @@ public class XmppConnectionService extends Service { } public void deleteBookmark(final Account account, final Bookmark bookmark) { - if (bookmark.getJid().toString().equals("support@conference.monocles.eu")) { + if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) { getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply(); } account.removeBookmark(bookmark); @@ -2522,11 +2431,11 @@ public class XmppConnectionService extends Service { if (connection == null) return; if (connection.getFeatures().bookmarks2()) { - final IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); + final Iq request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2"); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.ERROR) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); } }); } else if (connection.getFeatures().bookmarksConversion()) { @@ -2540,7 +2449,7 @@ public class XmppConnectionService extends Service { if (!account.areBookmarksLoaded()) return; Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); - IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); + final Iq iqPacket = new Iq(Iq.Type.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); for (final Bookmark bookmark : account.getBookmarks()) { @@ -2567,9 +2476,9 @@ public class XmppConnectionService extends Service { } private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) { - final IqPacket packet = mIqGenerator.publishElement(node, element, id, options); - sendIqPacket(account, packet, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq packet = mIqGenerator.publishElement(node, element, id, options); + sendIqPacket(account, packet, (response) -> { + if (response.getType() == Iq.Type.RESULT) { return; } if (retry && PublishOptions.preconditionNotMet(response)) { @@ -2884,11 +2793,11 @@ public class XmppConnectionService extends Service { public void maybeRegisterWithMuc(Conversation c, String nickArg) { final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg; - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final var register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER); register.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), register, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), register, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query = response.query(Namespace.REGISTER); String username = query.findChildContent("username", Namespace.REGISTER); if (username == null) username = query.findChildContent("nick", Namespace.REGISTER); @@ -2913,11 +2822,11 @@ public class XmppConnectionService extends Service { } form.put("muc#register_roomnick", nick); form.submit(); - final IqPacket finish = new IqPacket(IqPacket.TYPE.SET); + final var finish = new Iq(Iq.Type.SET); finish.query(Namespace.REGISTER).addChild(form); finish.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), finish, (a2, response2) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), finish, (response2) -> { + if (response.getType() == Iq.Type.RESULT) { Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick); } else { Log.w(Config.LOGTAG, "Error registering with channel: " + response2); @@ -2935,11 +2844,11 @@ public class XmppConnectionService extends Service { } public void deregisterWithMuc(Conversation c) { - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final Iq register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER).addChild("remove"); register.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), register, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), register, (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid()); } else { Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response); @@ -3119,6 +3028,10 @@ public class XmppConnectionService extends Service { return this.unifiedPushBroker.renewUnifiedPushEndpoints(null); } + public UnifiedPushBroker getUnifiedPushBroker() { + return this.unifiedPushBroker; + } + private void provisionAccount(final String address, final String password) { final Jid jid = Jid.ofEscaped(address); final Account account = new Account(jid, password); @@ -3223,12 +3136,12 @@ public class XmppConnectionService extends Service { } public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { - final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket(account, iq, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - a.setPassword(newPassword); - a.setOption(Account.OPTION_MAGIC_CREATE, false); - databaseBackend.updateAccount(a); + final Iq iq = getIqGenerator().generateSetPassword(account, newPassword); + sendIqPacket(account, iq, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + account.setPassword(newPassword); + account.setOption(Account.OPTION_MAGIC_CREATE, false); + databaseBackend.updateAccount(account); callback.onPasswordChangeSucceeded(); } else { callback.onPasswordChangeFailed(); @@ -3237,12 +3150,12 @@ public class XmppConnectionService extends Service { } public void unregisterAccount(final Account account, final Consumer callback) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); + final Iq iqPacket = new Iq(Iq.Type.SET); final Element query = iqPacket.addChild("query",Namespace.REGISTER); query.addChild("remove"); - sendIqPacket(account, iqPacket, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - deleteAccount(a); + sendIqPacket(account, iqPacket, (response) -> { + if (response.getType() == Iq.Type.RESULT) { + deleteAccount(account); callback.accept(true); } else { callback.accept(false); @@ -3576,7 +3489,7 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "app switched into background"); } - private void connectMultiModeConversations(Account account) { + public void connectMultiModeConversations(Account account) { List conversations = getConversations(); for (Conversation conversation : conversations) { if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { @@ -3600,20 +3513,20 @@ public class XmppConnectionService extends Service { } } final Jid self = conversation.getMucOptions().getSelf().getFullJid(); - final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); + final Iq ping = new Iq(Iq.Type.GET); ping.setTo(self); ping.addChild("ping", Namespace.PING); - sendIqPacket(conversation.getAccount(), ping, (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Element error = response.findChild("error"); + sendIqPacket(conversation.getAccount(), ping, (response) -> { + if (response.getType() == Iq.Type.ERROR) { + final var error = response.getError(); if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); joinMuc(conversation); } - } else if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back fine"); + } else if (response.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back fine"); } synchronized (account.inProgressConferencePings) { account.inProgressConferencePings.remove(conversation); @@ -3672,7 +3585,7 @@ public class XmppConnectionService extends Service { final Jid joinJid = mucOptions.getSelf().getFullJid(); Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); - PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick()); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick()); packet.setTo(joinJid); Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); if (conversation.getMucOptions().getPassword() != null) { @@ -3764,16 +3677,16 @@ public class XmppConnectionService extends Service { final var affiliations = new ArrayList(); affiliations.add("outcast"); if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner")); - OnIqPacketReceived callback = new OnIqPacketReceived() { + final Consumer callback = new Consumer() { private int i = 0; private boolean success = true; @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + public void accept(Iq response) { final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { + Element query = response.query("http://jabber.org/protocol/muc#admin"); + if (response.getType() == Iq.Type.RESULT && query != null) { for (Element child : query.getChildren()) { if ("item".equals(child.getName())) { MucOptions.User user = AbstractParser.parseItem(conversation, child); @@ -3861,29 +3774,29 @@ public class XmppConnectionService extends Service { } private void deletePepNode(final Account account, final String node, final Runnable runnable) { - final IqPacket request = mIqGenerator.deleteNode(node); - sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node); + final Iq request = mIqGenerator.deleteNode(node); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully deleted pep node "+node); if (runnable != null) { runnable.run(); } } else { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to delete "+ packet); } }); } private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { - final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); - sendIqPacket(account, retrieveVcard, (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); + sendIqPacket(account, retrieveVcard, (response) -> { + if (response.getType() != Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": no vCard set. nothing to do"); return; } final Element vcard = response.findChild("vCard", "vcard-temp"); if (vcard == null) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": no vCard set. nothing to do"); return; } Element photo = vcard.findChild("PHOTO"); @@ -3891,12 +3804,12 @@ public class XmppConnectionService extends Service { photo = vcard.addChild("PHOTO"); } photo.clearChildren(); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); - publication.setTo(a.getJid().asBareJid()); + final Iq publication = new Iq(Iq.Type.SET); + publication.setTo(account.getJid().asBareJid()); publication.addChild(vcard); - sendIqPacket(account, publication, (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar"); + sendIqPacket(account, publication, (publicationResponse) -> { + if (publicationResponse.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully deleted vcard avatar"); runnable.run(); } else { Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); @@ -3961,7 +3874,7 @@ public class XmppConnectionService extends Service { if (options.online()) { Account account = conversation.getAccount(); final Jid joinJid = options.getSelf().getFullJid(); - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick()); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick()); packet.setTo(joinJid); sendPresencePacket(account, packet); } @@ -3981,7 +3894,7 @@ public class XmppConnectionService extends Service { @Override public void onSuccess() { - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); packet.setTo(joinJid); sendPresencePacket(account, packet); callback.success(conversation); @@ -3993,7 +3906,7 @@ public class XmppConnectionService extends Service { } }); - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); packet.setTo(joinJid); sendPresencePacket(account, packet); } else { @@ -4157,12 +4070,12 @@ public class XmppConnectionService extends Service { return; } - IqPacket request = mIqGenerator.queryDiscoInfo(jid.asBareJid()); - sendIqPacket(account, request, (acct, reply) -> { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(reply); + final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid()); + sendIqPacket(account, request, (reply) -> { + final var result = new ServiceDiscoveryResult(reply); cb.accept( - result.getFeatures().contains("http://jabber.org/protocol/muc") && - result.hasIdentity("conference", null) + result.getFeatures().contains("http://jabber.org/protocol/muc") && + result.hasIdentity("conference", null) ); }); } @@ -4172,39 +4085,37 @@ public class XmppConnectionService extends Service { } public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { - IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final MucOptions mucOptions = conversation.getMucOptions(); - final Bookmark bookmark = conversation.getBookmark(); - final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); + final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); + final var account = conversation.getAccount(); + sendIqPacket(account, request, response -> { + if (response.getType() == Iq.Type.RESULT) { + final MucOptions mucOptions = conversation.getMucOptions(); + final Bookmark bookmark = conversation.getBookmark(); + final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); - if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); - updateConversation(conversation); - } - - if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { - if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { - createBookmark(account, bookmark); - } + if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); + updateConversation(conversation); + } + + if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { + if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { + createBookmark(account, bookmark); } + } - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } + if (callback != null) { + callback.onConferenceConfigurationFetched(conversation); + } - updateConversationUi(); - } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); - } else { - if (callback != null) { - callback.onFetchFailed(conversation, packet.getErrorCondition()); - } + updateConversationUi(); + } else if (response.getType() == Iq.Type.TIMEOUT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); + } else { + if (callback != null) { + callback.onFetchFailed(conversation, response.getErrorCondition()); } } }); @@ -4216,33 +4127,27 @@ public class XmppConnectionService extends Service { public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) { Log.d(Config.LOGTAG, "pushing node configuration"); - sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configuration = pubsub == null ? null : pubsub.findChild("configure"); - Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); - if (x != null) { - Data data = Data.parse(x); - data.submit(options); - sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node); - callback.onPushSucceeded(); - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { - callback.onPushFailed(); - } - } - }); - } else if (callback != null) { - callback.onPushFailed(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { + sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), responseToRequest -> { + if (responseToRequest.getType() == Iq.Type.RESULT) { + Element pubsub = responseToRequest.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); + Element configuration = pubsub == null ? null : pubsub.findChild("configure"); + Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); + if (x != null) { + final Data data = Data.parse(x); + data.submit(options); + sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), responseToPublish -> { + if (responseToPublish.getType() == Iq.Type.RESULT && callback != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node); + callback.onPushSucceeded(); + } else if (responseToPublish.getType() == Iq.Type.ERROR && callback != null) { + callback.onPushFailed(); + } + }); + } else if (callback != null) { callback.onPushFailed(); } + } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) { + callback.onPushFailed(); } }); } @@ -4256,54 +4161,56 @@ public class XmppConnectionService extends Service { final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom")); options.putString("members_by_default", moderated ? "0" : "1"); } - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + if (options.containsKey("muc#roomconfig_allowpm")) { + // ejabberd :-/ + final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm")); + options.putString("allow_private_messages", allow ? "1" : "0"); + options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody"); + } + final var account = conversation.getAccount(); + final Iq request = new Iq(Iq.Type.GET); request.setTo(conversation.getJid().asBareJid()); request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Data data = Data.parse(packet.query().findChild("x", Namespace.DATA)); - data.submit(options); - final IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.setTo(conversation.getJid().asBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket(account, set, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (callback != null) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onPushSucceeded(); - } else { - callback.onPushFailed(); - } - } - } - }); - } else { + sendIqPacket(account, request, response -> { + if (response.getType() == Iq.Type.RESULT) { + final Data data = Data.parse(response.query().findChild("x", Namespace.DATA)); + data.submit(options); + final Iq set = new Iq(Iq.Type.SET); + set.setTo(conversation.getJid().asBareJid()); + set.query("http://jabber.org/protocol/muc#owner").addChild(data); + sendIqPacket(account, set, packet -> { if (callback != null) { - callback.onPushFailed(); + if (packet.getType() == Iq.Type.RESULT) { + callback.onPushSucceeded(); + } else { + Log.d(Config.LOGTAG,"failed: "+packet.toString()); + callback.onPushFailed(); + } } + }); + } else { + if (callback != null) { + callback.onPushFailed(); } } }); } public void pushSubjectToConference(final Conversation conference, final String subject) { - MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); + final var packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); this.sendMessagePacket(conference.getAccount(), packet); } public void requestVoice(final Account account, final Jid jid) { - MessagePacket packet = this.getMessageGenerator().requestVoice(jid); + final var packet = this.getMessageGenerator().requestVoice(jid); this.sendMessagePacket(account, packet); } public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { final Jid jid = user.asBareJid(); - final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { conference.getMucOptions().changeAffiliation(jid, affiliation); getAvatarService().clear(conference); if (callback != null) { @@ -4320,39 +4227,37 @@ public class XmppConnectionService extends Service { } public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { - IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - sendIqPacket(conference.getAccount(), request, (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { + final var account =conference.getAccount(); + final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString()); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() != Iq.Type.RESULT) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick); } }); } public void moderateMessage(final Account account, final Message m, final String reason) { - IqPacket request = this.mIqGenerator.moderateMessage(account, m, reason); - sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { + final var request = this.mIqGenerator.moderateMessage(account, m, reason); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() != Iq.Type.RESULT) { showErrorToastInUi(R.string.unable_to_moderate); - Log.d(Config.LOGTAG, a.getJid().asBareJid() + " unable to moderate: " + packet); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet); } }); } public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) { - IqPacket request = new IqPacket(IqPacket.TYPE.SET); + final Iq request = new Iq(Iq.Type.SET); request.setTo(conversation.getJid().asBareJid()); request.query("http://jabber.org/protocol/muc#owner").addChild("destroy"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { - callback.onRoomDestroySucceeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (callback != null) { - callback.onRoomDestroyFailed(); - } + sendIqPacket(conversation.getAccount(), request, response -> { + if (response.getType() == Iq.Type.RESULT) { + if (callback != null) { + callback.onRoomDestroySucceeded(); + } + } else if (response.getType() == Iq.Type.ERROR) { + if (callback != null) { + callback.onRoomDestroyFailed(); } } }); @@ -4409,7 +4314,7 @@ public class XmppConnectionService extends Service { updateConversationUi(); } - protected void syncDirtyContacts(Account account) { + public void syncDirtyContacts(Account account) { for (Contact contact : account.getRoster().getContacts()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { pushContactToServer(contact); @@ -4453,7 +4358,7 @@ public class XmppConnectionService extends Service { final boolean sendUpdates = contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); iq.query(Namespace.ROSTER).addChild(contact.asElement()); account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); if (sendUpdates) { @@ -4505,10 +4410,11 @@ public class XmppConnectionService extends Service { } private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { - final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar); - sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> { - boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); - if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) { + final var account = conversation.getAccount(); + final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar); + sendIqPacket(account, retrieve, (response) -> { + boolean itemNotFound = response.getType() == Iq.Type.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); + if (response.getType() == Iq.Type.RESULT || itemNotFound) { Element vcard = response.findChild("vCard", "vcard-temp"); if (vcard == null) { vcard = new Element("vCard", "vcard-temp"); @@ -4520,11 +4426,11 @@ public class XmppConnectionService extends Service { photo.clearChildren(); photo.addChild("TYPE").setContent(avatar.type); photo.addChild("BINVAL").setContent(avatar.image); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); + final Iq publication = new Iq(Iq.Type.SET); publication.setTo(conversation.getJid().asBareJid()); publication.addChild(vcard); - sendIqPacket(account, publication, (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, publication, (publicationResponse) -> { + if (publicationResponse.getType() == Iq.Type.RESULT) { callback.onAvatarPublicationSucceeded(); } else { Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); @@ -4550,71 +4456,64 @@ public class XmppConnectionService extends Service { public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": publishing avatar. options=" + options); - IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - publishAvatarMetadata(account, avatar, options, true, callback); - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); - publishAvatar(account, avatar, options, false, callback); - } - - @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); + 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 IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); - sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - if (account.setAvatar(avatar.getFilename())) { - getAvatarService().clear(account); - databaseBackend.updateAccount(account); - notifyAccountAvatarHasChanged(account); + 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); } - 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); + @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); } } }); @@ -4625,10 +4524,10 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); return; } - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { + final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new Consumer() { - private Avatar parseAvatar(IqPacket packet) { + private Avatar parseAvatar(Iq packet) { Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); if (pubsub != null) { Element items = pubsub.findChild("items"); @@ -4639,16 +4538,16 @@ public class XmppConnectionService extends Service { return null; } - private boolean errorIsItemNotFound(IqPacket packet) { + private boolean errorIsItemNotFound(Iq packet) { Element error = packet.findChild("error"); - return packet.getType() == IqPacket.TYPE.ERROR + return packet.getType() == Iq.Type.ERROR && error != null && error.hasChild("item-not-found"); } @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) { + public void accept(final Iq packet) { + if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) { Avatar serverAvatar = parseAvatar(packet); if (serverAvatar == null && account.getAvatar() != null) { Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); @@ -4664,6 +4563,17 @@ public class XmppConnectionService extends Service { }); } + 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 fetchAvatar(Account account, Avatar avatar) { fetchAvatar(account, avatar, null); } @@ -4695,26 +4605,26 @@ public class XmppConnectionService extends Service { } } - private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket(account, packet, (a, result) -> { + 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(a, avatar)); + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); } - final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; - if (result.getType() == IqPacket.TYPE.RESULT) { - avatar.image = mIqParser.avatarData(result); + final String ERROR = account.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == Iq.Type.RESULT) { + avatar.image = IqParser.avatarData(result); if (avatar.image != null) { if (getFileBackend().save(avatar)) { - if (a.getJid().asBareJid().equals(avatar.owner)) { - if (a.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(a); + if (account.getJid().asBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); } - getAvatarService().clear(a); + getAvatarService().clear(account); updateConversationUi(); updateAccountUi(); } else { - final Contact contact = a.getRoster().getContact(avatar.owner); + final Contact contact = account.getRoster().getContact(avatar.owner); contact.setAvatar(avatar); syncRoster(account); getAvatarService().clear(contact); @@ -4724,7 +4634,7 @@ public class XmppConnectionService extends Service { if (callback != null) { callback.success(avatar); } - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner); return; } } else { @@ -4747,57 +4657,54 @@ public class XmppConnectionService extends Service { } private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.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(); + final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar); + this.sendIqPacket(account, packet, response -> { + final boolean previouslyOmittedPepFetch; + synchronized (mInProgressAvatarFetches) { + final String KEY = generateFetchKey(account, avatar); + mInProgressAvatarFetches.remove(KEY); + previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); + } + if (response.getType() == Iq.Type.RESULT) { + Element vCard = response.findChild("vCard", "vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; + if (image != null) { + avatar.image = image; + if (getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner + " omittedPep=" + previouslyOmittedPepFetch); + if (avatar.owner.isBareJid()) { + if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); + account.setAvatar(avatar.getFilename()); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); } else { - 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); - } + 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); } } } @@ -4808,36 +4715,32 @@ public class XmppConnectionService extends Service { }); } - public void checkForAvatar(Account account, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); + public 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) { + Element pubsub = response.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid().asBareJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); } - return; + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); } + return; } } } - callback.error(0, null); } + callback.error(0, null); }); } @@ -4854,10 +4757,10 @@ public class XmppConnectionService extends Service { } public void fetchVcard4(Account account, final Contact contact, final Consumer callback) { - IqPacket packet = this.mIqGenerator.retrieveVcard4(contact.getJid()); - sendIqPacket(account, packet, (a, result) -> { - if (result.getType() == IqPacket.TYPE.RESULT) { - final Element item = mIqParser.getItem(result); + final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid()); + sendIqPacket(account, packet, (result) -> { + if (result.getType() == Iq.Type.RESULT) { + final Element item = IqParser.getItem(result); if (item != null) { final Element vcard4 = item.findChild("vcard", Namespace.VCARD4); if (vcard4 != null) { @@ -4888,7 +4791,7 @@ public class XmppConnectionService extends Service { contact.setOption(Contact.Options.DIRTY_DELETE); Account account = contact.getAccount(); if (account.getStatus() == Account.State.ONLINE) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + 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"); @@ -4948,12 +4851,12 @@ public class XmppConnectionService extends Service { if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); } - final MessagePacket packet = mMessageGenerator.invite(conversation, contact); + final var packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } public void directInvite(Conversation conversation, Jid jid) { - MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); + final var packet = mMessageGenerator.directInvite(conversation, jid); sendMessagePacket(conversation.getAccount(), packet); } @@ -5297,7 +5200,7 @@ public class XmppConnectionService extends Service { if (sendDisplayedMarker && serverAssist) { final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation); - final MessagePacket packet = mMessageGenerator.confirm(last); + final var packet = mMessageGenerator.confirm(last); packet.addChild(mdsDisplayed); if (!last.isPrivateMessage()) { packet.setTo(packet.getTo().asBareJid()); @@ -5313,7 +5216,7 @@ public class XmppConnectionService extends Service { conversation.getAccount().getJid().asBareJid() + ": sending displayed marker to " + last.getCounterpart().toString()); - final MessagePacket packet = mMessageGenerator.confirm(last); + final var packet = mMessageGenerator.confirm(last); this.sendMessagePacket(account, packet); } } @@ -5367,7 +5270,6 @@ public class XmppConnectionService extends Service { public void updateMemorizingTrustManager() { final MemorizingTrustManager trustManager; - final var appSettings = new AppSettings(this); if (appSettings.isTrustSystemCAStore()) { trustManager = new MemorizingTrustManager(getApplicationContext()); } else { @@ -5399,7 +5301,7 @@ public class XmppConnectionService extends Service { if (Config.MAGIC_CREATE_DOMAIN != null) { hosts.add(Config.MAGIC_CREATE_DOMAIN); } - hosts.add("monocles.eu"); + hosts.add("chat.above.im"); return hosts; } @@ -5420,15 +5322,15 @@ public class XmppConnectionService extends Service { return mucServers; } - public void sendMessagePacket(Account account, MessagePacket packet) { + public void sendMessagePacket(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendMessagePacket(packet); } } - public void sendPresencePacket(Account account, PresencePacket packet) { - XmppConnection connection = account.getXmppConnection(); + 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); } @@ -5436,22 +5338,22 @@ public class XmppConnectionService extends Service { public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data); - connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true); + if (connection == null) { + return; } + connection.sendCreateAccountWithCaptchaPacket(id, data); } - public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { + public void sendIqPacket(final Account account, final Iq packet, final Consumer callback) { sendIqPacket(account, packet, callback, null); } - public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback, Long timeout) { + public void sendIqPacket(final Account account, final Iq packet, final Consumer callback, Long timeout) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendIqPacket(packet, callback, timeout); } else if (callback != null) { - callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT)); + callback.accept(Iq.TIMEOUT); } } @@ -5466,7 +5368,7 @@ public class XmppConnectionService extends Service { } else { status = getTargetPresence(); } - final PresencePacket packet = mPresenceGenerator.selfPresence(account, status); + 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)); @@ -5516,10 +5418,6 @@ public class XmppConnectionService extends Service { return this.mIqGenerator; } - public IqParser getIqParser() { - return this.mIqParser; - } - public JingleConnectionManager getJingleConnectionManager() { return this.mJingleConnectionManager; } @@ -5618,10 +5516,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(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - a.getBlocklist().add(jid); + this.sendIqPacket(account, getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (response) -> { + if (response.getType() == Iq.Type.RESULT) { + account.getBlocklist().add(jid); updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); } }); @@ -5662,31 +5561,29 @@ 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(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } + this.sendIqPacket(account, getIqGenerator().generateSetUnblockRequest(jid), response -> { + if (response.getType() == Iq.Type.RESULT) { + account.getBlocklist().remove(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); } }); } } - public void publishDisplayName(Account account) { + public void publishDisplayName(final Account account) { String displayName = account.getDisplayName(); - final IqPacket request; + final Iq request; if (TextUtils.isEmpty(displayName)) { request = mIqGenerator.deleteNode(Namespace.NICK); } else { request = mIqGenerator.publishNick(displayName); } mAvatarService.clear(account); - sendIqPacket(account, request, (account1, packet) -> { - if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.ERROR) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to modify nick name " + packet); } }); } @@ -5706,15 +5603,15 @@ public class XmppConnectionService extends Service { } public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) { - IqPacket request = new IqPacket(input == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); + final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET); request.setTo(jid); Element query = request.query("jabber:iq:gateway"); if (input != null) { Element prompt = query.addChild("prompt"); prompt.setContent(input); } - sendIqPacket(account, request, (Account acct, IqPacket packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, packet -> { + if (packet.getType() == Iq.Type.RESULT) { callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null); } else { Element error = packet.findChild("error"); @@ -5743,7 +5640,7 @@ public class XmppConnectionService extends Service { } updateConversationUi(true); } else { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(jid); final String node = presence == null ? null : presence.getNode(); final String ver = presence == null ? null : presence.getVer(); @@ -5752,12 +5649,12 @@ public class XmppConnectionService extends Service { query.setAttribute("node", node + "#" + ver); } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + (key == null ? "" : key.second) + " to " + jid); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) { databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult(a.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult); + injectServiceDiscoveryResult(account.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult); if (discoveryResult.hasIdentity("gateway", "pstn")) { final Contact contact = account.getRoster().getContact(jid); contact.registerAsPhoneAccount(this); @@ -5766,7 +5663,7 @@ public class XmppConnectionService extends Service { updateConversationUi(true); if (cb != null) cb.run(); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid); @@ -5775,8 +5672,8 @@ public class XmppConnectionService extends Service { } } - public void fetchCommands(Account account, final Jid jid, OnIqPacketReceived callback) { - final IqPacket request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands"); + public void fetchCommands(Account account, final Jid jid, Consumer callback) { + final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands"); sendIqPacket(account, request, callback); } @@ -5811,13 +5708,13 @@ public class XmppConnectionService extends Service { } } - public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) { + public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) { final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.addChild("prefs", version.namespace); - sendIqPacket(account, request, (account1, packet) -> { - Element prefs = packet.findChild("prefs", version.namespace); - if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { + sendIqPacket(account, request, (packet) -> { + final Element prefs = packet.findChild("prefs", version.namespace); + if (packet.getType() == Iq.Type.RESULT && prefs != null) { callback.onPreferencesFetched(prefs); } else { callback.onPreferencesFetchFailed(); @@ -5917,7 +5814,7 @@ public class XmppConnectionService extends Service { } public void pushMamPreferences(Account account, Element prefs) { - IqPacket set = new IqPacket(IqPacket.TYPE.SET); + final Iq set = new Iq(Iq.Type.SET); set.addChild(prefs); sendIqPacket(account, set, null); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index e4aaae9eb7..24a0284536 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -270,6 +270,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers popupMenu.show(); return true; }); + this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic)); this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked); this.binding.mucEditTitle.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(this); @@ -432,6 +433,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucEditor.setVisibility(View.GONE); this.binding.mucDisplay.setVisibility(View.VISIBLE); this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp); + this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic)); } private void onMucInfoUpdated(String subject, String name) { @@ -669,7 +671,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers }); this.mUserPreviewAdapter.submitList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users))); 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(users.size() > 0 ? View.VISIBLE : View.GONE); this.binding.showUsers.setText(getResources().getQuantityString(R.plurals.view_users, users.size(), users.size())); this.binding.usersWrapper.setVisibility(users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE); if (users.size() == 0) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 865fa10c06..2cb04c048e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -217,7 +217,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; import me.drakeet.support.toast.ToastCompat; @@ -3587,13 +3588,13 @@ public class ConversationFragment extends XmppFragment } else { if (!delayShow) conversation.showViewPager(); binding.commandsViewProgressbar.setVisibility(View.VISIBLE); - activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> { + activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (iq) -> { if (activity == null) return; activity.runOnUiThread(() -> { binding.commandsViewProgressbar.setVisibility(View.GONE); commandAdapter.clear(); - if (iq.getType() == IqPacket.TYPE.RESULT) { + if (iq.getType() == Iq.Type.RESULT) { for (Element child : iq.query().getChildren()) { if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue; commandAdapter.add(new CommandAdapter.Command0050(child)); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 0d7470d626..04c833f9dd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -257,7 +257,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (ExceptionHelper.checkForCrash(this)) return; if (offerToSetupDiallerIntegration()) return; if (offerToDownloadStickers()) return; - openBatteryOptimizationDialogIfNeeded(); + if (openBatteryOptimizationDialogIfNeeded()) return; + requestNotificationPermissionIfNeeded(); xmppConnectionService.rescanStickers(); } } @@ -282,7 +283,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio intent.setData(uri); try { startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); } }); @@ -375,16 +376,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private void notifyFragmentOfBackendConnected(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof OnBackendConnected) { - ((OnBackendConnected) fragment).onBackendConnected(); + if (fragment instanceof OnBackendConnected callback) { + callback.onBackendConnected(); } } private void refreshFragment(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof XmppFragment) { - ((XmppFragment) fragment).refresh(); - if (refreshForNewCaps) ((XmppFragment) fragment).refreshForNewCaps(newCapsJids); + if (fragment instanceof XmppFragment xmppFragment) { + xmppFragment.refresh(); + if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index de1f760c8b..788440148e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -47,6 +47,7 @@ import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; @@ -96,42 +97,36 @@ public class ConversationsOverviewFragment extends XmppFragment { private FragmentConversationsOverviewBinding binding; private ConversationAdapter conversationsAdapter; private XmppActivity activity; - private float mSwipeEscapeVelocity = 0f; private final PendingActionHelper pendingActionHelper = new PendingActionHelper(); private final ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,LEFT|RIGHT) { @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - //todo maybe we can manually changing the position of the conversation + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; } @Override - public float getSwipeEscapeVelocity (float defaultValue) { - return mSwipeEscapeVelocity; - } - - @Override - public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - float dX, float dY, int actionState, boolean isCurrentlyActive) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){ - Paint paint = new Paint(); - paint.setColor(MaterialColors.getColor(viewHolder.itemView, com.google.android.material.R.attr.colorSecondaryFixedDim)); - paint.setStyle(Paint.Style.FILL); - c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop() - ,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint); + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) { + getDefaultUIUtil().onDraw(c,recyclerView,conversationViewHolder.binding.frame,dX,dY,actionState,isCurrentlyActive); } } @Override - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewHolder.itemView.setAlpha(1f); + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) { + getDefaultUIUtil().clearView(conversationViewHolder.binding.frame); + } } @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + public float getSwipeEscapeVelocity(final float defaultEscapeVelocity) { + return 32 * defaultEscapeVelocity; + } + + @Override + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) { pendingActionHelper.execute(); int position = viewHolder.getLayoutPosition(); try { @@ -291,7 +286,6 @@ public class ConversationsOverviewFragment extends XmppFragment { @Override public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity); this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false); this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity())); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 3a48ca2eb3..bd685ea791 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -43,6 +43,7 @@ import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; import com.rarepebble.colorpicker.ColorPickerView; @@ -104,8 +105,14 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, - OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { +public class EditAccountActivity extends OmemoActivity + implements OnAccountUpdate, + OnUpdateBlocklist, + OnKeyStatusUpdated, + OnCaptchaRequested, + KeyChainAliasCallback, + XmppConnectionService.OnShowErrorToast, + XmppConnectionService.OnMamPreferencesFetched { public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification"; public static final String EXTRA_FORCE_REGISTER = "force_register"; @@ -122,37 +129,44 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private boolean mUsernameMode = false; private boolean mShowOptions = false; private Account mAccount; - private final OnClickListener mCancelButtonClickListener = v -> { - deleteAccountAndReturnIfNecessary(); - finish(); - }; - private final UiCallback mAvatarFetchCallback = new UiCallback() { + private final OnClickListener mCancelButtonClickListener = + v -> { + deleteAccountAndReturnIfNecessary(); + finish(); + }; + private final UiCallback mAvatarFetchCallback = + new UiCallback() { - @Override - public void userInputRequired(final PendingIntent pi, final Avatar avatar) { - finishInitialSetup(avatar); - } + @Override + public void userInputRequired(final PendingIntent pi, final Avatar avatar) { + finishInitialSetup(avatar); + } - @Override - public void success(final Avatar avatar) { - finishInitialSetup(avatar); - } + @Override + public void success(final Avatar avatar) { + finishInitialSetup(avatar); + } - @Override - public void error(final int errorCode, final Avatar avatar) { - finishInitialSetup(avatar); - } - }; - private final OnClickListener mAvatarClickListener = new OnClickListener() { - @Override - public void onClick(final View view) { - if (mAccount != null) { - final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } - } - }; + @Override + public void error(final int errorCode, final Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private final OnClickListener mAvatarClickListener = + new OnClickListener() { + @Override + public void onClick(final View view) { + if (mAccount != null) { + final Intent intent = + new Intent( + getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } + } + }; private String messageFingerprint; private boolean mFetchingAvatar = false; private Toast mFetchingMamPrefsToast; @@ -161,215 +175,268 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private XmppUri pendingUri = null; private boolean mUseTor; private ActivityEditAccountBinding binding; - private String newPassword = null; - private final OnClickListener mSaveButtonClickListener = new OnClickListener() { + private final OnClickListener mSaveButtonClickListener = + new OnClickListener() { - @Override - public void onClick(final View v) { - final String password = binding.accountPassword.getText().toString(); - final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED; - final boolean accountInfoEdited = accountInfoEdited(); + @Override + public void onClick(final View v) { + final String password = binding.accountPassword.getText().toString(); + final boolean wasDisabled = + mAccount != null && mAccount.getStatus() == Account.State.DISABLED; + final boolean accountInfoEdited = accountInfoEdited(); - ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); - if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) { - mAccount.setColor(previewColor.getColor()); - } + ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); + if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) { + mAccount.setColor(previewColor.getColor()); + } - if (mInitMode && mAccount != null) { - mAccount.setOption(Account.OPTION_DISABLED, false); - } - if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) { - mAccount.setOption(Account.OPTION_SOFT_DISABLED, false); - mAccount.setOption(Account.OPTION_DISABLED, false); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - } - return; - } - final boolean registerNewAccount; - if (mForceRegister != null) { - registerNewAccount = mForceRegister; - } else { - registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI; - } - if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - final boolean startOrbot = mAccount != null && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE; - if (startOrbot) { - if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) { - TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } else { - TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } - return; - } - - if (inNeedOfSaslAccept()) { - mAccount.resetPinnedMechanism(); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - } - return; - } - - final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB; - final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; - final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; - final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null; - if (url != null && !wasDisabled) { - try { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); - return; - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, Toast.LENGTH_SHORT).show(); - return; - } - } - - final Jid jid; - try { - if (mUsernameMode) { - jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null); - } else { - jid = Jid.ofEscaped(binding.accountJid.getText().toString()); - Resolver.checkDomain(jid); - } - } catch (final NullPointerException | IllegalArgumentException e) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - binding.accountJid.requestFocus(); - removeErrorsOnAllBut(binding.accountJidLayout); - return; - } - final String hostname; - int numericPort = 5222; - if (mShowOptions) { - hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText()); - final String port = CharMatcher.whitespace().removeFrom(binding.port.getText()); - if (Resolver.invalidHostname(hostname)) { - binding.hostnameLayout.setError(getString(R.string.not_valid_hostname)); - binding.hostname.requestFocus(); - removeErrorsOnAllBut(binding.hostnameLayout); - return; - } - if (!hostname.isEmpty()) { - try { - numericPort = Integer.parseInt(port); - if (numericPort < 0 || numericPort > 65535) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); - return; + if (mInitMode && mAccount != null) { + mAccount.setOption(Account.OPTION_DISABLED, false); + } + if (mAccount != null + && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT) + .contains(mAccount.getStatus()) + && !accountInfoEdited) { + mAccount.setOption(Account.OPTION_SOFT_DISABLED, false); + mAccount.setOption(Account.OPTION_DISABLED, false); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); } - - } catch (NumberFormatException e) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); return; } + final boolean registerNewAccount; + if (mForceRegister != null) { + registerNewAccount = mForceRegister; + } else { + registerNewAccount = + binding.accountRegisterNew.isChecked() + && !Config.DISALLOW_REGISTRATION_IN_UI; + } + if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); + return; + } + + XmppConnection connection = + mAccount == null ? null : mAccount.getXmppConnection(); + final boolean startOrbot = + mAccount != null + && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE; + if (startOrbot) { + if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) { + TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT); + } else { + TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT); + } + return; + } + + if (inNeedOfSaslAccept()) { + mAccount.resetPinnedMechanism(); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); + } + return; + } + + final boolean openRegistrationUrl = + registerNewAccount + && !accountInfoEdited + && mAccount != null + && mAccount.getStatus() == Account.State.REGISTRATION_WEB; + final boolean openPaymentUrl = + mAccount != null + && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; + final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; + final HttpUrl url = + connection != null && redirectionWorthyStatus + ? connection.getRedirectionUrl() + : null; + if (url != null && !wasDisabled) { + try { + startActivity( + new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); + return; + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + R.string.application_found_to_open_website, + Toast.LENGTH_SHORT) + .show(); + return; + } + } + + final Jid jid; + try { + if (mUsernameMode) { + jid = + Jid.ofEscaped( + binding.accountJid.getText().toString(), + getUserModeDomain(), + null); + } else { + jid = Jid.ofEscaped(binding.accountJid.getText().toString()); + Resolver.checkDomain(jid); + } + } catch (final NullPointerException | IllegalArgumentException e) { + if (mUsernameMode) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + } else { + binding.accountJidLayout.setError(getString(R.string.invalid_jid)); + } + binding.accountJid.requestFocus(); + removeErrorsOnAllBut(binding.accountJidLayout); + return; + } + final String hostname; + int numericPort = 5222; + if (mShowOptions) { + hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText()); + final String port = + CharMatcher.whitespace().removeFrom(binding.port.getText()); + if (Resolver.invalidHostname(hostname)) { + binding.hostnameLayout.setError(getString(R.string.not_valid_hostname)); + binding.hostname.requestFocus(); + removeErrorsOnAllBut(binding.hostnameLayout); + return; + } + if (!hostname.isEmpty()) { + try { + numericPort = Integer.parseInt(port); + if (numericPort < 0 || numericPort > 65535) { + binding.portLayout.setError( + getString(R.string.not_a_valid_port)); + removeErrorsOnAllBut(binding.portLayout); + binding.port.requestFocus(); + return; + } + + } catch (NumberFormatException e) { + binding.portLayout.setError(getString(R.string.not_a_valid_port)); + removeErrorsOnAllBut(binding.portLayout); + binding.port.requestFocus(); + return; + } + } + } else { + hostname = null; + } + + if (jid.getLocal() == null) { + if (mUsernameMode) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + } else { + binding.accountJidLayout.setError(getString(R.string.invalid_jid)); + } + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); + return; + } + if (mAccount != null) { + if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { + mAccount.setOption( + Account.OPTION_MAGIC_CREATE, + mAccount.getPassword().contains(password)); + } + mAccount.setJid(jid); + mAccount.setPort(numericPort); + mAccount.setHostname(hostname); + binding.accountJidLayout.setError(null); + mAccount.setPassword(password); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); + return; + } + } else { + if (xmppConnectionService.findAccountByJid(jid) != null) { + binding.accountJidLayout.setError( + getString(R.string.account_already_exists)); + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); + return; + } + mAccount = new Account(jid.asBareJid(), password); + mAccount.setPort(numericPort); + mAccount.setHostname(hostname); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.createAccount(mAccount); + } + binding.hostnameLayout.setError(null); + binding.portLayout.setError(null); + if (mAccount.isOnion()) { + Toast.makeText( + EditAccountActivity.this, + R.string.audio_video_disabled_tor, + Toast.LENGTH_LONG) + .show(); + } + if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) { + finish(); + } else { + updateSaveButton(); + updateAccountInformation(true); + } } - } else { - hostname = null; - } + }; + private final TextWatcher mTextWatcher = + new TextWatcher() { - if (jid.getLocal() == null) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); + @Override + public void onTextChanged( + final CharSequence s, final int start, final int before, final int count) { + updatePortLayout(); + updateSaveButton(); } - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - if (mAccount != null) { - if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password)); + + @Override + public void beforeTextChanged( + final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void afterTextChanged(final Editable s) {} + }; + private final View.OnFocusChangeListener mEditTextFocusListener = + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean b) { + EditText et = (EditText) view; + if (b) { + int resId = + mUsernameMode + ? R.string.username + : R.string.account_settings_example_jabber_id; + if (view.getId() == R.id.hostname) { + resId = + mUseTor + ? R.string.hostname_or_onion + : R.string.hostname_example; + } + final int res = resId; + new Handler().postDelayed(() -> et.setHint(res), 200); + } else { + et.setHint(null); + } } - mAccount.setJid(jid); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - binding.accountJidLayout.setError(null); - mAccount.setPassword(password); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - return; - } - } else { - if (xmppConnectionService.findAccountByJid(jid) != null) { - binding.accountJidLayout.setError(getString(R.string.account_already_exists)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - mAccount = new Account(jid.asBareJid(), password); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - xmppConnectionService.createAccount(mAccount); - } - binding.hostnameLayout.setError(null); - binding.portLayout.setError(null); - if (mAccount.isOnion()) { - Toast.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, Toast.LENGTH_LONG).show(); - } - if (mAccount.isEnabled() - && !registerNewAccount - && !mInitMode) { - finish(); - } else { - updateSaveButton(); - updateAccountInformation(true); - } + }; - } - }; - private final TextWatcher mTextWatcher = new TextWatcher() { - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - updatePortLayout(); - updateSaveButton(); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { - } - - @Override - public void afterTextChanged(final Editable s) { - - } - }; - private final View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - EditText et = (EditText) view; - if (b) { - int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id; - if (view.getId() == R.id.hostname) { - resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example; - } - final int res = resId; - new Handler().postDelayed(() -> et.setHint(res), 200); - } else { - et.setHint(null); - } - } - }; - - private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) { + private static void setAvailabilityRadioButton( + Presence.Status status, DialogPresenceBinding binding) { if (status == null) { binding.online.setChecked(true); return; @@ -403,9 +470,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat public void refreshUiReal() { invalidateOptionsMenu(); - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { + if (mAccount != null && mAccount.getStatus() != Account.State.ONLINE && mFetchingAvatar) { Intent intent = new Intent(this, StartConversationActivity.class); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); @@ -435,30 +500,41 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void deleteAccountAndReturnIfNecessary() { - if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { + if (mInitMode + && mAccount != null + && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { xmppConnectionService.deleteAccount(mAccount); } - final boolean magicCreate = mAccount != null && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean magicCreate = + mAccount != null + && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); final Jid jid = mAccount == null ? null : mAccount.getJid(); - if (SignupUtils.isSupportTokenRegistry() && jid != null && magicCreate && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) { + if (SignupUtils.isSupportTokenRegistry() + && jid != null + && magicCreate + && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) { final Jid preset; if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) { preset = jid.asBareJid(); } else { preset = jid.getDomain(); } - final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); + final Intent intent = + SignupUtils.getTokenRegistrationIntent( + this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); return; } - - final List accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); + final List accounts = + xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); + Intent intent = + SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); } @@ -470,27 +546,38 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } protected void finishInitialSetup(final Avatar avatar) { - runOnUiThread(() -> { - SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); - final Intent intent; - final XmppConnection connection = mAccount.getXmppConnection(); - final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1; - if (avatar != null || (connection != null && !connection.getFeatures().pep())) { - intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.putExtra("init", true); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - } else { - intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - } - if (wasFirstAccount) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - finish(); - }); + runOnUiThread( + () -> { + SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); + final Intent intent; + final XmppConnection connection = mAccount.getXmppConnection(); + final boolean wasFirstAccount = + xmppConnectionService != null + && xmppConnectionService.getAccounts().size() == 1; + if (avatar != null || (connection != null && !connection.getFeatures().pep())) { + intent = + new Intent( + getApplicationContext(), StartConversationActivity.class); + intent.putExtra("init", true); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + } else { + intent = + new Intent( + getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + intent.putExtra("setup", true); + } + if (wasFirstAccount) { + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + } + StartConversationActivity.addInviteUri(intent, getIntent()); + startActivity(intent); + finish(); + }); } @Override @@ -514,8 +601,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (requestCode == REQUEST_UNLOCK) { if (resultCode == RESULT_OK) { openChangePassword(true); - } else { - this.newPassword = null; } } } @@ -526,7 +611,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) { - if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) { + if (mAccount != null + && mAccount.getJid().asBareJid().equals(uri.getJid()) + && uri.hasFingerprints()) { if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) { Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show(); updateAccountInformation(false); @@ -553,10 +640,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.saveButton.setText(R.string.save); this.binding.saveButton.setEnabled(true); } else if (mAccount != null - && (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) { + && (mAccount.getStatus() == Account.State.CONNECTING + || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL + || mFetchingAvatar)) { this.binding.saveButton.setEnabled(false); this.binding.saveButton.setText(R.string.account_status_connecting); - } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) { + } else if (mAccount != null + && mAccount.getStatus() == Account.State.DISABLED + && !mInitMode) { this.binding.saveButton.setEnabled(true); this.binding.saveButton.setText(R.string.enable); } else if (torNeedsInstall(mAccount)) { @@ -574,8 +665,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.saveButton.setEnabled(false); } } else { - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null; + XmppConnection connection = + mAccount == null ? null : mAccount.getXmppConnection(); + HttpUrl url = + connection != null + && mAccount.getStatus() + == Account.State.PAYMENT_REQUIRED + ? connection.getRedirectionUrl() + : null; if (url != null) { this.binding.saveButton.setText(R.string.open_website); } else if (inNeedOfSaslAccept()) { @@ -586,8 +683,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } else { XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null; - if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) { + HttpUrl url = + connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB + ? connection.getRedirectionUrl() + : null; + if (url != null + && this.binding.accountRegisterNew.isChecked() + && !accountInfoEdited) { this.binding.saveButton.setText(R.string.open_website); } else { this.binding.saveButton.setText(R.string.next); @@ -597,7 +699,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private boolean torNeedsInstall(final Account account) { - return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE && !TorServiceUtils.isOrbotInstalled(this); + return account != null + && account.getStatus() == Account.State.TOR_NOT_AVAILABLE + && !TorServiceUtils.isOrbotInstalled(this); } private boolean torNeedsStart(final Account account) { @@ -609,11 +713,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat return false; } ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); - return jidEdited() || - !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) || - !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) || - this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) || - !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString()); + return jidEdited() + || !this.mAccount + .getPassword() + .equals(this.binding.accountPassword.getText().toString()) + || !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) + || this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) + || !String.valueOf(this.mAccount.getPort()) + .equals(this.binding.port.getText().toString()); } protected boolean jidEdited() { @@ -660,7 +767,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) { changeMoreTableVisibility(true); } - final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> updateSaveButton(); + final OnCheckedChangeListener OnCheckedShowConfirmPassword = + (buttonView, isChecked) -> updateSaveButton(); this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); if (Config.DISALLOW_REGISTRATION_IN_UI) { this.binding.accountRegisterNew.setVisibility(View.GONE); @@ -703,18 +811,23 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void onEditYourNameClicked(View view) { - quickEdit(mAccount.getDisplayName(), R.string.your_name, value -> { - final String displayName = value.trim(); - updateDisplayName(displayName); - mAccount.setDisplayName(displayName); - xmppConnectionService.publishDisplayName(mAccount); - refreshAvatar(); - return null; - }, true); + quickEdit( + mAccount.getDisplayName(), + R.string.your_name, + value -> { + final String displayName = value.trim(); + updateDisplayName(displayName); + mAccount.setDisplayName(displayName); + xmppConnectionService.publishDisplayName(mAccount); + refreshAvatar(); + return null; + }, + true); } private void refreshAvatar() { - AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } @Override @@ -789,9 +902,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } boolean init = intent.getBooleanExtra("init", false); - boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); + boolean openedFromNotification = + intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); Log.d(Config.LOGTAG, "extras " + intent.getExtras()); - this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) : null; + this.mForceRegister = + intent.hasExtra(EXTRA_FORCE_REGISTER) + ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) + : null; Log.d(Config.LOGTAG, "force register=" + mForceRegister); this.mInitMode = init || this.jidToEdit == null; this.messageFingerprint = intent.getStringExtra("fingerprint"); @@ -801,7 +918,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat configureActionBar(getSupportActionBar(), !openedFromNotification); } else { this.binding.avater.setVisibility(View.GONE); - configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null)); + configureActionBar( + getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null)); if (mForceRegister != null) { if (mForceRegister) { setTitle(R.string.register_new_account); @@ -833,13 +951,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat TextView warning = view.findViewById(R.id.warning); warning.setText(R.string.verifying_omemo_keys_trusted_source_account); builder.setView(view); - builder.setPositiveButton(R.string.continue_btn, (dialog, which) -> { - if (isTrustedSource.isChecked()) { - processFingerprintVerification(xmppUri, false); - } else { - finish(); - } - }); + builder.setPositiveButton( + R.string.continue_btn, + (dialog, which) -> { + if (isTrustedSource.isChecked()) { + processFingerprintVerification(xmppUri, false); + } else { + finish(); + } + }); builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish()); final var dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); @@ -863,9 +983,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat @Override public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (mAccount != null) { - savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); + savedInstanceState.putString( + "account", mAccount.getJid().asBareJid().toEscapedString()); savedInstanceState.putBoolean("initMode", mInitMode); - savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); + savedInstanceState.putBoolean( + "showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); } super.onSaveInstanceState(savedInstanceState); } @@ -874,7 +996,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat boolean init = true; if (mSavedInstanceAccount != null) { try { - this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount)); + this.mAccount = + xmppConnectionService.findAccountByJid( + Jid.ofEscaped(mSavedInstanceAccount)); this.mInitMode = mSavedInstanceInit; init = false; } catch (IllegalArgumentException e) { @@ -887,7 +1011,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (mAccount != null) { this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER); - this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER); + this.mUsernameMode |= + mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + && mAccount.isOptionSet(Account.OPTION_REGISTER); if (mPendingFingerprintVerificationUri != null) { processFingerprintVerification(mPendingFingerprintVerificationUri, false); mPendingFingerprintVerificationUri = null; @@ -895,16 +1021,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat updateAccountInformation(init); } - - if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) { + if (Config.MAGIC_CREATE_DOMAIN == null + && this.xmppConnectionService.getAccounts().size() == 0) { this.binding.cancelButton.setEnabled(false); } if (mUsernameMode) { this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); } else { - final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - R.layout.item_autocomplete, - xmppConnectionService.getKnownHosts()); + final KnownHostsAdapter mKnownHostsAdapter = + new KnownHostsAdapter( + this, + R.layout.item_autocomplete, + xmppConnectionService.getKnownHosts()); this.binding.accountJid.setAdapter(mKnownHostsAdapter); } @@ -952,7 +1080,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat shareLink(false); break; case R.id.action_change_password_on_server: - gotoChangePassword(null); + gotoChangePassword(); break; case R.id.action_delete_account: deleteAccount(); @@ -971,13 +1099,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void deleteAccount() { - this.deleteAccount(mAccount,()->{ - finish(); - }); + this.deleteAccount( + mAccount, + () -> { + finish(); + }); } private boolean inNeedOfSaslAccept() { - return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited(); + return mAccount != null + && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK + && mAccount.getPinnedMechanismPriority() >= 0 + && !accountInfoEdited(); } private void shareBarcode() { @@ -988,12 +1121,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat startActivity(Intent.createChooser(intent, getText(R.string.share_with))); } - private void changeMoreTableVisibility(boolean visible) { + private void changeMoreTableVisibility(final boolean visible) { binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE); + binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE); } - private void gotoChangePassword(String newPassword) { - this.newPassword = newPassword; + private void gotoChangePassword() { KeyguardManager keyguardManager = (KeyguardManager) this.getSystemService(Context.KEYGUARD_SERVICE); Intent credentialsIntent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock required", "Please unlock in order to change your password"); if (credentialsIntent == null) { @@ -1007,10 +1140,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); changePasswordIntent.putExtra("did_unlock", didUnlock); - if (newPassword != null) { - changePasswordIntent.putExtra("password", newPassword); - } - this.newPassword = null; startActivity(changePasswordIntent); } @@ -1020,9 +1149,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private void changePresence() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean manualStatus = sharedPreferences.getBoolean(AppSettings.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence)); + boolean manualStatus = + sharedPreferences.getBoolean( + AppSettings.MANUALLY_CHANGE_PRESENCE, + getResources().getBoolean(R.bool.manually_change_presence)); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false); + final DialogPresenceBinding binding = + DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false); String current = mAccount.getPresenceStatusMessage(); if (current != null && !current.trim().isEmpty()) { binding.statusMessage.append(current); @@ -1030,47 +1163,66 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding); binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE); List templates = xmppConnectionService.getPresenceTemplates(mAccount); - PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates); + PresenceTemplateAdapter presenceTemplateAdapter = + new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates); binding.statusMessage.setAdapter(presenceTemplateAdapter); - binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> { - PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position); - setAvailabilityRadioButton(template.getStatus(), binding); - }); + binding.statusMessage.setOnItemClickListener( + (parent, view, position, id) -> { + PresenceTemplate template = + (PresenceTemplate) parent.getItemAtPosition(position); + setAvailabilityRadioButton(template.getStatus(), binding); + }); builder.setTitle(R.string.edit_status_message_title); builder.setView(binding.getRoot()); builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - PresenceTemplate template = new PresenceTemplate(getAvailabilityRadioButton(binding), binding.statusMessage.getText().toString().trim()); - if (mAccount.getPgpId() != 0 && hasPgp()) { - generateSignature(null, template); - } else { - xmppConnectionService.changeStatus(mAccount, template, null); - } - }); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + PresenceTemplate template = + new PresenceTemplate( + getAvailabilityRadioButton(binding), + binding.statusMessage.getText().toString().trim()); + if (mAccount.getPgpId() != 0 && hasPgp()) { + generateSignature(null, template); + } else { + xmppConnectionService.changeStatus(mAccount, template, null); + } + }); builder.create().show(); } private void generateSignature(Intent intent, PresenceTemplate template) { - xmppConnectionService.getPgpEngine().generateSignature(intent, mAccount, template.getStatusMessage(), new UiCallback() { - @Override - public void success(String signature) { - xmppConnectionService.changeStatus(mAccount, template, signature); - } + xmppConnectionService + .getPgpEngine() + .generateSignature( + intent, + mAccount, + template.getStatusMessage(), + new UiCallback() { + @Override + public void success(String signature) { + xmppConnectionService.changeStatus(mAccount, template, signature); + } - @Override - public void error(int errorCode, String object) { + @Override + public void error(int errorCode, String object) {} - } - - @Override - public void userInputRequired(PendingIntent pi, String object) { - mPendingPresenceTemplate.push(template); - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); - } catch (final IntentSender.SendIntentException ignored) { - } - } - }); + @Override + public void userInputRequired(PendingIntent pi, String object) { + mPendingPresenceTemplate.push(template); + try { + startIntentSenderForResult( + pi.getIntentSender(), + REQUEST_CHANGE_STATUS, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); + } catch (final IntentSender.SendIntentException ignored) { + } + } + }); } @Override @@ -1104,9 +1256,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (init) { this.binding.accountJid.getEditableText().clear(); if (mUsernameMode) { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getEscapedLocal()); + this.binding + .accountJid + .getEditableText() + .append(this.mAccount.getJid().getEscapedLocal()); } else { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().asBareJid().toEscapedString()); + this.binding + .accountJid + .getEditableText() + .append(this.mAccount.getJid().asBareJid().toEscapedString()); } this.binding.accountPassword.getEditableText().clear(); this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword()); @@ -1115,7 +1273,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.port.setText(""); this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort())); this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); - } final var preferences = getPreferences(); @@ -1143,13 +1300,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); } - final boolean editable = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME) && QuickConversationsService.isConversations(); + final boolean editable = + !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) + && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME) + && QuickConversationsService.isConversations(); this.binding.accountJid.setEnabled(editable); this.binding.accountJid.setFocusable(editable); this.binding.accountJid.setFocusableInTouchMode(editable); this.binding.accountJid.setCursorVisible(editable); - final String displayName = mAccount.getDisplayName(); updateDisplayName(displayName); @@ -1162,8 +1321,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat binding.quietHoursBox.setVisibility(View.GONE); } - final boolean togglePassword = mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - final boolean editPassword = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) || mAccount.getLastErrorStatus() == Account.State.UNAUTHORIZED; + final boolean togglePassword = + mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean neverLoggedIn = + !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) + && QuickConversationsService.isConversations(); + final boolean editPassword = mAccount.unauthorized() || neverLoggedIn; this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword); @@ -1174,11 +1338,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (!mInitMode) { this.binding.avater.setVisibility(View.VISIBLE); - AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } else { this.binding.avater.setVisibility(View.GONE); } - this.binding.accountRegisterNew.setChecked(this.mAccount.isOptionSet(Account.OPTION_REGISTER)); + this.binding.accountRegisterNew.setChecked( + this.mAccount.isOptionSet(Account.OPTION_REGISTER)); if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { ActionBar actionBar = getSupportActionBar(); @@ -1193,13 +1359,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.accountRegisterNew.setVisibility(View.GONE); } if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { - Features features = this.mAccount.getXmppConnection().getFeatures(); + final Features features = this.mAccount.getXmppConnection().getFeatures(); this.binding.stats.setVisibility(View.VISIBLE); boolean showBatteryWarning = isOptimizingBattery(); boolean showDataSaverWarning = isAffectedByDataSaver(); showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning); - this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() - .getLastSessionEstablished())); + this.binding.sessionEst.setText( + UIHelper.readableTimeDifferenceFull( + this, this.mAccount.getXmppConnection().getLastSessionEstablished())); if (features.rosterVersioning()) { this.binding.serverInfoRosterVersion.setText(R.string.server_info_available); } else { @@ -1235,6 +1402,17 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable); } + if (features.bind2()) { + this.binding.serverInfoBind2.setText(R.string.server_info_available); + } else { + this.binding.serverInfoBind2.setText(R.string.server_info_unavailable); + } + if (features.sasl2()) { + this.binding.serverInfoSasl2.setText(R.string.server_info_available); + } else { + this.binding.serverInfoSasl2.setText(R.string.server_info_unavailable); + } + this.binding.loginMechanism.setText(Strings.nullToEmpty(features.loginMechanism())); if (features.pep()) { AxolotlService axolotlService = this.mAccount.getAxolotlService(); if (axolotlService != null && axolotlService.isPepBroken()) { @@ -1250,7 +1428,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (features.httpUpload(0)) { final long maxFileSize = features.getMaxHttpUploadSize(); if (maxFileSize > 0) { - this.binding.serverInfoHttpUpload.setText(UIHelper.filesizeToString(maxFileSize)); + this.binding.serverInfoHttpUpload.setText( + UIHelper.filesizeToString(maxFileSize)); } else { this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); } @@ -1258,7 +1437,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); } - this.binding.pushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE); + this.binding.pushRow.setVisibility( + xmppConnectionService.getPushManagementService().isStub() + ? View.GONE + : View.VISIBLE); if (xmppConnectionService.getPushManagementService().available(mAccount)) { this.binding.serverInfoPush.setText(R.string.server_info_available); @@ -1273,24 +1455,36 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId)); this.binding.pgpFingerprint.setOnClickListener(openPgp); if ("pgp".equals(messageFingerprint)) { - this.binding.pgpFingerprintDesc.setTextColor(MaterialColors.getColor(binding.pgpFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant)); + this.binding.pgpFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.pgpFingerprintDesc, + com.google.android.material.R.attr.colorPrimaryVariant)); } this.binding.pgpFingerprintDesc.setOnClickListener(openPgp); this.binding.actionDeletePgp.setOnClickListener(delete); } else { this.binding.pgpFingerprintBox.setVisibility(View.GONE); } - final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint(); + final String ownAxolotlFingerprint = + this.mAccount.getAxolotlService().getOwnFingerprint(); if (ownAxolotlFingerprint != null && Config.supportOmemo()) { this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE); if (ownAxolotlFingerprint.equals(messageFingerprint)) { - this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant)); - this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message); + this.binding.ownFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.ownFingerprintDesc, + com.google.android.material.R.attr.colorPrimaryVariant)); + this.binding.ownFingerprintDesc.setText( + R.string.omemo_fingerprint_selected_message); } else { - this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorOnSurface)); + this.binding.ownFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.ownFingerprintDesc, + com.google.android.material.R.attr.colorOnSurface)); this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint); } - this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); + this.binding.axolotlFingerprint.setText( + CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); this.binding.showQrCodeButton.setVisibility(View.VISIBLE); this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode()); } else { @@ -1299,7 +1493,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat boolean hasKeys = false; boolean showUnverifiedWarning = false; binding.otherDeviceKeys.removeAllViews(); - for (final XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { + for (final XmppAxolotlSession session : + mAccount.getAxolotlService().findOwnSessions()) { final FingerprintStatus trust = session.getTrust(); if (!trust.isCompromised()) { boolean highlight = session.getFingerprint().equals(messageFingerprint); @@ -1310,7 +1505,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat showUnverifiedWarning = true; } } - if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices + if (hasKeys + && Config.supportOmemo()) { // TODO: either the button should be visible if we + // print an active device or the device list should + // be fed with reactived devices this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE); Set otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); if (otherDevices == null || otherDevices.isEmpty()) { @@ -1318,7 +1516,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { binding.clearDevices.setVisibility(View.VISIBLE); } - binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); + binding.unverifiedWarning.setVisibility( + showUnverifiedWarning ? View.VISIBLE : View.GONE); binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); } else { this.binding.otherDeviceKeysCard.setVisibility(View.GONE); @@ -1339,7 +1538,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { final TextInputLayout errorLayout; if (this.mAccount.errorStatus()) { - if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { + if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED + || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { errorLayout = this.binding.accountPasswordLayout; } else if (mShowOptions && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND @@ -1365,10 +1565,16 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private void updateDisplayName(String displayName) { if (TextUtils.isEmpty(displayName)) { this.binding.yourName.setText(R.string.no_name_set_instructions); - this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant)); + this.binding.yourName.setTextColor( + MaterialColors.getColor( + binding.yourName, + com.google.android.material.R.attr.colorOnSurfaceVariant)); } else { this.binding.yourName.setText(displayName); - this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant)); + this.binding.yourName.setTextColor( + MaterialColors.getColor( + binding.yourName, + com.google.android.material.R.attr.colorOnSurfaceVariant)); } } @@ -1396,46 +1602,71 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat builder.setTitle(R.string.unpublish_pgp); builder.setMessage(R.string.unpublish_pgp_message); builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialogInterface, i) -> { - mAccount.setPgpSignId(0); - mAccount.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(mAccount); - xmppConnectionService.sendPresence(mAccount); - refreshUiReal(); - }); + builder.setPositiveButton( + R.string.confirm, + (dialogInterface, i) -> { + mAccount.setPgpSignId(0); + mAccount.unsetPgpSignature(); + xmppConnectionService.databaseBackend.updateAccount(mAccount); + xmppConnectionService.sendPresence(mAccount); + refreshUiReal(); + }); builder.create().show(); } - private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) { - this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); - if (showDataSaverWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + private void showOsOptimizationWarning( + boolean showBatteryWarning, boolean showDataSaverWarning) { + this.binding.osOptimization.setVisibility( + showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); + if (showDataSaverWarning + && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled); - this.binding.osOptimizationBody.setText(getString(R.string.data_saver_enabled_explained, getString(R.string.app_name))); + this.binding.osOptimizationBody.setText( + getString(R.string.data_saver_enabled_explained, getString(R.string.app_name))); this.binding.osOptimizationDisable.setText(R.string.allow); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_DATA_SAVER); - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, getString(R.string.device_does_not_support_data_saver, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); - } - }); + this.binding.osOptimizationDisable.setOnClickListener( + v -> { + Intent intent = + new Intent( + Settings + .ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + try { + startActivityForResult(intent, REQUEST_DATA_SAVER); + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + getString( + R.string.device_does_not_support_data_saver, + getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); + } + }); } else if (showBatteryWarning) { this.binding.osOptimizationDisable.setText(R.string.disable); this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled); - this.binding.osOptimizationBody.setText(getString(R.string.battery_optimizations_enabled_explained, getString(R.string.app_name))); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); - } - }); + this.binding.osOptimizationBody.setText( + getString( + R.string.battery_optimizations_enabled_explained, + getString(R.string.app_name))); + this.binding.osOptimizationDisable.setOnClickListener( + v -> { + Intent intent = + new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + try { + startActivityForResult(intent, REQUEST_BATTERY_OP); + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + R.string.device_does_not_support_battery_op, + Toast.LENGTH_SHORT) + .show(); + } + }); } } @@ -1445,13 +1676,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setMessage(getString(R.string.clear_other_devices_desc)); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.accept), + builder.setPositiveButton( + getString(R.string.accept), (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices()); builder.create().show(); } private void editMamPrefs() { - this.mFetchingMamPrefsToast = Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG); + this.mFetchingMamPrefsToast = + Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG); this.mFetchingMamPrefsToast.show(); xmppConnectionService.fetchMamPreferences(mAccount, this); } @@ -1462,86 +1695,110 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } @Override - public void onCaptchaRequested(final Account account, final String id, final Data data, final Bitmap captcha) { - runOnUiThread(() -> { - if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) { - mCaptchaDialog.dismiss(); - } - if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { - Log.d(Config.LOGTAG,"activity not running when captcha was requested"); - return; - } - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this); - final View view = getLayoutInflater().inflate(R.layout.captcha, null); - final ImageView imageView = view.findViewById(R.id.captcha); - final EditText input = view.findViewById(R.id.input); - imageView.setImageBitmap(captcha); + public void onCaptchaRequested( + final Account account, final String id, final Data data, final Bitmap captcha) { + runOnUiThread( + () -> { + if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) { + mCaptchaDialog.dismiss(); + } + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + Log.d(Config.LOGTAG, "activity not running when captcha was requested"); + return; + } + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(EditAccountActivity.this); + final View view = getLayoutInflater().inflate(R.layout.captcha, null); + final ImageView imageView = view.findViewById(R.id.captcha); + final EditText input = view.findViewById(R.id.input); + imageView.setImageBitmap(captcha); - builder.setTitle(getString(R.string.captcha_required)); - builder.setView(view); + builder.setTitle(getString(R.string.captcha_required)); + builder.setView(view); - 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(); + 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); - } - }); - builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); + if (xmppConnectionServiceBound) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, id, data); + } + }); + builder.setNegativeButton( + getString(R.string.cancel), + (dialog, which) -> { + if (xmppConnectionService != null) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, null, null); + } + }); - builder.setOnCancelListener(dialog -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); - mCaptchaDialog = builder.create(); - mCaptchaDialog.show(); - input.requestFocus(); - }); + builder.setOnCancelListener( + dialog -> { + if (xmppConnectionService != null) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, null, null); + } + }); + mCaptchaDialog = builder.create(); + mCaptchaDialog.show(); + input.requestFocus(); + }); } public void onShowErrorToast(final int resId) { - runOnUiThread(() -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show()); + runOnUiThread( + () -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show()); } @Override public void onPreferencesFetched(final Element prefs) { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this); - builder.setTitle(R.string.server_side_mam_prefs); - String defaultAttr = prefs.getAttribute("default"); - final List defaults = Arrays.asList("never", "roster", "always"); - final AtomicInteger choice = new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr))); - builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - prefs.setAttribute("default", defaults.get(choice.get())); - xmppConnectionService.pushMamPreferences(mAccount, prefs); - }); - builder.create().show(); - }); + runOnUiThread( + () -> { + if (mFetchingMamPrefsToast != null) { + mFetchingMamPrefsToast.cancel(); + } + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(EditAccountActivity.this); + builder.setTitle(R.string.server_side_mam_prefs); + String defaultAttr = prefs.getAttribute("default"); + final List defaults = Arrays.asList("never", "roster", "always"); + final AtomicInteger choice = + new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr))); + builder.setSingleChoiceItems( + R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which)); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.ok, + (dialog, which) -> { + prefs.setAttribute("default", defaults.get(choice.get())); + xmppConnectionService.pushMamPreferences(mAccount, prefs); + }); + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + builder.create().show(); + } + }); } @Override public void onPreferencesFetchFailed() { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - Toast.makeText(EditAccountActivity.this, R.string.unable_to_fetch_mam_prefs, Toast.LENGTH_LONG).show(); - }); + runOnUiThread( + () -> { + if (mFetchingMamPrefsToast != null) { + mFetchingMamPrefsToast.cancel(); + } + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_fetch_mam_prefs, + Toast.LENGTH_LONG) + .show(); + }); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 6fd1c39e3e..0ef3625fdb 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -34,6 +34,7 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -363,10 +364,15 @@ public class RtpSessionActivity extends XmppActivity private void acceptContentAdd() { try { - requireRtpConnection() - .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary); + final ContentAddition pendingContentAddition = + requireRtpConnection().getPendingContentAddition(); + if (pendingContentAddition == null) { + Log.d(Config.LOGTAG, "content offer was gone after granting permission"); + return; + } + requireRtpConnection().acceptContentAdd(pendingContentAddition.summary); } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show(); } } @@ -537,7 +543,12 @@ public class RtpSessionActivity extends XmppActivity final String action = intent.getAction(); Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")"); final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); + final var extraWith = intent.getStringExtra(EXTRA_WITH); + final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.ofEscaped(extraWith); + if (with == null || account == null) { + Log.e(Config.LOGTAG, "intent is missing extras (account or with)"); + return; + } final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { @@ -1089,16 +1100,21 @@ public class RtpSessionActivity extends XmppActivity final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE -> { - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_volume_off_24dp); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp); if (numberOfChoices >= 2) { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker)); this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); } else { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_earpiece)); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } } case WIRED_HEADSET -> { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_wired_headset)); this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); @@ -1106,15 +1122,20 @@ public class RtpSessionActivity extends XmppActivity case SPEAKER_PHONE -> { this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp); if (numberOfChoices >= 2) { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece)); this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece); } else { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_speaker)); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } } case BLUETOOTH -> { - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_bluetooth_audio_24dp); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_bluetooth)); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } @@ -1131,15 +1152,21 @@ public class RtpSessionActivity extends XmppActivity R.drawable.ic_flip_camera_android_24dp); this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); + this.binding.inCallActionFarRight.setContentDescription( + getString(R.string.flip_camera)); } else { this.binding.inCallActionFarRight.setVisibility(View.GONE); } if (videoEnabled) { this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp); this.binding.inCallActionRight.setOnClickListener(this::disableVideo); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.video_is_enabled_tap_to_disable)); } else { this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp); this.binding.inCallActionRight.setOnClickListener(this::enableVideo); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.video_is_disabled_tap_to_enable)); } } @@ -1331,13 +1358,21 @@ public class RtpSessionActivity extends XmppActivity } private void switchToEarpiece(final View view) { - requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); - acquireProximityWakeLock(); + try { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); + acquireProximityWakeLock(); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } private void switchToSpeaker(final View view) { - requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); - releaseProximityWakeLock(); + try { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); + releaseProximityWakeLock(); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } private void retry(final View view) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 4e707ab89e..013c31e21f 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -39,7 +39,6 @@ import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ListView; -import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -50,7 +49,7 @@ import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; +import androidx.core.app.ActivityCompat; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -67,21 +66,11 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.leinardi.android.speeddial.SpeedDialActionItem; import com.leinardi.android.speeddial.SpeedDialView; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -113,11 +102,29 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; - private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent"; +public class StartConversationActivity extends XmppActivity + implements XmppConnectionService.OnConversationUpdate, + OnRosterUpdate, + OnUpdateBlocklist, + CreatePrivateGroupChatDialog.CreateConferenceDialogListener, + JoinConferenceDialog.JoinConferenceDialogListener, + SwipeRefreshLayout.OnRefreshListener, + CreatePublicChannelDialog.CreatePublicChannelDialogListener { + + private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = + "contact_list_integration_consent"; public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri"; @@ -139,126 +146,138 @@ public class StartConversationActivity extends XmppActivity implements XmppConne private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); private boolean mHideOfflineContacts = false; private boolean createdByViewIntent = false; - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + private final MenuItem.OnActionExpandListener mOnActionExpandListener = + new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(() -> { - updateSearchViewHint(); - mSearchEditText.requestFocus(); - if (oneShotKeyboardSuppress.compareAndSet(true, false)) { - return; + @Override + public boolean onMenuItemActionExpand(@NonNull final MenuItem item) { + mSearchEditText.post( + () -> { + updateSearchViewHint(); + mSearchEditText.requestFocus(); + if (oneShotKeyboardSuppress.compareAndSet(true, false)) { + return; + } + InputMethodManager imm = + (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput( + mSearchEditText, InputMethodManager.SHOW_IMPLICIT); + } + }); + if (binding.speedDial.isOpen()) { + binding.speedDial.close(); + } + return true; } - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); + + @Override + public boolean onMenuItemActionCollapse(@NonNull final MenuItem item) { + SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); + mSearchEditText.setText(""); + filter(null); + navigateBack(); + return true; } - }); - if (binding.speedDial.isOpen()) { - binding.speedDial.close(); - } - return true; - } + }; + private final TextWatcher mSearchTextWatcher = + new TextWatcher() { - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mSearchEditText.setText(""); - filter(null); - navigateBack(); - return true; - } - }; - private final TextWatcher mSearchTextWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + filter(editable.toString()); + } - @Override - public void afterTextChanged(Editable editable) { - filter(editable.toString()); - } + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - }; + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + }; private MenuItem mMenuSearchView; - private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { - @Override - public void onTagClicked(String tag) { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(tag); - filter(tag); - } - } - }; + private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = + new ListItemAdapter.OnTagClickedListener() { + @Override + public void onTagClicked(String tag) { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(tag); + filter(tag); + } + } + }; private Pair mPostponedActivityResult; private Toast mToast; - private final UiCallback mAdhocConferenceCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); - }); - } + private final UiCallback mAdhocConferenceCallback = + new UiCallback<>() { + @Override + public void success(final Conversation conversation) { + runOnUiThread( + () -> { + hideToast(); + switchToConversation(conversation); + }); + } - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } + @Override + public void error(final int errorCode, Conversation object) { + runOnUiThread(() -> replaceToast(getString(errorCode))); + } - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - }; + @Override + public void userInputRequired(PendingIntent pi, Conversation object) {} + }; private ActivityStartConversationBinding binding; - private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - int pos = binding.startConversationViewPager.getCurrentItem(); - if (pos == 0) { - if (contacts.size() == 1) { - openConversation(contacts.get(0)); - return true; - } else if (contacts.size() == 0 && conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); + private final TextView.OnEditorActionListener mSearchDone = + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + int pos = binding.startConversationViewPager.getCurrentItem(); + if (pos == 0) { + if (contacts.size() == 1) { + openConversation(contacts.get(0)); + return true; + } else if (contacts.isEmpty() && conferences.size() == 1) { + openConversationsForBookmark((Bookmark) conferences.get(0)); + return true; + } + } else { + if (conferences.size() == 1) { + openConversationsForBookmark((Bookmark) conferences.get(0)); + return true; + } else if (conferences.isEmpty() && contacts.size() == 1) { + openConversation(contacts.get(0)); + return true; + } + } + SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); + mListPagerAdapter.requestFocus(pos); return true; } - } else { - if (conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); - return true; - } else if (conferences.size() == 0 && contacts.size() == 1) { - openConversation(contacts.get(0)); - return true; - } - } - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mListPagerAdapter.requestFocus(pos); - return true; - } - }; + }; - public static void populateAccountSpinner(final Context context, final List accounts, final AutoCompleteTextView spinner) { + public static void populateAccountSpinner( + final Context context, + final List accounts, + final AutoCompleteTextView spinner) { if (accounts.isEmpty()) { - ArrayAdapter adapter = new ArrayAdapter<>(context, - R.layout.item_autocomplete, - Collections.singletonList(context.getString(R.string.no_accounts))); + ArrayAdapter adapter = + new ArrayAdapter<>( + context, + R.layout.item_autocomplete, + Collections.singletonList(context.getString(R.string.no_accounts))); adapter.setDropDownViewResource(R.layout.item_autocomplete); spinner.setAdapter(adapter); spinner.setEnabled(false); } else { - final ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts); + final ArrayAdapter adapter = + new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts); adapter.setDropDownViewResource(R.layout.item_autocomplete); spinner.setAdapter(adapter); spinner.setEnabled(true); - spinner.setText(Iterables.getFirst(accounts,null),false); + spinner.setText(Iterables.getFirst(accounts, null), false); } } @@ -275,7 +294,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private static boolean isViewIntent(final Intent i) { - return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI)); + return i != null + && (Intent.ACTION_VIEW.equals(i.getAction()) + || Intent.ACTION_SENDTO.equals(i.getAction()) + || i.hasExtra(EXTRA_INVITE_URI)); } protected void hideToast() { @@ -305,12 +327,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu); binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); - binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - updateSearchViewHint(); - } - }); + binding.startConversationViewPager.addOnPageChangeListener( + new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + updateSearchViewHint(); + } + }); mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); binding.startConversationViewPager.setAdapter(mListPagerAdapter); @@ -320,9 +343,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final SharedPreferences preferences = getPreferences(); - this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false); + this.mHideOfflineContacts = + QuickConversationsService.isConversations() + && preferences.getBoolean("hide_offline", false); - final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching)); + final boolean startSearching = + preferences.getBoolean( + "start_searching", getResources().getBoolean(R.bool.start_searching)); final Intent intent; if (savedInstanceState == null) { @@ -347,6 +374,66 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } else if (startSearching && mInitialSearchValue.peek() == null) { mInitialSearchValue.push(""); } + mRequestedContactsPermission.set( + savedInstanceState != null + && savedInstanceState.getBoolean("requested_contacts_permission", false)); + mOpenedFab.set( + savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false)); + binding.speedDial.setOnActionSelectedListener( + actionItem -> { + final String searchString = + mSearchEditText != null ? mSearchEditText.getText().toString() : null; + final String prefilled; + if (isValidJid(searchString)) { + prefilled = Jid.ofEscaped(searchString).toEscapedString(); + } else { + prefilled = null; + } + switch (actionItem.getId()) { + case R.id.discover_public_channels: + if (QuickConversationsService.isPlayStoreFlavor()) { + throw new IllegalStateException( + "Channel discovery is not available on Google Play flavor"); + } else { + startActivity(new Intent(this, ChannelDiscoveryActivity.class)); + } + break; + case R.id.create_private_group_chat: + showCreatePrivateGroupChatDialog(); + break; + case R.id.create_public_channel: + showPublicChannelDialog(); + break; + case R.id.create_contact: + showCreateContactDialog(prefilled, null); + break; + } + return false; + }); + + BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation); + bottomNavigationView.setOnItemSelectedListener(item -> { + + switch (item.getItemId()) { + case R.id.chats -> { + startActivity(new Intent(getApplicationContext(), ConversationsActivity.class)); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + return true; + } + case R.id.contactslist -> { + return true; + } + case R.id.manageaccounts -> { + Intent i = new Intent(getApplicationContext(), MANAGE_ACCOUNT_ACTIVITY); + i.putExtra("show_nav_bar", true); + startActivity(i); + overridePendingTransition(R.animator.fade_in, R.animator.fade_out); + return true; + } + default -> + throw new IllegalStateException("Unexpected value: " + item.getItemId()); + } + }); mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false)); mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false)); binding.speedDial.setOnActionSelectedListener(actionItem -> { @@ -410,16 +497,29 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final Menu menu = popupMenu.getMenu(); for (int i = 0; i < menu.size(); i++) { final MenuItem menuItem = menu.getItem(i); - if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) { + if (QuickConversationsService.isPlayStoreFlavor() + && menuItem.getItemId() == R.id.discover_public_channels) { continue; } - final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) - .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null) - .setFabImageTintColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorOnSurface)) - .setFabBackgroundColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorSurfaceContainerHighest)) - .create(); + final SpeedDialActionItem actionItem = + new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) + .setLabel( + menuItem.getTitle() != null + ? menuItem.getTitle().toString() + : null) + .setFabImageTintColor( + MaterialColors.getColor( + speedDialView, + com.google.android.material.R.attr.colorOnSurface)) + .setFabBackgroundColor( + MaterialColors.getColor( + speedDialView, + com.google.android.material.R.attr + .colorSurfaceContainerHighest)) + .create(); speedDialView.addActionItem(actionItem); } + speedDialView.setContentDescription(getString(R.string.add_contact_or_create_or_join_group_chat)); } public static boolean isValidJid(String input) { @@ -434,12 +534,16 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override public void onSaveInstanceState(Bundle savedInstanceState) { Intent pendingIntent = pendingViewIntent.peek(); - savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent()); - savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get()); + savedInstanceState.putParcelable( + "intent", pendingIntent != null ? pendingIntent : getIntent()); + savedInstanceState.putBoolean( + "requested_contacts_permission", mRequestedContactsPermission.get()); savedInstanceState.putBoolean("opened_fab", mOpenedFab.get()); savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent); if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { - savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); + savedInstanceState.putString( + "search", + mSearchEditText != null ? mSearchEditText.getText().toString() : null); } super.onSaveInstanceState(savedInstanceState); } @@ -447,11 +551,24 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override public void onStart() { super.onStart(); - if (pendingViewIntent.peek() == null) { - askForContactsPermissions(); - } mConferenceAdapter.refreshSettings(); mContactsAdapter.refreshSettings(); + if (pendingViewIntent.peek() == null) { + if (askForContactsPermissions()) { + return; + } + requestNotificationPermissionIfNeeded(); + } + } + + private void requestNotificationPermissionIfNeeded() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_POST_NOTIFICATION); + } BottomNavigationView bottomNavigationView=findViewById(R.id.bottom_navigation); bottomNavigationView.setSelectedItemId(R.id.contactslist); @@ -487,7 +604,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void openConversationForContact(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); SoftKeyboardUtils.hideSoftKeyboard(this); switchToConversation(conversation); } @@ -512,9 +631,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + Uri.encode(address, "@/+") + "?join"); shareIntent.setType("text/plain"); try { - context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); + context.startActivity( + Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); } catch (ActivityNotFoundException e) { - Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT) + .show(); } } @@ -524,7 +645,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); return; } - final Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + bookmark.getAccount(), jid, true, true, true); bookmark.setConversation(conversation); if (!bookmark.autojoin()) { bookmark.setAutojoin(true); @@ -551,11 +674,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.action_delete_contact); - builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString())); - builder.setPositiveButton(R.string.delete, (dialog, which) -> { - xmppConnectionService.deleteContactOnServer(contact); - filter(mSearchEditText.getText().toString()); - }); + builder.setMessage( + JidDialog.style( + this, R.string.remove_contact_text, contact.getJid().toEscapedString())); + builder.setPositiveButton( + R.string.delete, + (dialog, which) -> { + xmppConnectionService.deleteContactOnServer(contact); + filter(mSearchEditText.getText().toString()); + }); builder.create().show(); } @@ -567,21 +694,28 @@ public class StartConversationActivity extends XmppActivity implements XmppConne builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_bookmark); if (hasConversation) { - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString())); + builder.setMessage( + JidDialog.style( + this, + R.string.remove_bookmark_and_close, + bookmark.getJid().toEscapedString())); } else { - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString())); + builder.setMessage( + JidDialog.style( + this, R.string.remove_bookmark, bookmark.getJid().toEscapedString())); } - builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> { - bookmark.setConversation(null); - final Account account = bookmark.getAccount(); - xmppConnectionService.deleteBookmark(account, bookmark); - if (conversation != null) { - xmppConnectionService.archiveConversation(conversation); - } - filter(mSearchEditText.getText().toString()); - }); + builder.setPositiveButton( + hasConversation ? R.string.delete_and_close : R.string.delete, + (dialog, which) -> { + bookmark.setConversation(null); + final Account account = bookmark.getAccount(); + xmppConnectionService.deleteBookmark(account, bookmark); + if (conversation != null) { + xmppConnectionService.archiveConversation(conversation); + } + filter(mSearchEditText.getText().toString()); + }); builder.create().show(); - } @SuppressLint("InflateParams") @@ -678,7 +812,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts); + JoinConferenceDialog joinConferenceFragment = + JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts); joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } @@ -689,7 +824,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts); + CreatePrivateGroupChatDialog createConferenceFragment = + CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts); createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } @@ -700,11 +836,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts); + CreatePublicChannelDialog dialog = + CreatePublicChannelDialog.newInstance(mActivatedAccounts); dialog.show(ft, FRAGMENT_TAG_DIALOG); } - public static Account getSelectedAccount(final Context context, final AutoCompleteTextView spinner) { + public static Account getSelectedAccount( + final Context context, final AutoCompleteTextView spinner) { if (spinner == null || !spinner.isEnabled()) { return null; } @@ -726,7 +864,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void switchToConversation(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation); } @@ -735,7 +875,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation, body, false, null, false, true, postInit); } @@ -881,7 +1023,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne this.mPostponedActivityResult = null; if (requestCode == REQUEST_CREATE_CONFERENCE) { Account account = extractAccount(intent); - final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME); + final String name = + intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME); final List jids = ChooseContactActivity.extractJabberIds(intent); if (account != null && jids.size() > 0) { // This hardcodes cheogram.com and is in general a terrible hack @@ -916,104 +1059,109 @@ public class StartConversationActivity extends XmppActivity implements XmppConne super.onActivityResult(requestCode, requestCode, intent); } - private void askForContactsPermissions() { - if (QuickConversationsService.isContactListIntegration(this)) { - if (checkSelfPermission(Manifest.permission.READ_CONTACTS) - != PackageManager.PERMISSION_GRANTED) { - if (mRequestedContactsPermission.compareAndSet(false, true)) { - final String consent = - PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) - .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null); - final boolean requiresConsent = - (QuickConversationsService.isQuicksy() - || QuickConversationsService.isPlayStoreFlavor()) - && !"agreed".equals(consent); - if (requiresConsent && "declined".equals(consent)) { - Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined"); - return; - } - if (requiresConsent - || shouldShowRequestPermissionRationale( - Manifest.permission.READ_CONTACTS)) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - final AtomicBoolean requestPermission = new AtomicBoolean(false); - if (QuickConversationsService.isQuicksy()) { - builder.setTitle(R.string.quicksy_wants_your_consent); - builder.setMessage( - Html.fromHtml( - getString(R.string.sync_with_contacts_quicksy_static))); - } else { - builder.setTitle(R.string.sync_with_contacts); - builder.setMessage( - getString( - R.string.sync_with_contacts_long, - getString(R.string.app_name))); - } - @StringRes int confirmButtonText; - if (requiresConsent) { - confirmButtonText = R.string.agree_and_continue; - } else { - confirmButtonText = R.string.next; - } - builder.setPositiveButton( - confirmButtonText, - (dialog, which) -> { - if (requiresConsent) { - PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()) - .edit() - .putString( - PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed") - .apply(); - } - if (requestPermission.compareAndSet(false, true)) { - requestPermissions( - new String[] {Manifest.permission.READ_CONTACTS}, - REQUEST_SYNC_CONTACTS); - } - }); - if (requiresConsent) { - builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()) - .edit() - .putString( - PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined") - .apply()); - } else { - builder.setOnDismissListener( - dialog -> { - if (requestPermission.compareAndSet(false, true)) { - requestPermissions( - new String[] { - Manifest.permission.READ_CONTACTS - }, - REQUEST_SYNC_CONTACTS); - } - }); - } - builder.setCancelable(requiresConsent); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(requiresConsent); - dialog.setOnShowListener( - dialogInterface -> { - final TextView tv = dialog.findViewById(android.R.id.message); - if (tv != null) { - tv.setMovementMethod(LinkMovementMethod.getInstance()); - } - }); - dialog.show(); - } else { - requestPermissions( - new String[] {Manifest.permission.READ_CONTACTS}, - REQUEST_SYNC_CONTACTS); - } + private boolean askForContactsPermissions() { + if (!QuickConversationsService.isContactListIntegration(this)) { + return false; + } + if (checkSelfPermission(Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED) { + return false; + } + if (mRequestedContactsPermission.compareAndSet(false, true)) { + final ImmutableList.Builder permissionBuilder = new ImmutableList.Builder<>(); + permissionBuilder.add(Manifest.permission.READ_CONTACTS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS); + } + final String[] permission = permissionBuilder.build().toArray(new String[0]); + final String consent = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) + .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null); + final boolean requiresConsent = + (QuickConversationsService.isQuicksy() + || QuickConversationsService.isPlayStoreFlavor()) + && !"agreed".equals(consent); + if (requiresConsent && "declined".equals(consent)) { + Log.d( + Config.LOGTAG, + "not asking for contacts permission because consent has been declined"); + return false; + } + if (requiresConsent + || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + final AtomicBoolean requestPermission = new AtomicBoolean(false); + if (QuickConversationsService.isQuicksy()) { + builder.setTitle(R.string.quicksy_wants_your_consent); + builder.setMessage( + Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static))); + } else { + builder.setTitle(R.string.sync_with_contacts); + builder.setMessage( + getString( + R.string.sync_with_contacts_long, + getString(R.string.app_name))); } + @StringRes int confirmButtonText; + if (requiresConsent) { + confirmButtonText = R.string.agree_and_continue; + } else { + confirmButtonText = R.string.next; + } + builder.setPositiveButton( + confirmButtonText, + (dialog, which) -> { + if (requiresConsent) { + PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed") + .apply(); + } + if (requestPermission.compareAndSet(false, true)) { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); + } + }); + if (requiresConsent) { + builder.setNegativeButton( + R.string.decline, + (dialog, which) -> + PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString( + PREF_KEY_CONTACT_INTEGRATION_CONSENT, + "declined") + .apply()); + } else { + builder.setOnDismissListener( + dialog -> { + if (requestPermission.compareAndSet(false, true)) { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); + } + }); + } + builder.setCancelable(requiresConsent); + final AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(requiresConsent); + dialog.setOnShowListener( + dialogInterface -> { + final TextView tv = dialog.findViewById(android.R.id.message); + if (tv != null) { + tv.setMovementMethod(LinkMovementMethod.getInstance()); + } + }); + dialog.show(); + } else { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); } } + return true; } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -1033,11 +1181,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (actionBar == null) { return; } - boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null); - boolean showNavBar = binding.bottomNavigation.getVisibility() == VISIBLE; - actionBar.setDisplayHomeAsUpEnabled(openConversations && !showNavBar); - actionBar.setDisplayHomeAsUpEnabled(openConversations && !showNavBar); - + boolean openConversations = + !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null); + actionBar.setDisplayHomeAsUpEnabled(openConversations); + actionBar.setDisplayHomeAsUpEnabled(openConversations); } @Override @@ -1049,7 +1196,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne xmppConnectionService.getQuickConversationsService().considerSyncBackground(false); } if (mPostponedActivityResult != null) { - onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); + onActivityResult( + mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); this.mPostponedActivityResult = null; } this.mActivatedAccounts.clear(); @@ -1118,7 +1266,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (QuickConversationsService.isQuicksy()) { setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing()); } - if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) { + if (QuickConversationsService.isConversations() + && AccountUtils.hasEnabledAccounts(xmppConnectionService) + && this.contacts.size() == 0 + && this.conferences.size() == 0 + && mOpenedFab.compareAndSet(false, true)) { binding.speedDial.open(); } } @@ -1141,7 +1293,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne case Intent.ACTION_VIEW: Uri uri = intent.getData(); if (uri != null) { - Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); + Invite invite = + new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); invite.account = intent.getStringExtra(EXTRA_ACCOUNT); invite.forceDialog = intent.getBooleanExtra("force_dialog", false); return invite.invite(); @@ -1153,7 +1306,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private boolean handleJid(Invite invite) { - final List contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account); + List contacts = + xmppConnectionService.findContacts(invite.getJid(), invite.account); final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account); if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) { if (muc != null && !invite.forceDialog) { @@ -1175,8 +1329,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne displayVerificationWarningDialog(contact, invite); } else { if (invite.hasFingerprints()) { - if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) { - Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show(); + if (xmppConnectionService.verifyFingerprints( + contact, invite.getFingerprints())) { + Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT) + .show(); } } if (invite.account != null) { @@ -1204,15 +1360,23 @@ public class StartConversationActivity extends XmppActivity implements XmppConne View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null); final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source); TextView warning = view.findViewById(R.id.warning); - warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName())); + warning.setText( + JidDialog.style( + this, + R.string.verifying_omemo_keys_trusted_source, + contact.getJid().asBareJid().toEscapedString(), + contact.getDisplayName())); builder.setView(view); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (isTrustedSource.isChecked() && invite.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); - } - switchToConversationDoNotAppend(contact, invite.getBody()); - }); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish()); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + if (isTrustedSource.isChecked() && invite.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); + } + switchToConversationDoNotAppend(contact, invite.getBody()); + }); + builder.setNegativeButton( + R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish()); AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish()); @@ -1235,10 +1399,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (account.isEnabled()) { for (Contact contact : account.getRoster().getContacts()) { Presence.Status s = contact.getShownStatus(); - if (contact.showInContactList() && contact.match(this, needle) + if (contact.showInContactList() + && contact.match(this, needle) && (!this.mHideOfflineContacts - || (needle != null && !needle.trim().isEmpty()) - || s.compareTo(Presence.Status.OFFLINE) < 0)) { + || (needle != null && !needle.trim().isEmpty()) + || s.compareTo(Presence.Status.OFFLINE) < 0)) { this.contacts.add(contact); tags.addAll(contact.getTags(this)); } @@ -1330,7 +1495,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private void navigateBack() { - if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) { + if (!createdByViewIntent + && xmppConnectionService != null + && !xmppConnectionService.isConversationsListEmpty(null)) { Intent intent = new Intent(this, ConversationsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); startActivity(intent); @@ -1354,13 +1521,20 @@ public class StartConversationActivity extends XmppActivity implements XmppConne intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false); intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true); intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim()); - intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + intent.putExtra( + ChooseContactActivity.EXTRA_ACCOUNT, + account.getJid().asBareJid().toEscapedString()); intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants); startActivityForResult(intent, REQUEST_CREATE_CONFERENCE); } @Override - public void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout layout, AutoCompleteTextView jid, String password) { + public void onJoinDialogPositiveClick( + final Dialog dialog, + final AutoCompleteTextView spinner, + final TextInputLayout layout, + final AutoCompleteTextView jid, + final String password) { if (!xmppConnectionServiceBound) { return; } @@ -1395,9 +1569,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne bookmark.setNick(nick); } xmppConnectionService.createBookmark(account, bookmark); - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, conferenceJid, true, true, null, true, password); - + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + account, conferenceJid, true, true, null, true, password); bookmark.setConversation(conversation); switchToConversation(conversation); } @@ -1417,7 +1591,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } } - private void setRefreshing(boolean refreshing) { MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0); if (fragment != null) { @@ -1429,29 +1602,32 @@ public class StartConversationActivity extends XmppActivity implements XmppConne public void onCreatePublicChannel(Account account, String name, Jid address) { mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG); mToast.show(); - xmppConnectionService.createPublicChannel(account, name, address, new UiCallback() { - @Override - public void success(Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); + xmppConnectionService.createPublicChannel( + account, + name, + address, + new UiCallback() { + @Override + public void success(Conversation conversation) { + runOnUiThread( + () -> { + hideToast(); + switchToConversation(conversation); + }); + } + + @Override + public void error(int errorCode, Conversation conversation) { + runOnUiThread( + () -> { + replaceToast(getString(errorCode)); + switchToConversation(conversation); + }); + } + + @Override + public void userInputRequired(PendingIntent pi, Conversation object) {} }); - - } - - @Override - public void error(int errorCode, Conversation conversation) { - runOnUiThread(() -> { - replaceToast(getString(errorCode)); - switchToConversation(conversation); - }); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - }); } public static class MyListFragment extends SwipeRefreshListFragment { @@ -1463,7 +1639,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onListItemClick(final ListView l, final View v, final int position, final long id) { + public void onListItemClick( + final ListView l, final View v, final int position, final long id) { if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick(l, v, position, id); } @@ -1483,7 +1660,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onCreateContextMenu(@NonNull final ContextMenu menu, @NonNull final View v, final ContextMenuInfo menuInfo) { + public void onCreateContextMenu( + @NonNull final ContextMenu menu, + @NonNull final View v, + final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); final StartConversationActivity activity = (StartConversationActivity) getActivity(); if (activity == null) { @@ -1517,7 +1697,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (contact.isSelf()) { showContactDetailsItem.setVisible(false); } - deleteContactMenuItem.setVisible(contact.showInRoster() && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER)); + deleteContactMenuItem.setVisible( + contact.showInRoster() + && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER)); final XmppConnection xmpp = contact.getAccount().getXmppConnection(); if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) { if (contact.isBlocked()) { @@ -1576,7 +1758,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + public void destroyItem( + @NonNull ViewGroup container, int position, @NonNull Object object) { FragmentTransaction trans = fragmentManager.beginTransaction(); trans.remove(fragments[position]); trans.commit(); @@ -1592,7 +1775,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne try { trans.commit(); } catch (IllegalStateException e) { - //ignore + // ignore } return fragment; } @@ -1626,11 +1809,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (position == 1) { listFragment.setListAdapter(mConferenceAdapter); listFragment.setContextMenu(R.menu.conference_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p)); + listFragment.setOnListItemClickListener( + (arg0, arg1, p, arg3) -> openConversationForBookmark(p)); } else { listFragment.setListAdapter(mContactsAdapter); listFragment.setContextMenu(R.menu.contact_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p)); + listFragment.setOnListItemClickListener( + (arg0, arg1, p, arg3) -> openConversationForContact(p)); if (QuickConversationsService.isQuicksy()) { listFragment.setOnRefreshListener(StartConversationActivity.this); } @@ -1654,7 +1839,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne boolean forceDialog = false; - Invite(final String uri) { super(uri); } @@ -1665,7 +1849,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne boolean invite() { if (!isValidJid()) { - Toast.makeText(StartConversationActivity.this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); + Toast.makeText( + StartConversationActivity.this, + R.string.invalid_jid, + Toast.LENGTH_SHORT) + .show(); return false; } if (getJid() != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index 494771df14..00d867fba6 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -68,9 +68,7 @@ public class UriHandlerActivity extends BaseActivity { } public static void scan(final Activity activity, final boolean provisioning) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { final Intent intent = new Intent(activity, UriHandlerActivity.class); intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); if (provisioning) { @@ -114,6 +112,7 @@ public class UriHandlerActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); } @Override @@ -187,7 +186,7 @@ public class UriHandlerActivity extends BaseActivity { startActivity(intent); return true; } - if (accounts.size() == 0 + if (accounts.isEmpty() && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y" .equalsIgnoreCase( @@ -203,7 +202,7 @@ public class UriHandlerActivity extends BaseActivity { return false; } - if (accounts.size() == 0) { + if (accounts.isEmpty()) { if (xmppUri.isValidJid()) { intent = SignupUtils.getSignUpIntent(this); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); @@ -259,14 +258,14 @@ public class UriHandlerActivity extends BaseActivity { private void checkForLinkHeader(final HttpUrl url) { Log.d(Config.LOGTAG, "checking for link header on " + url); this.call = - HttpConnectionManager.OK_HTTP_CLIENT.newCall( + HttpConnectionManager.okHttpClient(this).newCall( new Request.Builder().url(url).head().build()); this.call.enqueue( new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d(Config.LOGTAG, "unable to check HTTP url", e); - showError(R.string.no_xmpp_adddress_found); + showErrorOnUiThread(R.string.no_xmpp_adddress_found); } @Override @@ -277,7 +276,7 @@ public class UriHandlerActivity extends BaseActivity { return; } } - showError(R.string.no_xmpp_adddress_found); + showErrorOnUiThread(R.string.no_xmpp_adddress_found); } }); } @@ -301,6 +300,10 @@ public class UriHandlerActivity extends BaseActivity { this.binding.error.setVisibility(View.VISIBLE); } + private void showErrorOnUiThread(@StringRes int error) { + runOnUiThread(()-> showError(error)); + } + private static Class findShareViaAccountClass() { try { return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 42ab5ecd1b..fd62176005 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -523,14 +523,9 @@ public abstract class XmppActivity extends ActionBarActivity { } protected boolean isOptimizingBattery() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); - return pm != null - && !pm.isIgnoringBatteryOptimizations(getPackageName()); - } else { - return false; - } - } + final PowerManager pm = getSystemService(PowerManager.class); + return !pm.isIgnoringBatteryOptimizations(getPackageName()); +} protected boolean isAffectedByDataSaver() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/:w b/src/main/java/eu/siacs/conversations/ui/adapter/:w new file mode 100644 index 0000000000..b033660fd7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/:w @@ -0,0 +1,1701 @@ +package eu.siacs.conversations.ui.adapter; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.Spanned; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import android.text.style.ClickableSpan; +import android.text.format.DateUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.text.style.URLSpan; +import android.util.DisplayMetrics; +import android.util.LruCache; +import android.view.accessibility.AccessibilityEvent; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.widget.ImageViewCompat; + +import com.google.android.material.imageview.ShapeableImageView; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.ShapeAppearanceModel; + +import com.cheogram.android.BobTransfer; +import com.cheogram.android.MessageTextActionModeCallback; +import com.cheogram.android.SwipeDetector; +import com.cheogram.android.WebxdcPage; +import com.cheogram.android.WebxdcUpdate; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.color.MaterialColors; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import com.lelloman.identicon.view.GithubIdenticonView; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.ipfs.cid.Cid; + +import me.saket.bettermovementmethod.BetterLinkMovementMethod; + +import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message.FileParams; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.entities.RtpSessionStatus; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.MessageArchiveService; +import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.ui.Activities; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.service.AudioPlayer; +import eu.siacs.conversations.ui.text.DividerSpan; +import eu.siacs.conversations.ui.text.QuoteSpan; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.ui.util.AvatarWorkerTask; +import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.util.QuoteHelper; +import eu.siacs.conversations.ui.util.ShareUtil; +import eu.siacs.conversations.ui.util.ViewUtil; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.Emoticons; +import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.utils.MessageUtils; +import eu.siacs.conversations.utils.StylingHelper; +import eu.siacs.conversations.utils.TimeFrameUtils; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.mam.MamReference; +import eu.siacs.conversations.xml.Element; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MessageAdapter extends ArrayAdapter { + + public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int DATE_SEPARATOR = 3; + private static final int RTP_SESSION = 4; + private final XmppActivity activity; + private final AudioPlayer audioPlayer; + private List highlightedTerm = null; + private final DisplayMetrics metrics; + private ConversationFragment mConversationFragment = null; + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureClicked mOnMessageBoxClickedListener; + private OnContactPictureClicked mOnMessageBoxSwipedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + private OnInlineImageLongClicked mOnInlineImageLongClickedListener; + private boolean mUseGreenBackground = false; + private BubbleDesign bubbleDesign = new BubbleDesign(false, false); + private final boolean mForceNames; + private final Map lastWebxdcUpdate = new HashMap<>(); + private String selectionUuid = null; + + public MessageAdapter( + final XmppActivity activity, final List messages, final boolean forceNames) { + super(activity, 0, messages); + this.audioPlayer = new AudioPlayer(this); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + updatePreferences(); + this.mForceNames = forceNames; + } + + public MessageAdapter(final XmppActivity activity, final List messages) { + this(activity, messages, false); + } + + private static void resetClickListener(View... views) { + for (View view : views) { + if (view != null) view.setOnClickListener(null); + } + } + + public void flagScreenOn() { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void flagScreenOff() { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void setVolumeControl(final int stream) { + activity.setVolumeControlStream(stream); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public void setOnMessageBoxClicked(OnContactPictureClicked listener) { + this.mOnMessageBoxClickedListener = listener; + } + + public void setOnMessageBoxSwiped(OnContactPictureClicked listener) { + this.mOnMessageBoxSwipedListener = listener; + } + + public void setConversationFragment(ConversationFragment frag) { + mConversationFragment = frag; + } + + public void quoteText(String text) { + if (mConversationFragment != null) mConversationFragment.quoteText(text); + } + + public boolean hasSelection() { + return selectionUuid != null; + } + + public Activity getActivity() { + return activity; + } + + public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) { + this.mOnInlineImageLongClickedListener = listener; + } + + @Override + public int getViewTypeCount() { + return 5; + } + + private int getItemViewType(Message message) { + if (message.getType() == Message.TYPE_STATUS) { + if (DATE_SEPARATOR_BODY.equals(message.getBody())) { + return DATE_SEPARATOR; + } else { + return STATUS; + } + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return RTP_SESSION; + } else if (message.getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + @Override + public int getItemViewType(int position) { + return this.getItemViewType(getItem(position)); + } + + private void displayStatus( + final ViewHolder viewHolder, + final Message message, + final int type, + final BubbleColor bubbleColor) { + final int mergedStatus = message.getMergedStatus(); + final boolean error; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + final Transferable transferable = message.getTransferable(); + final boolean multiReceived = + message.getConversation().getMode() == Conversation.MODE_MULTI + && mergedStatus <= Message.STATUS_RECEIVED; + final String fileSize; + if (message.isFileOrImage() + || transferable != null + || MessageUtils.unInitiatedButKnownSize(message)) { + final FileParams params = message.getFileParams(); + fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null; + if (message.getStatus() == Message.STATUS_SEND_FAILED + || (transferable != null + && (transferable.getStatus() == Transferable.STATUS_FAILED + || transferable.getStatus() + == Transferable.STATUS_CANCELLED))) { + error = true; + } else { + error = message.getStatus() == Message.STATUS_SEND_FAILED; + } + } else { + fileSize = null; + error = message.getStatus() == Message.STATUS_SEND_FAILED; + } + if (type == SENT) { + final @DrawableRes Integer receivedIndicator = + getMessageStatusAsDrawable(message, mergedStatus); + if (receivedIndicator == null) { + viewHolder.indicatorReceived.setVisibility(View.INVISIBLE); + } else { + viewHolder.indicatorReceived.setImageResource(receivedIndicator); + if (mergedStatus == Message.STATUS_SEND_FAILED) { + setImageTintError(viewHolder.indicatorReceived); + } else { + setImageTint(viewHolder.indicatorReceived, bubbleColor); + } + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + } + final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus); + + if (error && type == SENT) { + viewHolder.time.setTextColor( + MaterialColors.getColor( + viewHolder.time, com.google.android.material.R.attr.colorError)); + } else { + setTextColor(viewHolder.time, bubbleColor); + } + setTextColor(viewHolder.subject, bubbleColor); + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + boolean verified = false; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + final FingerprintStatus status = + message.getConversation() + .getAccount() + .getAxolotlService() + .getFingerprintTrust(message.getFingerprint()); + if (status != null && status.isVerified()) { + verified = true; + } + } + if (verified) { + viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp); + } else { + viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp); + } + if (error && type == SENT) { + setImageTintError(viewHolder.indicator); + } else { + setImageTint(viewHolder.indicator, bubbleColor); + } + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + if (viewHolder.edit_indicator != null) { + if (message.edited()) { + viewHolder.edit_indicator.setVisibility(View.VISIBLE); + if (error && type == SENT) { + setImageTintError(viewHolder.edit_indicator); + } else { + setImageTint(viewHolder.edit_indicator, bubbleColor); + } + } else { + viewHolder.edit_indicator.setVisibility(View.GONE); + } + } + + final String formattedTime = + UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + final String bodyLanguage = message.getBodyLanguage(); + final ImmutableList.Builder timeInfoBuilder = new ImmutableList.Builder<>(); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + timeInfoBuilder.add(formattedTime); + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) { + final String displayName = UIHelper.getMessageDisplayName(message); + if (displayName != null) { + timeInfoBuilder.add(displayName); + } + } + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + } else { + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + // for space reasons we display only 'additional status info' (send progress or concrete + // failure reason) or the time + if (additionalStatusInfo != null) { + timeInfoBuilder.add(additionalStatusInfo); + } else { + timeInfoBuilder.add(formattedTime); + } + } + final var timeInfo = timeInfoBuilder.build(); + viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo)); + } + + public static @DrawableRes Integer getMessageStatusAsDrawable( + final Message message, final int status) { + final var transferable = message.getTransferable(); + return switch (status) { + case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp; + case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp; + case Message.STATUS_SEND -> R.drawable.ic_done_24dp; + case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable + .ic_done_all_24dp; + case Message.STATUS_SEND_FAILED -> { + final String errorMessage = message.getErrorMessage(); + if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { + yield R.drawable.ic_cancel_24dp; + } else { + yield R.drawable.ic_error_24dp; + } + } + case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp; + default -> null; + }; + } + + @Nullable + private String getAdditionalStatusInfo(final Message message, final int mergedStatus) { + final String additionalStatusInfo; + if (mergedStatus == Message.STATUS_SEND_FAILED) { + final String errorMessage = Strings.nullToEmpty(message.getErrorMessage()); + final String[] errorParts = errorMessage.split("\\u001f", 2); + if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) { + additionalStatusInfo = getContext().getString(R.string.file_too_large); + } else { + additionalStatusInfo = null; + } + } else if (mergedStatus == Message.STATUS_UNSEND) { + final var transferable = message.getTransferable(); + if (transferable == null) { + return null; + } + return getContext().getString(R.string.sending_file, transferable.getProgress()); + } else { + additionalStatusInfo = null; + } + return additionalStatusInfo; + } + + private void displayInfoMessage( + ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(text); + viewHolder.messageBody.setTextColor( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayEmojiMessage( + final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + setTextColor(viewHolder.messageBody, bubbleColor); + ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class); + float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f; + body.setSpan( + new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + } + + private void applyQuoteSpan( + final TextView textView, + Editable body, + int start, + int end, + final BubbleColor bubbleColor, + final boolean makeEdits) { + if (makeEdits && start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan( + new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan( + new DividerSpan(false), + end, + end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan( + new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + public boolean handleTextQuotes(final TextView textView, final Editable body) { + return handleTextQuotes(textView, body, true); + } + + public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) { + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE; + return handleTextQuotes(textView, body, bubbleColor, deleteMarkers); + } + + /** + * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks + * and applies DividerSpan to them to show a padding between quote and text. + */ + public boolean handleTextQuotes( + final TextView textView, + final Editable body, + final BubbleColor bubbleColor, + final boolean deleteMarkers) { + boolean startsWithQuote = false; + int quoteDepth = 1; + while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + int skipped = 0; + for (int i = 0; i <= body.length(); i++) { + if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) { + skipped++; + continue; + } + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i - skipped; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + if (deleteMarkers) { + i -= lineTextStart - lineStart; + body.delete(lineStart, lineTextStart); + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + } else { + body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + skipped = 0; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers); + } + quoteDepth++; + } + return startsWithQuote; + } + + private SpannableStringBuilder getSpannableBody(final Message message) { + Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null); + return message.getMergedBody((cid) -> { + try { + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null || !f.canRead()) { + if (!message.trusted() && !message.getConversation().canInferPresence()) return null; + + try { + new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + return null; + } + + Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); + if (d == null) { + new ThumbnailTask().execute(f); + } + return d; + } catch (final IOException e) { + return null; + } + }, fallbackImg); + } + + private void displayTextMessage( + final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + setTextColor(viewHolder.messageBody, bubbleColor); + setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont); + + final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + viewHolder.messageBody.setLayoutParams(layoutParams); + + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + + if (message.getBody() != null && !message.getBody().equals("")) { + viewHolder.messageBody.setTextIsSelectable(true); + viewHolder.messageBody.setVisibility(View.VISIBLE); + final String nick = UIHelper.getMessageDisplayName(message); + SpannableStringBuilder body = getSpannableBody(message); + final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0; + boolean hasMeCommand = message.hasMeCommand(); + if (hasMeCommand) { + body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); + } + if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("\u2026"); + } + Message.MergeSeparator[] mergeSeparators = + body.getSpans(0, body.length(), Message.MergeSeparator.class); + for (Message.MergeSeparator mergeSeparator : mergeSeparators) { + int start = body.getSpanStart(mergeSeparator); + int end = body.getSpanEnd(mergeSeparator); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) { + int start = body.getSpanStart(quote); + int end = body.getSpanEnd(quote); + body.removeSpan(quote); + applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true); + } + boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false; + if (!message.isPrivateMessage()) { + if (hasMeCommand) { + body.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + 0, + nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = + activity.getString( + R.string.private_message_to, + Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + body.insert(0, privateMarker); + int privateMarkerIndex = privateMarker.length(); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan( + new DividerSpan(false), + privateMarkerIndex, + privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } + body.setSpan( + new ForegroundColorSpan( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), + 0, + privateMarkerIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan( + new StyleSpan(Typeface.BOLD), + 0, + privateMarkerIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hasMeCommand) { + body.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + privateMarkerIndex + 1, + privateMarkerIndex + 1 + nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation() instanceof Conversation conversation) { + Pattern pattern = + NotificationService.generateNickHighlightPattern( + conversation.getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan( + new StyleSpan(Typeface.BOLD), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName()); + matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); + while (matcher.find()) { + if (matcher.start() < matcher.end()) { + body.setSpan( + new RelativeSizeSpan(1.2f), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); + MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid()); + if (highlightedTerm != null) { + StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm); + } + + viewHolder.messageBody.setAutoLinkMask(0); + viewHolder.messageBody.setText(body); + BetterLinkMovementMethod method = new BetterLinkMovementMethod() { + @Override + protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) { + if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + super.dispatchUrlLongClick(tv, span); + return; + } + + Spannable body = (Spannable) tv.getText(); + ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class); + if (imageSpans.length > 0) { + Uri uri = Uri.parse(imageSpans[0].getSource()); + Cid cid = BobTransfer.cid(uri); + if (cid == null) return; + if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + } + } + } + }; + method.setOnLinkLongClickListener((tv, url) -> { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + ShareUtil.copyLinkToClipboard(activity, url); + return true; + }); + viewHolder.messageBody.setMovementMethod(method); + } else { + viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); + toggleWhisperInfo(viewHolder, message, bubbleColor); + } + } + + private void displayDownloadableMessage( + ViewHolder viewHolder, + final Message message, + String text, + final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + if (thumbs != null && !thumbs.isEmpty()) { + for (Element thumb : thumbs) { + Uri uri = Uri.parse(thumb.getAttribute("uri")); + if (uri.getScheme().equals("data")) { + String[] parts = uri.getSchemeSpecificPart().split(",", 2); + parts = parts[0].split(";"); + if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue; + } else if (uri.getScheme().equals("cid")) { + Cid cid = BobTransfer.cid(uri); + if (cid == null) continue; + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null || !f.canRead()) { + if (!message.trusted() && !message.getConversation().canInferPresence()) continue; + + try { + new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + continue; + } + } else { + continue; + } + + int width = message.getFileParams().width; + if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width")); + if (width < 1) width = 1920; + + int height = message.getFileParams().height; + if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height")); + if (height < 1) height = 1080; + + viewHolder.image.setVisibility(View.VISIBLE); + imagePreviewLayout(width, height, viewHolder.image, true, type, viewHolder); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); + + break; + } + } + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(text); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener( + v -> ConversationFragment.downloadFile(activity, message)); + } + + private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + Cid webxdcCid = message.getFileParams().getCids().get(0); + WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService); + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText("Open " + webxdc.getName()); + viewHolder.download_button.setOnClickListener(v -> { + Conversation conversation = (Conversation) message.getConversation(); + if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { + conversation.startWebxdc(webxdc); + } + }); + + final WebxdcUpdate lastUpdate; + synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); } + if (lastUpdate == null) { + new Thread(() -> { + final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message); + if (update != null) { + synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); } + activity.xmppConnectionService.updateConversationUi(); + } + }).start(); + } else { + if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) { + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText( + (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") + + (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary()) + ); + } + } + + final LruCache cache = activity.xmppConnectionService.getDrawableCache(); + final Drawable d = cache.get("webxdc:icon:" + webxdcCid); + if (d == null) { + new Thread(() -> { + Drawable icon = webxdc.getIcon(); + if (icon != null) { + cache.put("webxdc:icon:" + webxdcCid, icon); + activity.xmppConnectionService.updateConversationUi(); + } + }).start(); + } else { + viewHolder.image.setVisibility(View.VISIBLE); + viewHolder.image.setImageDrawable(d); + } + } + + private void displayOpenableMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText( + activity.getString( + R.string.open_x_file, + UIHelper.getFileDescriptionString(activity, message))); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); + } + + private void displayLocationMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener(v -> showLocation(message)); + } + + private void displayAudioMessage( + ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.GONE); + final RelativeLayout audioPlayer = viewHolder.audioPlayer; + audioPlayer.setVisibility(View.VISIBLE); + AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor); + this.audioPlayer.init(audioPlayer, message); + } + + private void displayMediaPreviewMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + final FileParams params = message.getFileParams(); + imagePreviewLayout(params.width, params.height, viewHolder.image, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> openDownloadable(message)); + } + + private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean withOther, int type, ViewHolder viewHolder) { + final float target = activity.getResources().getDimension(R.dimen.image_preview_width); + final int scaledW; + final int scaledH; + if (Math.max(h, w) * metrics.density <= target) { + scaledW = (int) (w * metrics.density); + scaledH = (int) (h * metrics.density); + } else if (Math.max(h, w) <= target) { + scaledW = w; + scaledH = h; + } else if (w <= h) { + scaledW = (int) (w / ((double) h / target)); + scaledH = (int) target; + } else { + scaledW = (int) target; + scaledH = (int) (h / ((double) w / target)); + } + final var small = withOther ? scaledW < target : scaledW < 110 * metrics.density; + final LinearLayout.LayoutParams layoutParams = + new LinearLayout.LayoutParams(scaledW, scaledH); + image.setLayoutParams(layoutParams); + + final var bubbleRadius = activity.getResources().getDimension(R.dimen.bubble_radius); + var shape = new ShapeAppearanceModel.Builder().setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius); + if (type == SENT) { + shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius); + } + if (small) { + final var imageRadius = activity.getResources().getDimension(R.dimen.image_radius); + shape = shape.setAllCorners(CornerFamily.ROUNDED, imageRadius); + image.setPadding(0, (int)(8 * metrics.density), 0, 0); + } else { + image.setPadding(0, 0, 0, 0); + } + image.setShapeAppearanceModel(shape.build()); + + if (!small) { + final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams(); + blayoutParams.width = (int) (target - (22 * metrics.density)); + viewHolder.messageBody.setLayoutParams(blayoutParams); + } + } + + private void toggleWhisperInfo( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { + if (message.isPrivateMessage()) { + final String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = + activity.getString( + R.string.private_message_to, + Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + final SpannableString body = new SpannableString(privateMarker); + body.setSpan( + new ForegroundColorSpan( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), + 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan( + new StyleSpan(Typeface.BOLD), + 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + viewHolder.messageBody.setVisibility(View.VISIBLE); + } else { + viewHolder.messageBody.setVisibility(View.GONE); + } + } + + private void loadMoreMessages(Conversation conversation) { + conversation.setLastClearHistory(0, null); + activity.xmppConnectionService.updateConversation(conversation); + conversation.setHasMessagesLeftOnServer(true); + conversation.setFirstMamReference(null); + long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); + if (timestamp == 0) { + timestamp = System.currentTimeMillis(); + } + conversation.messagesLoaded.set(true); + MessageArchiveService.Query query = + activity.xmppConnectionService + .getMessageArchiveService() + .query(conversation, new MamReference(0), timestamp, false); + if (query != null) { + Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG) + .show(); + } else { + Toast.makeText( + activity, + R.string.not_fetching_history_retention_period, + Toast.LENGTH_SHORT) + .show(); + } + } + + @Override + public View getView(final int position, View view, final @NonNull ViewGroup parent) { + final Message message = getItem(position); + final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; + final boolean isInValidSession = + message.isValidInSession() && (!omemoEncryption || message.isTrusted()); + final Conversational conversation = message.getConversation(); + final Account account = conversation.getAccount(); + final List commands = message.getCommands(); + final int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case DATE_SEPARATOR: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_date_bubble, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + break; + case RTP_SESSION: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_rtp_session, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + break; + case SENT: + view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false); + viewHolder.status_line = view.findViewById(R.id.status_line); + viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.subject = view.findViewById(R.id.message_subject); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + break; + case RECEIVED: + view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false); + viewHolder.status_line = view.findViewById(R.id.status_line); + viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.subject = view.findViewById(R.id.message_subject); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.encryption = view.findViewById(R.id.message_encryption); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.commands_list = view.findViewById(R.id.commands_list); + viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + break; + case STATUS: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_status, parent, false); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.status_message = view.findViewById(R.id.status_message); + viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); + break; + default: + throw new AssertionError("Unknown view type"); + } + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + if (viewHolder == null) { + return view; + } + } + + if (viewHolder.messageBody != null) { + viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody)); + } + + if (viewHolder.thread_identicon != null) { + viewHolder.thread_identicon.setVisibility(View.GONE); + final Element thread = message.getThread(); + if (thread != null) { + final String threadId = thread.getContent(); + if (threadId != null) { + viewHolder.thread_identicon.setVisibility(View.VISIBLE); + viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId)); + viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId)); + } + } + } + + final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black); + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final BubbleColor bubbleColor; + if (type == RECEIVED) { + if (isInValidSession) { + bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE; + } else { + bubbleColor = BubbleColor.WARNING; + } + } else { + if (!colorfulBackground && black) { + bubbleColor = BubbleColor.SECONDARY; + } else { + bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH; + } + } + + if (type == DATE_SEPARATOR) { + if (UIHelper.today(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.today); + } else if (UIHelper.yesterday(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.yesterday); + } else { + viewHolder.status_message.setText( + DateUtils.formatDateTime( + activity, + message.getTimeSent(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY); + setTextColor(viewHolder.status_message, BubbleColor.PRIMARY); + } else { + setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); + } + return view; + } else if (type == RTP_SESSION) { + final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; + final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final long duration = rtpSessionStatus.duration; + final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent()); + if (received) { + if (duration > 0) { + viewHolder.status_message.setText( + activity.getString( + R.string.incoming_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else if (rtpSessionStatus.successful) { + viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime)); + } else { + viewHolder.status_message.setText( + activity.getString( + R.string.missed_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } else { + if (duration > 0) { + viewHolder.status_message.setText( + activity.getString( + R.string.outgoing_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else { + viewHolder.status_message.setText( + activity.getString( + R.string.outgoing_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY); + setTextColor(viewHolder.status_message, BubbleColor.SECONDARY); + setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY); + } else { + setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); + setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH); + } + viewHolder.indicatorReceived.setImageResource( + RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful)); + return view; + } else if (type == STATUS) { + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.status_message.setVisibility(View.GONE); + viewHolder.contact_picture.setVisibility(View.GONE); + viewHolder.load_more_messages.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setOnClickListener( + v -> loadMoreMessages((Conversation) message.getConversation())); + } else { + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setVisibility(View.GONE); + viewHolder.status_message.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else if (message.getCounterpart() != null + || message.getTrueCounterpart() != null + || (message.getCounterparts() != null + && message.getCounterparts().size() > 0)) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + } else { + viewHolder.contact_picture.setVisibility(View.GONE); + } + } + return view; + } else { + // viewHolder.message_box.setClipToOutline(true); This eats the bubble tails on A14 for some reason + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); + } + + resetClickListener(viewHolder.message_box, viewHolder.messageBody); + + viewHolder.message_box.setOnClickListener(v -> { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + }); + SwipeDetector swipeDetector = new SwipeDetector((action) -> { + if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) { + MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message); + } + }); + viewHolder.message_box.setOnTouchListener(swipeDetector); + viewHolder.image.setOnTouchListener(swipeDetector); + viewHolder.time.setOnTouchListener(swipeDetector); + + // Treat touch-up as click so we don't have to touch twice + // (touch twice is because it's waiting to see if you double-touch for text selection) + viewHolder.messageBody.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + } + + swipeDetector.onTouch(v, event); + + return false; + }); + viewHolder.messageBody.setOnClickListener(v -> { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + }); + viewHolder.contact_picture.setOnClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } + + }); + viewHolder.contact_picture.setOnLongClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(v, message); + return true; + } else { + return false; + } + }); + viewHolder.messageBody.setAccessibilityDelegate(null); + + boolean footerWrap = false; + + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + + final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null)); + if (muted) { + // Muted MUC participant + displayInfoMessage(viewHolder, "Muted", bubbleColor); + } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { + if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type); + } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type); + } else { + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor); + } + } else if (message.isFileOrImage() + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { + displayMediaPreviewMessage(viewHolder, message, bubbleColor, type); + if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) { + footerWrap = true; + } + } else if (message.getFileParams().runtime > 0) { + displayAudioMessage(viewHolder, message, bubbleColor, type); + } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) { + displayWebxdcMessage(viewHolder, message, bubbleColor, type); + } else { + displayOpenableMessage(viewHolder, message, bubbleColor, type); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (account.isPgpDecryptionServiceConnected()) { + if (conversation instanceof Conversation + && !account.hasPendingPgpIntent((Conversation) conversation)) { + displayInfoMessage( + viewHolder, + activity.getString(R.string.message_decrypting), + bubbleColor); + } else { + displayInfoMessage( + viewHolder, activity.getString(R.string.pgp_message), bubbleColor); + } + } else { + displayInfoMessage( + viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor); + viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayInfoMessage( + viewHolder, activity.getString(R.string.decryption_failed), bubbleColor); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { + displayInfoMessage( + viewHolder, + activity.getString(R.string.not_encrypted_for_this_device), + bubbleColor); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + displayInfoMessage( + viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor); + } else { + if (message.isGeoUri()) { + displayLocationMessage(viewHolder, message, bubbleColor, type); + } else if (message.treatAsDownloadable()) { + try { + final URI uri = message.getOob(); + displayDownloadableMessage(viewHolder, + message, + activity.getString( + R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + uri.getHost()), + bubbleColor, type); + } catch (Exception e) { + displayDownloadableMessage( + viewHolder, + message, + activity.getString( + R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + bubbleColor, type); + } + } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { + displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor); + } else { + displayTextMessage(viewHolder, message, bubbleColor, message.getType()); + } + } + + viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0); + LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams(); + statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + viewHolder.status_line.setLayoutParams(statusParams); + + setBackgroundTint(viewHolder.message_box, bubbleColor); + setTextColor(viewHolder.messageBody, bubbleColor); + viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor)); + + if (type == RECEIVED) { + if (!muted && commands != null && conversation instanceof Conversation) { + CommandButtonAdapter adapter = new CommandButtonAdapter(activity); + adapter.addAll(commands); + viewHolder.commands_list.setAdapter(adapter); + viewHolder.commands_list.setVisibility(View.VISIBLE); + viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> { + final Element command = adapter.getItem(pos); + activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node")); + }); + } else { + // It's unclear if we can set this to null... + ListAdapter adapter = viewHolder.commands_list.getAdapter(); + if (adapter instanceof ArrayAdapter) { + ((ArrayAdapter) adapter).clear(); + } + viewHolder.commands_list.setVisibility(View.GONE); + viewHolder.commands_list.setOnItemClickListener(null); + } + + setTextColor(viewHolder.encryption, bubbleColor); + + if (isInValidSession) { + viewHolder.encryption.setVisibility(View.GONE); + } else { + viewHolder.encryption.setVisibility(View.VISIBLE); + if (omemoEncryption && !message.isTrusted()) { + viewHolder.encryption.setText(R.string.not_trusted); + } else { + viewHolder.encryption.setText( + CryptoHelper.encryptionTypeToText(message.getEncryption())); + } + } + } + + if (type == RECEIVED || type == SENT) { + String subject = message.getSubject(); + if (subject == null && message.getThread() != null) { + final var thread = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()); + if (thread != null) subject = thread.getSubject(); + } + if (muted || subject == null) { + viewHolder.subject.setVisibility(View.GONE); + } else { + viewHolder.subject.setVisibility(View.VISIBLE); + viewHolder.subject.setText(subject); + } + } + + displayStatus(viewHolder, message, type, bubbleColor); + + viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void sendAccessibilityEvent(View host, int eventType) { + super.sendAccessibilityEvent(host, eventType); + if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) { + if (viewHolder.messageBody.hasSelection()) { + selectionUuid = message.getUuid(); + } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) { + selectionUuid = null; + } + } + } + }); + + return view; + } + + private void promptOpenKeychainInstall(View view) { + activity.showInstallPgpDialog(); + } + + public FileBackend getFileBackend() { + return activity.xmppConnectionService.getFileBackend(); + } + + public void stopAudioPlayer() { + audioPlayer.stop(); + } + + public void unregisterListenerInAudioPlayer() { + audioPlayer.unregisterListener(); + } + + public void startStopPending() { + audioPlayer.startStopPending(); + } + + public void openDownloadable(Message message) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + && ContextCompat.checkSelfPermission( + activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ConversationFragment.registerPendingMessage(activity, message); + ActivityCompat.requestPermissions( + activity, + new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, + ConversationsActivity.REQUEST_OPEN_MESSAGE); + return; + } + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); + ViewUtil.view(activity, file); + } + + private void showLocation(Message message) { + for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText( + activity, + R.string.no_application_found_to_display_location, + Toast.LENGTH_SHORT) + .show(); + } + + public void updatePreferences() { + final AppSettings appSettings = new AppSettings(activity); + this.bubbleDesign = + new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont()); + } + + public void setHighlightedTerm(List terms) { + this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); + } + + public interface OnContactPictureClicked { + void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + void onContactPictureLongClicked(View v, Message message); + } + + public interface OnInlineImageLongClicked { + boolean onInlineImageLongClicked(Cid cid); + } + + private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) { + view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor)); + } + + private static ColorStateList bubbleToColorStateList( + final View view, final BubbleColor bubbleColor) { + final @AttrRes int colorAttributeResId = + switch (bubbleColor) { + case SURFACE -> Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr.colorSurfaceContainerHigh + : com.google.android.material.R.attr.colorSurfaceContainerLow; + case SURFACE_HIGH -> Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr.colorSurfaceContainerHighest + : com.google.android.material.R.attr.colorSurfaceContainerHigh; + case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer; + case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer; + case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer; + case WARNING -> com.google.android.material.R.attr.colorErrorContainer; + }; + return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId)); + } + + public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) { + ImageViewCompat.setImageTintList( + imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor)); + } + + public static void setImageTintError(final ImageView imageView) { + ImageViewCompat.setImageTintList( + imageView, + ColorStateList.valueOf( + MaterialColors.getColor( + imageView, com.google.android.material.R.attr.colorError))); + } + + public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) { + final var color = bubbleToOnSurfaceColor(textView, bubbleColor); + textView.setTextColor(color); + if (BubbleColor.SURFACES.contains(bubbleColor)) { + textView.setLinkTextColor( + MaterialColors.getColor( + textView, com.google.android.material.R.attr.colorPrimary)); + } else { + textView.setLinkTextColor(color); + } + } + + private static void setTextSize(final TextView textView, final boolean largeFont) { + if (largeFont) { + textView.setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); + } else { + textView.setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); + } + } + + private static @ColorInt int bubbleToOnSurfaceVariant( + final View view, final BubbleColor bubbleColor) { + final @AttrRes int colorAttributeResId; + if (BubbleColor.SURFACES.contains(bubbleColor)) { + colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant; + } else { + colorAttributeResId = bubbleToOnSurface(bubbleColor); + } + return MaterialColors.getColor(view, colorAttributeResId); + } + + private static @ColorInt int bubbleToOnSurfaceColor( + final View view, final BubbleColor bubbleColor) { + return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor)); + } + + public static ColorStateList bubbleToOnSurfaceColorStateList( + final View view, final BubbleColor bubbleColor) { + return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor)); + } + + private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) { + return switch (bubbleColor) { + case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface; + case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer; + case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer; + case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer; + case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer; + }; + } + + public enum BubbleColor { + SURFACE, + SURFACE_HIGH, + PRIMARY, + SECONDARY, + TERTIARY, + WARNING; + + private static final Collection SURFACES = + Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH); + } + + private static class BubbleDesign { + public final boolean colorfulChatBubbles; + public final boolean largeFont; + + private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) { + this.colorfulChatBubbles = colorfulChatBubbles; + this.largeFont = largeFont; + } + } + + private static class ViewHolder { + + public MaterialButton load_more_messages; + public ImageView edit_indicator; + public RelativeLayout audioPlayer; + protected View status_line; + protected LinearLayout message_box; + protected View message_box_inner; + protected MaterialButton download_button; + protected ShapeableImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected TextView subject; + protected TextView messageBody; + protected ImageView contact_picture; + protected TextView status_message; + protected TextView encryption; + protected ListView commands_list; + protected GithubIdenticonView thread_identicon; + } + + class ThumbnailTask extends AsyncTask { + @Override + protected Drawable[] doInBackground(DownloadableFile... params) { + if (isCancelled()) return null; + + Drawable[] d = new Drawable[params.length]; + for (int i = 0; i < params.length; i++) { + try { + d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false); + } catch (final IOException e) { + d[i] = null; + } + } + + return d; + } + + @Override + protected void onPostExecute(final Drawable[] d) { + if (isCancelled()) return; + activity.xmppConnectionService.updateConversationUi(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index d6930c24e7..b97746f222 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -279,8 +279,8 @@ public class ConversationAdapter void onConversationClick(View view, Conversation conversation); } - static class ConversationViewHolder extends RecyclerView.ViewHolder { - private final ItemConversationBinding binding; + public static class ConversationViewHolder extends RecyclerView.ViewHolder { + public final ItemConversationBinding binding; private ConversationViewHolder(final ItemConversationBinding binding) { super(binding.getRoot()); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index 7efaff3b02..c8f8417af3 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -45,6 +45,14 @@ public class MediaAdapter extends RecyclerView.Adapter ARCHIVE_MIMES = + Arrays.asList( + "application/x-7z-compressed", + "application/zip", + "application/rar", + "application/x-gtar", + "application/x-tar"); public static final List CODE_MIMES = Arrays.asList("text/html", "text/xml"); private final ArrayList attachments = new ArrayList<>(); @@ -95,7 +103,7 @@ public class MediaAdapter extends RecyclerView.Adapter Toast.makeText(requireActivity(), "Blocked media will be displayed again", Toast.LENGTH_LONG).show()); return true; }); + + final ListPreference autoAcceptFileSize = findPreference("auto_accept_file_size"); + if (autoAcceptFileSize == null) { + throw new IllegalStateException("The preference resource file is missing preferences"); + } + setValues( + autoAcceptFileSize, + R.array.file_size_values, + value -> { + if (value <= 0) { + return getString(R.string.never); + } else { + return UIHelper.filesizeToString(value); + } + }); } protected void downloadStickers() { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java index 9a526c7593..582ddec0b6 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java @@ -196,7 +196,12 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment { uri = appSettings().getRingtone(); } Log.i(Config.LOGTAG, "current ringtone: " + uri); - this.pickRingtoneLauncher.launch(uri); + try { + this.pickRingtoneLauncher.launch(uri); + } catch (final ActivityNotFoundException e) { + Toast.makeText(requireActivity(), R.string.no_application_found, Toast.LENGTH_LONG) + .show(); + } } private AppSettings appSettings() { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java index 183f2c4d23..e026f25732 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; +import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.ListPreference; @@ -13,6 +14,7 @@ import androidx.preference.Preference; import com.rarepebble.colorpicker.ColorPreference; +import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.primitives.Ints; @@ -111,14 +113,29 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat { } } - protected static class TimeframeSummaryProvider - implements Preference.SummaryProvider { - - @Nullable - @Override - public CharSequence provideSummary(@NonNull ListPreference preference) { - final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue())); - return timeframeValueToName(preference.getContext(), value == null ? 0 : value); + protected void setValues( + final ListPreference listPreference, + @ArrayRes int resId, + final Function valueToName) { + final int[] choices = getResources().getIntArray(resId); + final CharSequence[] entries = new CharSequence[choices.length]; + final CharSequence[] entryValues = new CharSequence[choices.length]; + for (int i = 0; i < choices.length; ++i) { + final int value = choices[i]; + entryValues[i] = String.valueOf(choices[i]); + entries[i] = valueToName.apply(value); } + listPreference.setEntries(entries); + listPreference.setEntryValues(entryValues); + listPreference.setSummaryProvider( + new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + final Integer value = + Ints.tryParse(Strings.nullToEmpty(preference.getValue())); + return valueToName.apply(value == null ? 0 : value); + } + }); } } diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index ded53567ad..6e4f397bbe 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -1,12 +1,12 @@ package eu.siacs.conversations.utils; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; import android.util.Log; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Charsets; +import com.google.common.io.CharSink; +import com.google.common.io.Files; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; @@ -17,20 +17,16 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity; -import java.io.BufferedReader; -import java.io.FileInputStream; +import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.ClassNotFoundException; import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Locale; public class ExceptionHelper { private static final String FILENAME = "stacktrace.txt"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); public static void init(final Context context) { if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) { @@ -45,72 +41,65 @@ public class ExceptionHelper { return false; } catch (final ClassNotFoundException e) { } - try { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return false; - } - final AppSettings appSettings = new AppSettings(activity); - if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) { - return false; - } - final Account account = AccountUtils.getFirstEnabled(service); - if (account == null) { - return false; - } - final FileInputStream file = activity.openFileInput(FILENAME); - final InputStreamReader inputStreamReader = new InputStreamReader(file); - final BufferedReader stacktrace = new BufferedReader(inputStreamReader); - final StringBuilder report = new StringBuilder(); - final PackageManager pm = activity.getPackageManager(); - final PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - final String versionName = packageInfo.versionName; - final int versionCode = packageInfo.versionCode; - final int version = versionCode > 10000 ? (versionCode / 100) : versionCode; - report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n'); - report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); - Signature[] signatures = packageInfo.signatures; - if (signatures != null && signatures.length >= 1) { - report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); - } - report.append('\n'); - } catch (final Exception e) { - return false; - } - String line; - while ((line = stacktrace.readLine()) != null) { - report.append(line); - report.append('\n'); - } - file.close(); - activity.deleteFile(FILENAME); - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); - builder.setTitle(activity.getString(R.string.crash_report_title, activity.getString(R.string.app_name))); - builder.setMessage(activity.getString(R.string.crash_report_message, activity.getString(R.string.app_name))); - builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { - - Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); - Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); - Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message); - }); - builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> appSettings.setSendCrashReports(false)); - builder.create().show(); - return true; - } catch (final IOException ignored) { + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; + if (service == null) { return false; } + final AppSettings appSettings = new AppSettings(activity); + if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) { + return false; + } + final Account account = AccountUtils.getFirstEnabled(service); + if (account == null) { + return false; + } + final var file = new File(activity.getCacheDir(), FILENAME); + if (!file.exists()) { + return false; + } + final String report; + try { + report = Files.asCharSource(file, Charsets.UTF_8).read(); + } catch (final IOException e) { + return false; + } + if (file.delete()) { + Log.d(Config.LOGTAG, "deleted crash report file"); + } + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); + builder.setTitle( + activity.getString( + R.string.crash_report_title, activity.getString(R.string.app_name))); + builder.setMessage( + activity.getString( + R.string.crash_report_message, activity.getString(R.string.app_name))); + builder.setPositiveButton( + activity.getText(R.string.send_now), + (dialog, which) -> { + Log.d( + Config.LOGTAG, + "using account=" + + account.getJid().asBareJid() + + " to send in stack trace"); + Conversation conversation = + service.findOrCreateConversation( + account, Config.BUG_REPORTS, false, true); + Message message = new Message(conversation, report, Message.ENCRYPTION_NONE); + service.sendMessage(message); + }); + builder.setNegativeButton( + activity.getText(R.string.send_never), + (dialog, which) -> appSettings.setSendCrashReports(false)); + builder.create().show(); + return true; } - static void writeToStacktraceFile(Context context, String msg) { + static void writeToStacktraceFile(final Context context, final String msg) { try { - OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - os.write(msg.getBytes()); - os.flush(); - os.close(); - } catch (IOException ignored) { + Files.asCharSink(new File(context.getCacheDir(), FILENAME), Charsets.UTF_8).write(msg); + } catch (IOException e) { + Log.w(Config.LOGTAG, "could not write stack trace to file", e); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 4c53df6692..90dfe137ac 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -141,6 +141,7 @@ public final class MimeUtils { add("application/vnd.sun.xml.writer.global", "sxg"); add("application/vnd.sun.xml.writer.template", "stw"); add("application/vnd.visio", "vsd"); + add("application/x-7z-compressed","7z"); add("application/x-abiword", "abw"); add("application/x-apple-diskimage", "dmg"); add("application/x-bcpio", "bcpio"); diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 236a9b41c6..00fffea9ba 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -6,24 +6,56 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.common.net.InetAddresses; import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.Conversations; +import eu.siacs.conversations.xmpp.Jid; + +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsname.InvalidDnsNameException; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.CNAME; +import org.minidns.record.Data; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.Record; +import org.minidns.record.SRV; import java.io.IOException; -import java.lang.reflect.Field; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; -//import de.gultsch.minidns.AndroidDNSClient; import org.minidns.AbstractDnsClient; import org.minidns.DnsCache; import org.minidns.DnsClient; @@ -44,13 +76,35 @@ import org.minidns.record.Data; import org.minidns.record.InternetAddressRR; import org.minidns.record.Record; import org.minidns.record.SRV; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; public class Resolver { + private static final Comparator RESULT_COMPARATOR = + (left, right) -> { + if (left.priority == right.priority) { + if (left.directTls == right.directTls) { + if (left.ip == null && right.ip == null) { + return 0; + } else if (left.ip != null && right.ip != null) { + if (left.ip instanceof Inet4Address + && right.ip instanceof Inet4Address) { + return 0; + } else { + return left.ip instanceof Inet4Address ? -1 : 1; + } + } else { + return left.ip != null ? -1 : 1; + } + } else { + return left.directTls ? -1 : 1; + } + } else { + return left.priority - right.priority; + } + }; + + private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12); + public static final int DEFAULT_PORT_XMPP = 5222; private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; @@ -203,7 +257,7 @@ public class Resolver { try { DnsName.from(hostname); return false; - } catch (IllegalArgumentException e) { + } catch (final InvalidDnsNameException | IllegalArgumentException e) { return true; } } @@ -224,206 +278,234 @@ public class Resolver { } } - public static boolean useDirectTls(final int port) { return port == 443 || port == 5223; } public static List resolve(final String domain) { - final List ipResults = fromIpAddress(domain); - if (ipResults.size() > 0) { + final List ipResults = fromIpAddress(domain); + if (!ipResults.isEmpty()) { return ipResults; } - final List results = new ArrayList<>(); - final List fallbackResults = new ArrayList<>(); - final Thread[] threads = new Thread[3]; - threads[0] = new Thread(() -> { - try { - final List list = resolveSrv(domain, true); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); - } - } - }); - threads[1] = new Thread(() -> { - try { - final List list = resolveSrv(domain, false); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); - } - } - }); - threads[2] = new Thread(() -> { - List list = resolveNoSrvRecords(DnsName.from(domain), true); - synchronized (fallbackResults) { - fallbackResults.addAll(list); - } - }); - for (final Thread thread : threads) { - thread.start(); - } + + final var startTls = resolveSrvAsFuture(domain, false); + final var directTls = resolveSrvAsFuture(domain, true); + + final var combined = merge(ImmutableList.of(startTls, directTls)); + + final var combinedWithFallback = + Futures.transformAsync( + combined, + results -> { + if (results.isEmpty()) { + return resolveNoSrvAsFuture(DnsName.from(domain), true); + } else { + return Futures.immediateFuture(results); + } + }, + MoreExecutors.directExecutor()); + final var orderedFuture = + Futures.transform( + combinedWithFallback, + all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all), + MoreExecutors.directExecutor()); try { - threads[0].join(); - threads[1].join(); - if (results.size() > 0) { - threads[2].interrupt(); - synchronized (results) { - Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results); - return results; - } - } else { - threads[2].join(); - synchronized (fallbackResults) { - Collections.sort(fallbackResults); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults); - return fallbackResults; - } - } - } catch (InterruptedException e) { - for (Thread thread : threads) { - thread.interrupt(); - } + final var ordered = orderedFuture.get(); + Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered); + return ordered; + } catch (final ExecutionException e) { + Log.d(Config.LOGTAG, "error resolving DNS", e); + return Collections.emptyList(); + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "DNS resolution interrupted"); return Collections.emptyList(); } } - private static List fromIpAddress(String domain) { - if (!IP.matches(domain)) { - return Collections.emptyList(); - } - try { - Result result = new Result(); - result.ip = InetAddress.getByName(domain); - result.port = DEFAULT_PORT_XMPP; - result.authenticated = true; - return Collections.singletonList(result); - } catch (UnknownHostException e) { - return Collections.emptyList(); - } - } - - private static List resolveSrv(String domain, final boolean directTls) throws IOException { - final String dnsNameS = (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain; - DnsName dnsName = DnsName.from(dnsNameS); - ResolverResult result = resolveWithFallback(dnsName, SRV.class); - final List results = new ArrayList<>(); - final List threads = new ArrayList<>(); - for (SRV record : result.getAnswersOrEmptySet()) { - if (record.name.length() == 0 && record.priority == 0) { - continue; - } - final boolean authentic = result.isAuthenticData() || record.target.toString().equals(knownSRV.get(dnsNameS)); - threads.add(new Thread(() -> { - final List ipv4s = resolveIp(record, A.class, authentic, directTls); - if (ipv4s.size() == 0) { - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = result.isAuthenticData(); - ipv4s.add(resolverResult); - } - synchronized (results) { - results.addAll(ipv4s); - } - - })); - threads.add(new Thread(() -> { - final List ipv6s = resolveIp(record, AAAA.class, authentic, directTls); - synchronized (results) { - results.addAll(ipv6s); - } - })); - } - for (Thread thread : threads) { - thread.start(); - } - for (Thread thread : threads) { + private static List fromIpAddress(final String domain) { + if (IP.matches(domain)) { + final InetAddress inetAddress; try { - thread.join(); - } catch (InterruptedException e) { + inetAddress = InetAddress.getByName(domain); + } catch (final UnknownHostException e) { return Collections.emptyList(); } + final Result result = new Result(); + result.ip = inetAddress; + result.port = DEFAULT_PORT_XMPP; + return Collections.singletonList(result); + } else { + return Collections.emptyList(); } - return results; } - private static List resolveIp(SRV srv, Class type, boolean authenticated, boolean directTls) { - List list = new ArrayList<>(); - try { - ResolverResult results = resolveWithFallback(srv.target, type); - for (D record : results.getAnswersOrEmptySet()) { - Result resolverResult = Result.fromRecord(srv, directTls); - resolverResult.authenticated = results.isAuthenticData() && authenticated; - resolverResult.ip = record.getInetAddress(); - list.add(resolverResult); - } - } catch (Throwable t) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage()); - } - return list; + private static ListenableFuture> resolveSrvAsFuture( + final String domain, final boolean directTls) { + final DnsName dnsName = + DnsName.from( + (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); + final var resultFuture = resolveAsFuture(dnsName, SRV.class); + return Futures.transformAsync( + resultFuture, + result -> resolveIpsAsFuture(result, directTls), + MoreExecutors.directExecutor()); } - private static List resolveNoSrvRecords(DnsName dnsName, boolean withCnames) { - final List results = new ArrayList<>(); - try { - ResolverResult aResult = resolveWithFallback(dnsName, A.class); - for (A a : aResult.getAnswersOrEmptySet()) { - Result r = Result.createDefault(dnsName, a.getInetAddress()); - r.authenticated = aResult.isAuthenticData(); - results.add(r); + @NonNull + private static ListenableFuture> resolveIpsAsFuture( + final ResolverResult srvResolverResult, final boolean directTls) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) { + if (record.target.length() == 0 && record.priority == 0) { + continue; } - ResolverResult aaaaResult = resolveWithFallback(dnsName, AAAA.class); - for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) { - Result r = Result.createDefault(dnsName, aaaa.getInetAddress()); - r.authenticated = aaaaResult.isAuthenticData(); - results.add(r); - } - if (results.size() == 0 && withCnames) { - ResolverResult cnameResult = resolveWithFallback(dnsName, CNAME.class); - for (CNAME cname : cnameResult.getAnswersOrEmptySet()) { - for (Result r : resolveNoSrvRecords(cname.name, false)) { - r.authenticated = r.authenticated && cnameResult.isAuthenticData(); - results.add(r); + final var ipv4sRaw = + resolveIpsAsFuture( + record, A.class, srvResolverResult.isAuthenticData(), directTls); + final var ipv4s = + Futures.transform( + ipv4sRaw, + results -> { + if (results.isEmpty()) { + final Result resolverResult = + Result.fromRecord(record, directTls); + resolverResult.authenticated = + srvResolverResult.isAuthenticData(); + return Collections.singletonList(resolverResult); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); + final var ipv6s = + resolveIpsAsFuture( + record, AAAA.class, srvResolverResult.isAuthenticData(), directTls); + futuresBuilder.add(ipv4s); + futuresBuilder.add(ipv6s); + } + final ImmutableList>> futures = futuresBuilder.build(); + return merge(futures); + } + + private static ListenableFuture> merge( + 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); } - } - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); - } - } - results.add(Result.createDefault(dnsName)); - return results; + return builder.build(); + }, + MoreExecutors.directExecutor()); } - private static ResolverResult resolveWithFallback(DnsName dnsName, Class type) throws IOException { - final Question question = new Question(dnsName, Record.TYPE.getType(type)); - if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) { - try { - ResolverResult result = DnssecResolverApi.INSTANCE.resolve(question); - if (result.wasSuccessful() && !result.isAuthenticData()) { - Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons()); - } - return result; - } catch (DnssecValidationFailedException e) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); - } catch (IOException e) { - throw e; - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); - } - } - return ResolverApi.INSTANCE.resolve(question); + private static > + ListenableFuture> resolveIpsAsFuture( + final SRV srv, Class type, boolean authenticated, boolean directTls) { + final var resultFuture = resolveAsFuture(srv.target, type); + return Futures.transform( + resultFuture, + result -> { + final var builder = new ImmutableList.Builder(); + for (D record : result.getAnswersOrEmptySet()) { + Result resolverResult = Result.fromRecord(srv, directTls); + resolverResult.authenticated = + result.isAuthenticData() + && authenticated; // TODO technically it does not matter if + // the IP + // was authenticated + resolverResult.ip = record.getInetAddress(); + builder.add(resolverResult); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); } - public static class Result implements Comparable { + private static ListenableFuture> resolveNoSrvAsFuture( + final DnsName dnsName, boolean cName) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + ListenableFuture> aRecordResults = + Futures.transform( + resolveAsFuture(dnsName, A.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aRecordResults); + ListenableFuture> aaaaRecordResults = + Futures.transform( + resolveAsFuture(dnsName, AAAA.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + aaaa -> + Result.createDefault( + dnsName, aaaa.getInetAddress(), result.isAuthenticData())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aaaaRecordResults); + if (cName) { + ListenableFuture> cNameRecordResults = + Futures.transformAsync( + resolveAsFuture(dnsName, CNAME.class), + result -> { + Collection>> test = + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + cname -> resolveNoSrvAsFuture(cname.target, false)); + return merge(test); + }, + MoreExecutors.directExecutor()); + futuresBuilder.add(cNameRecordResults); + } + final ImmutableList>> futures = futuresBuilder.build(); + final var noSrvFallbacks = merge(futures); + return Futures.transform( + noSrvFallbacks, + results -> { + if (results.isEmpty()) { + return Collections.singletonList(Result.createDefault(dnsName)); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); + } + + private static ListenableFuture> resolveAsFuture( + final DnsName dnsName, final Class type) { + return Futures.submit( + () -> { + final Question question = new Question(dnsName, Record.TYPE.getType(type)); + if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) { + try { + ResolverResult result = DnssecResolverApi.INSTANCE.resolve(question); + if (result.wasSuccessful() && !result.isAuthenticData()) { + Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons()); + } + return result; + } catch (DnssecValidationFailedException e) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); + } catch (IOException e) { + throw e; + } catch (Throwable throwable) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); + } + } + return ResolverApi.INSTANCE.resolve(question); + }, + DNS_QUERY_EXECUTOR); + } + + public static class Result { public static final String DOMAIN = "domain"; public static final String IP = "ip"; public static final String HOSTNAME = "hostname"; @@ -438,40 +520,42 @@ public class Resolver { private boolean authenticated = false; private int priority; - static Result fromRecord(SRV srv, boolean directTls) { - Result result = new Result(); + static Result fromRecord(final SRV srv, final boolean directTls) { + final Result result = new Result(); result.port = srv.port; - result.hostname = srv.name; + result.hostname = srv.target; result.directTls = directTls; result.priority = srv.priority; return result; } - static Result createDefault(DnsName hostname, InetAddress ip) { + static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) { Result result = new Result(); result.port = DEFAULT_PORT_XMPP; result.hostname = hostname; result.ip = ip; + result.authenticated = authenticated; return result; } - static Result createDefault(DnsName hostname) { - return createDefault(hostname, null); + static Result createDefault(final DnsName hostname) { + return createDefault(hostname, null, false); } - public static Result fromCursor(Cursor cursor) { + public static Result fromCursor(final Cursor cursor) { final Result result = new Result(); try { - result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP))); - } catch (UnknownHostException e) { + result.ip = + InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP))); + } catch (final UnknownHostException e) { result.ip = null; } - final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME)); + final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)); result.hostname = hostname == null ? null : DnsName.from(hostname); - result.port = cursor.getInt(cursor.getColumnIndex(PORT)); - result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); - result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; - result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; + result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT)); + result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY)); + result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0; + result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0; return result; } @@ -479,26 +563,18 @@ public class Resolver { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Result result = (Result) o; - - if (port != result.port) return false; - if (directTls != result.directTls) return false; - if (authenticated != result.authenticated) return false; - if (priority != result.priority) return false; - if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false; - return hostname != null ? hostname.equals(result.hostname) : result.hostname == null; + return port == result.port + && directTls == result.directTls + && authenticated == result.authenticated + && priority == result.priority + && Objects.equal(ip, result.ip) + && Objects.equal(hostname, result.hostname); } @Override public int hashCode() { - int result = ip != null ? ip.hashCode() : 0; - result = 31 * result + (hostname != null ? hostname.hashCode() : 0); - result = 31 * result + port; - result = 31 * result + (directTls ? 1 : 0); - result = 31 * result + (authenticated ? 1 : 0); - result = 31 * result + priority; - return result; + return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority); } public InetAddress getIp() { @@ -522,38 +598,16 @@ public class Resolver { } @Override + @NonNull public String toString() { - return "Result{" + - "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' + - ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' + - ", port=" + port + - ", directTls=" + directTls + - ", authenticated=" + authenticated + - ", priority=" + priority + - '}'; - } - - @Override - public int compareTo(@NonNull Result result) { - if (result.priority == priority) { - if (directTls == result.directTls) { - if (ip == null && result.ip == null) { - return 0; - } else if (ip != null && result.ip != null) { - if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) { - return 0; - } else { - return ip instanceof Inet4Address ? -1 : 1; - } - } else { - return ip != null ? -1 : 1; - } - } else { - return directTls ? 1 : -1; - } - } else { - return priority - result.priority; - } + return MoreObjects.toStringHelper(this) + .add("ip", ip) + .add("hostname", hostname) + .add("port", port) + .add("directTls", directTls) + .add("authenticated", authenticated) + .add("priority", priority) + .toString(); } public ContentValues toContentValues() { @@ -626,5 +680,4 @@ public class Resolver { return result; } } - } diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index c79a152327..0b893019e8 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -18,6 +18,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.work.ForegroundInfo; +import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; @@ -35,7 +36,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.receiver.WorkManagerEventReceiver; import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.Compatibility; @@ -99,6 +99,7 @@ public class ExportBackupWorker extends Worker { @NonNull @Override public Result doWork() { + setForegroundAsync(getForegroundInfo()); final List files; try { files = export(); @@ -227,18 +228,14 @@ public class ExportBackupWorker extends Worker { IV, salt); final var notification = getNotification(); - if (!recurringBackup) { - final var cancel = new Intent(context, WorkManagerEventReceiver.class); - cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP); - final var cancelPendingIntent = - PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS); - notification.addAction( - new NotificationCompat.Action.Builder( - R.drawable.ic_cancel_24dp, - context.getString(R.string.cancel), - cancelPendingIntent) - .build()); - } + final var cancelPendingIntent = + WorkManager.getInstance(context).createCancelPendingIntent(getId()); + notification.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_cancel_24dp, + context.getString(R.string.cancel), + cancelPendingIntent) + .build()); final Progress progress = new Progress(notification, max, count); final File directory = file.getParentFile(); if (directory != null && directory.mkdirs()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index ccdc688fe1..45431f0ffc 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -5,7 +5,9 @@ import androidx.annotation.NonNull; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.base.Strings; import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; import java.util.ArrayList; import java.util.Collection; @@ -16,7 +18,7 @@ import java.util.stream.Collectors; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Message; public class Element implements Node { private final String name; @@ -141,6 +143,10 @@ public class Element implements Node { return ImmutableList.copyOf(this.children); } + public void setAttribute(final String name, final boolean value) { + this.setAttribute(name, value ? "1" : "0"); + } + // Deprecated: you probably want bindTo or replaceChildren public Element setChildren(List children) { this.childNodes = new ArrayList(children); @@ -165,6 +171,31 @@ public class Element implements Node { return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining()); } + public long getLongAttribute(final String name) { + final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name))); + return value == null ? 0 : value; + } + + public Optional getOptionalIntAttribute(final String name) { + final String value = getAttribute(name); + if (value == null) { + return Optional.absent(); + } + return Optional.fromNullable(Ints.tryParse(value)); + } + + public Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.ofEscaped(jid); + } catch (final IllegalArgumentException e) { + return InvalidJid.of(jid, this instanceof Message); + } + } + return null; + } + public Element setAttribute(String name, String value) { if (name != null && value != null) { this.attributes.put(name, value); @@ -224,7 +255,7 @@ public class Element implements Node { return result; } - public Element removeAttribute(String name) { + public Element removeAttribute(final String name) { this.attributes.remove(name); return this; } @@ -242,26 +273,6 @@ public class Element implements Node { } } - public Optional getOptionalIntAttribute(final String name) { - final String value = getAttribute(name); - if (value == null) { - return Optional.absent(); - } - return Optional.fromNullable(Ints.tryParse(value)); - } - - public Jid getAttributeAsJid(String name) { - final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.ofEscaped(jid); - } catch (final IllegalArgumentException e) { - return InvalidJid.of(jid, this instanceof MessagePacket); - } - } - return null; - } - public Hashtable getAttributes() { return this.attributes; } diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index 8afd52c4c8..a02041acb9 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -37,7 +37,7 @@ public class LocalizedContent { } } } - if (contents.size() == 0) { + if (contents.isEmpty()) { return null; } final String userLanguage = Locale.getDefault().getLanguage(); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e10b6bb941..00df32651d 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,8 +1,29 @@ package eu.siacs.conversations.xml; public final class Namespace { + public static final String ADDRESSING = "http://jabber.org/protocol/address"; + public static final String AXOLOTL = "eu.siacs.conversations.axolotl"; + public static final String PGP_SIGNED = "jabber:x:signed"; + public static final String PGP_ENCRYPTED = "jabber:x:encrypted"; + public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles"; + public static final String AXOLOTL_DEVICE_LIST = AXOLOTL + ".devicelist"; + public static final String HINTS = "urn:xmpp:hints"; + public static final String MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2"; + public static final String VERSION = "jabber:iq:version"; + public static final String LAST_MESSAGE_CORRECTION = "urn:xmpp:message-correct:0"; + public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm"; + public static final String CHAT_MARKERS = "urn:xmpp:chat-markers:0"; + public static final String CHAT_STATES = "http://jabber.org/protocol/chatstates"; + public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts"; + public static final String REACTIONS = "urn:xmpp:reactions:0"; + public static final String VCARD_TEMP = "vcard-temp"; + public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update"; + public static final String DELAY = "urn:xmpp:delay"; + public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0"; public static final String STREAMS = "http://etherx.jabber.org/streams"; + public static final String STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"; public static final String JABBER_CLIENT = "jabber:client"; + public static final String FORWARD = "urn:xmpp:forward:0"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; @@ -23,12 +44,15 @@ public final class Namespace { public static final String FAST = "urn:xmpp:fast:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; + public static final String PUBSUB_EVENT = PUBSUB + "#event"; + public static final String MUC = "http://jabber.org/protocol/muc"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String NICK = "http://jabber.org/protocol/nick"; - public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; + public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = + "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; public static final String BIND2 = "urn:xmpp:bind:0"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; @@ -38,7 +62,7 @@ public final class Namespace { public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_DATA = "urn:xmpp:avatar:data"; - public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; + public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; @@ -48,7 +72,8 @@ public final class Namespace { public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; - public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1"; + public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = + "urn:xmpp:jingle:transports:webrtc-datachannel:1"; public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1"; public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1"; @@ -57,9 +82,12 @@ public final class Namespace { public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; - public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; - public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; - public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0"; + public static final String JINGLE_RTP_HEADER_EXTENSIONS = + "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = + "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = + "urn:xmpp:jingle:apps:rtp:ssma:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; @@ -70,8 +98,10 @@ public final class Namespace { public static final String INVITE = "urn:xmpp:invite"; public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; - public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; - public static final String JINGLE_TRANSPORT_ICE_OPTION = "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option"; + public static final String OMEMO_DTLS_SRTP_VERIFICATION = + "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + public static final String JINGLE_TRANSPORT_ICE_OPTION = + "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; public static final String VCARD4 = "urn:ietf:params:xml:ns:vcard-4.0"; public static final String REPORTING = "urn:xmpp:reporting:1"; @@ -80,4 +110,7 @@ public final class Namespace { public static final String HASHES = "urn:xmpp:hashes:2"; public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; + + public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; + public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; } diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index aee7d7819a..0d5377cc9a 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -3,6 +3,12 @@ package eu.siacs.conversations.xml; import android.util.Log; import android.util.Xml; +import eu.siacs.conversations.Config; + +import im.conversations.android.xmpp.ExtensionFactory; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -11,8 +17,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import eu.siacs.conversations.Config; - public class XmlReader implements Closeable { private final XmlPullParser parser; private InputStream is; @@ -90,8 +94,21 @@ public class XmlReader implements Closeable { return null; } - public Element readElement(Tag currentTag) throws IOException { - Element element = new Element(currentTag.getName()); + public T readElement(final Tag current, final Class clazz) + throws IOException { + final Element element = readElement(current); + if (clazz.isInstance(element)) { + return clazz.cast(element); + } + throw new IOException( + String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); + } + + public Element readElement(final Tag currentTag) throws IOException { + final var attributes = currentTag.getAttributes(); + final var namespace = attributes.get("xmlns"); + final var name = currentTag.getName(); + final Element element = ExtensionFactory.create(name, namespace); element.setAttributes(currentTag.getAttributes()); Tag nextTag = this.readTag(); if (nextTag == null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 60380d648e..5232bc1a31 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -15,7 +15,6 @@ import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.util.Consumer; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; @@ -70,6 +69,7 @@ import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -83,6 +83,9 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MessageArchiveService; @@ -105,18 +108,42 @@ import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; -import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket; -import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + +import im.conversations.android.xmpp.model.AuthenticationFailure; +import im.conversations.android.xmpp.model.AuthenticationRequest; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.bind2.Bind; +import im.conversations.android.xmpp.model.bind2.Bound; +import im.conversations.android.xmpp.model.csi.Active; +import im.conversations.android.xmpp.model.csi.Inactive; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.fast.Fast; +import im.conversations.android.xmpp.model.fast.RequestToken; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.sasl.Auth; +import im.conversations.android.xmpp.model.sasl.Failure; +import im.conversations.android.xmpp.model.sasl.Mechanisms; +import im.conversations.android.xmpp.model.sasl.Response; +import im.conversations.android.xmpp.model.sasl.SaslError; +import im.conversations.android.xmpp.model.sasl.Success; +import im.conversations.android.xmpp.model.sasl2.Authenticate; +import im.conversations.android.xmpp.model.sasl2.Authentication; +import im.conversations.android.xmpp.model.sasl2.UserAgent; +import im.conversations.android.xmpp.model.sm.Ack; +import im.conversations.android.xmpp.model.sm.Enable; +import im.conversations.android.xmpp.model.sm.Enabled; +import im.conversations.android.xmpp.model.sm.Failed; +import im.conversations.android.xmpp.model.sm.Request; +import im.conversations.android.xmpp.model.sm.Resume; +import im.conversations.android.xmpp.model.sm.Resumed; +import im.conversations.android.xmpp.model.sm.StreamManagement; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Presence; +import im.conversations.android.xmpp.model.stanza.Stanza; +import im.conversations.android.xmpp.model.tls.Proceed; +import im.conversations.android.xmpp.model.tls.StartTls; +import im.conversations.android.xmpp.processor.BindProcessor; import okhttp3.HttpUrl; @@ -151,6 +178,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.regex.Matcher; import javax.net.ssl.KeyManager; @@ -163,46 +191,12 @@ import javax.net.ssl.X509TrustManager; public class XmppConnection implements Runnable { - private static final int PACKET_IQ = 0; - private static final int PACKET_MESSAGE = 1; - private static final int PACKET_PRESENCE = 2; - public final OnIqPacketReceived registrationResponseListener = - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final List PASSWORD_TOO_WEAK_MSGS = - Arrays.asList( - "The password is too weak", "Please use a longer password."); - Element error = packet.findChild("error"); - Account.State state = Account.State.REGISTRATION_FAILED; - if (error != null) { - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains( - error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; - } - } - throw new StateChangingError(state); - } - }; protected final Account account; private final Features features = new Features(this); private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); - private final SparseArray mStanzaQueue = new SparseArray<>(); - private final Hashtable>> packetCallbacks = - new Hashtable<>(); + private final SparseArray mStanzaQueue = new SparseArray<>(); + private final Hashtable, ScheduledFuture>>> packetCallbacks = new Hashtable<>(); private final Set advancedStreamFeaturesLoadedListeners = new HashSet<>(); private final AppSettings appSettings; @@ -215,8 +209,8 @@ public class XmppConnection implements Runnable { private boolean quickStartInProgress = false; private boolean isBound = false; private boolean offlineMessagesRetrieved = false; - private Element streamFeatures; - private Element boundStreamFeatures; + private im.conversations.android.xmpp.model.streams.Features streamFeatures; + private im.conversations.android.xmpp.model.streams.Features boundStreamFeatures; private StreamId streamId = null; private int stanzasReceived = 0; private int stanzasSent = 0; @@ -233,12 +227,13 @@ public class XmppConnection implements Runnable { private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private boolean mInteractive = false; private int attempt = 0; - private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; - private OnIqPacketReceived unregisteredIqListener = null; - private OnMessagePacketReceived messageListener = null; + + private final Consumer presenceListener; + private final Consumer unregisteredIqListener; + private final Consumer messageListener; private OnStatusChanged statusListener = null; - private OnBindListener bindListener = null; + private final Runnable bindListener; private OnMessageAcknowledged acknowledgedListener = null; private LoginInfo loginInfo; private HashedToken.Mechanism hashTokenRequest; @@ -254,7 +249,11 @@ public class XmppConnection implements Runnable { public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; - this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext()); + this.appSettings = mXmppConnectionService.getAppSettings(); + this.presenceListener = new PresenceParser(service, account); + this.unregisteredIqListener = new IqParser(service, account); + this.messageListener = new MessageParser(service, account); + this.bindListener = new BindProcessor(service, account); } private static void fixResource(final Context context, final Account account) { @@ -412,7 +411,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); return; } - if (results.size() == 0) { + if (results.isEmpty()) { Log.e( Config.LOGTAG, account.getJid().asBareJid() + ": Resolver results were empty"); @@ -664,7 +663,7 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { - switchOverToTls(); + switchOverToTls(nextTag); } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); } else if (account.isOptionSet(Account.OPTION_REGISTER) @@ -677,8 +676,13 @@ public class XmppConnection implements Runnable { if (processSuccess(success)) { break; } - } else if (nextTag.isStart("failure")) { - final Element failure = tagReader.readElement(nextTag); + } else if (nextTag.isStart("failure", Namespace.SASL)) { + final var failure = tagReader.readElement(nextTag, Failure.class); + processFailure(failure); + } else if (nextTag.isStart("failure", Namespace.SASL_2)) { + final var failure = + tagReader.readElement( + nextTag, im.conversations.android.xmpp.model.sasl2.Failure.class); processFailure(failure); } else if (nextTag.isStart("continue", Namespace.SASL_2)) { // two step sasl2 - we don’t support this yet @@ -690,10 +694,10 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (this.streamId != null && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { - final Element resumed = tagReader.readElement(nextTag); + final Resumed resumed = tagReader.readElement(nextTag, Resumed.class); processResumed(resumed); } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) { - final Element failed = tagReader.readElement(nextTag); + final Failed failed = tagReader.readElement(nextTag, Failed.class); processFailed(failed, true); } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { processIq(nextTag); @@ -709,7 +713,7 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { processPresence(nextTag); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { - final Element enabled = tagReader.readElement(nextTag); + final var enabled = tagReader.readElement(nextTag, Enabled.class); processEnabled(enabled); } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) { tagReader.readElement(nextTag); @@ -720,7 +724,7 @@ public class XmppConnection implements Runnable { + ": acknowledging stanza #" + this.stanzasReceived); } - final AckPacket ack = new AckPacket(this.stanzasReceived); + final Ack ack = new Ack(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) { boolean accountUiNeedsRefresh = false; @@ -747,11 +751,11 @@ public class XmppConnection implements Runnable { if (accountUiNeedsRefresh) { mXmppConnectionService.updateAccountUi(); } - final Element ack = tagReader.readElement(nextTag); + final var ack = tagReader.readElement(nextTag, Ack.class); lastPacketReceived = SystemClock.elapsedRealtime(); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { - final Optional serverSequence = ack.getOptionalIntAttribute("h"); + final Optional serverSequence = ack.getHandled(); if (serverSequence.isPresent()) { acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get()); } else { @@ -787,11 +791,11 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final Element response; + final StreamElement response; if (version == SaslMechanism.Version.SASL) { - response = new Element("response", Namespace.SASL); + response = new Response(); } else if (version == SaslMechanism.Version.SASL_2) { - response = new Element("response", Namespace.SASL_2); + response = new im.conversations.android.xmpp.model.sasl2.Response(); } else { throw new AssertionError("Missing implementation for " + version); } @@ -811,26 +815,23 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(response); } - private boolean processSuccess(final Element success) + private boolean processSuccess(final Element element) throws IOException, XmlPullParserException { - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(success); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } final LoginInfo currentLoginInfo = this.loginInfo; final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo); if (currentLoginInfo == null || currentSaslMechanism == null) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final SaslMechanism.Version version; final String challenge; - if (version == SaslMechanism.Version.SASL) { + if (element instanceof Success success) { challenge = success.getContent(); - } else if (version == SaslMechanism.Version.SASL_2) { + version = SaslMechanism.Version.SASL; + } else if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) { challenge = success.findChildContent("additional-data"); + version = SaslMechanism.Version.SASL_2; } else { - throw new AssertionError("Missing implementation for " + version); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } try { currentLoginInfo.success(challenge, sslSocketOrNull(socket)); @@ -844,47 +845,24 @@ public class XmppConnection implements Runnable { if (SaslMechanism.pin(currentSaslMechanism)) { account.setPinnedMechanism(currentSaslMechanism); } - if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = - success.findChildContent("authorization-identifier"); - final Jid authorizationJid; - try { - authorizationJid = - Strings.isNullOrEmpty(authorizationIdentifier) - ? null - : Jid.ofEscaped(authorizationIdentifier); - } catch (final IllegalArgumentException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": SASL 2.0 authorization identifier was not a valid jid"); - throw new StateChangingException(Account.State.BIND_FAILURE); - } - if (authorizationJid == null) { - throw new StateChangingException(Account.State.BIND_FAILURE); - } + if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) { + final var authorizationJid = success.getAuthorizationIdentifier(); + checkAssignedDomainOrThrow(authorizationJid); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": SASL 2.0 authorization identifier was " + authorizationJid); - if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server tried to re-assign domain to " - + authorizationJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } + // TODO this should only happen when we used Bind 2 if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); } - final Element bound = success.findChild("bound", Namespace.BIND2); - final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT); - final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT); + final Bound bound = success.getExtension(Bound.class); + final Resumed resumed = success.getExtension(Resumed.class); + final Failed failed = success.getExtension(Failed.class); final Element tokenWrapper = success.findChild("token", Namespace.FAST); final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { @@ -911,8 +889,7 @@ public class XmppConnection implements Runnable { this.isBound = true; processNopStreamFeatures(); this.boundStreamFeatures = this.streamFeatures; - final Element streamManagementEnabled = - bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); + final Enabled streamManagementEnabled = bound.getExtension(Enabled.class); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); final boolean waitForDisco; if (streamManagementEnabled != null) { @@ -931,7 +908,8 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons (via Bind 2.0)"); features.carbonsEnabled = true; - } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { + } else if (currentLoginInfo.inlineBindFeatures != null + && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { negotiatedCarbons = true; Log.d( Config.LOGTAG, @@ -997,7 +975,7 @@ public class XmppConnection implements Runnable { private void resetOutboundStanzaQueue() { synchronized (this.mStanzaQueue) { - final ImmutableList.Builder intermediateStanzasBuilder = + final ImmutableList.Builder intermediateStanzasBuilder = new ImmutableList.Builder<>(); if (Config.EXTENDED_SM_LOGGING) { Log.d( @@ -1007,7 +985,7 @@ public class XmppConnection implements Runnable { + this.stanzasSentBeforeAuthentication); } for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) { - final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i); + final Stanza stanza = this.mStanzaQueue.get(i); if (stanza != null) { intermediateStanzasBuilder.add(stanza); } @@ -1031,7 +1009,9 @@ public class XmppConnection implements Runnable { private void processNopStreamFeatures() throws IOException { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { - this.streamFeatures = tagReader.readElement(tag); + this.streamFeatures = + tagReader.readElement( + tag, im.conversations.android.xmpp.model.streams.Features.class); Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1047,7 +1027,7 @@ public class XmppConnection implements Runnable { } } - private void processFailure(final Element failure) throws IOException { + private void processFailure(final AuthenticationFailure failure) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); @@ -1061,10 +1041,21 @@ public class XmppConnection implements Runnable { account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); } - if (failure.hasChild("temporary-auth-failure")) { + final var errorCondition = failure.getErrorCondition(); + if (errorCondition instanceof SaslError.InvalidMechanism + || errorCondition instanceof SaslError.MechanismTooWeak) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": invalid or too weak mechanism. resetting quick start"); + if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } else if (errorCondition instanceof SaslError.TemporaryAuthFailure) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); + } else if (errorCondition instanceof SaslError.AccountDisabled) { + final String text = failure.getText(); if (Strings.isNullOrEmpty(text)) { throw new StateChangingException(Account.State.UNAUTHORIZED); } @@ -1101,22 +1092,8 @@ public class XmppConnection implements Runnable { } } - private void processEnabled(final Element enabled) { - final String id; - if (enabled.getAttributeAsBoolean("resume")) { - id = enabled.getAttribute("id"); - } else { - id = null; - } - final String locationAttribute = enabled.getAttribute("location"); - final Resolver.Result currentResolverResult = this.currentResolverResult; - final Resolver.Result location; - if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) { - location = null; - } else { - location = currentResolverResult.seeOtherHost(locationAttribute); - } - final StreamId streamId = id == null ? null : new StreamId(id, location); + private void processEnabled(final Enabled enabled) { + final StreamId streamId = getStreamId(enabled); if (streamId == null) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled"); } else { @@ -1129,16 +1106,30 @@ public class XmppConnection implements Runnable { this.streamId = streamId; this.stanzasReceived = 0; this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(); + final var r = new Request(); tagWriter.writeStanzaAsync(r); } - private void processResumed(final Element resumed) throws StateChangingException { + @Nullable + private StreamId getStreamId(final Enabled enabled) { + final Optional id = enabled.getResumeId(); + final String locationAttribute = enabled.getLocation(); + final Resolver.Result currentResolverResult = this.currentResolverResult; + final Resolver.Result location; + if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) { + location = null; + } else { + location = currentResolverResult.seeOtherHost(locationAttribute); + } + return id.isPresent() ? new StreamId(id.get(), location) : null; + } + + private void processResumed(final Resumed resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket()); + this.tagWriter.writeStanzaAsync(new Request()); lastPacketReceived = SystemClock.elapsedRealtime(); - final Optional h = resumed.getOptionalIntAttribute("h"); + final Optional h = resumed.getHandled(); final int serverCount; if (h.isPresent()) { serverCount = h.get(); @@ -1146,7 +1137,7 @@ public class XmppConnection implements Runnable { resetStreamId(); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final ArrayList failedStanzas = new ArrayList<>(); + final ArrayList failedStanzas = new ArrayList<>(); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { if (serverCount < stanzasSent) { @@ -1169,8 +1160,8 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas"); - for (final AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket message) { + for (final Stanza packet : failedStanzas) { + if (packet instanceof im.conversations.android.xmpp.model.stanza.Message message) { mXmppConnectionService.markMessage( account, message.getTo().asBareJid(), @@ -1203,8 +1194,8 @@ public class XmppConnection implements Runnable { } } - private void processFailed(final Element failed, final boolean sendBindRequest) { - final Optional serverCount = failed.getOptionalIntAttribute("h"); + private void processFailed(final Failed failed, final boolean sendBindRequest) { + final Optional serverCount = failed.getHandled(); if (serverCount.isPresent()) { Log.d( Config.LOGTAG, @@ -1251,8 +1242,9 @@ public class XmppConnection implements Runnable { + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i)); } - final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket packet && acknowledgedListener != null) { + final Stanza stanza = mStanzaQueue.valueAt(i); + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet + && acknowledgedListener != null) { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { @@ -1267,29 +1259,9 @@ public class XmppConnection implements Runnable { return acknowledgedMessages; } - private @NonNull Element processPacket(final Tag currentTag, final int packetType) + private @NonNull S processPacket(final Tag currentTag, final Class clazz) throws IOException { - final Element element = - switch (packetType) { - case PACKET_IQ -> new IqPacket(); - case PACKET_MESSAGE -> new MessagePacket(); - case PACKET_PRESENCE -> new PresencePacket(); - default -> throw new AssertionError("Should never encounter invalid type"); - }; - element.setAttributes(currentTag.getAttributes()); - Tag nextTag = tagReader.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - while (!nextTag.isEnd(element.getName())) { - if (!nextTag.isNo()) { - element.addChild(tagReader.readElement(nextTag)); - } - nextTag = tagReader.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - } + final S stanza = tagReader.readElement(currentTag, clazz); if (stanzasReceived == Integer.MAX_VALUE) { resetStreamId(); throw new IOException("time to restart the session. cant handle >2 billion pcks"); @@ -1301,25 +1273,19 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": not counting stanza(" - + element.getClass().getSimpleName() + + stanza.getClass().getSimpleName() + "). Not in smacks session."); } lastPacketReceived = SystemClock.elapsedRealtime(); if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) { - Log.d(Config.LOGTAG, "[background stanza] " + element); - } - if (element instanceof IqPacket - && (((IqPacket) element).getType() == IqPacket.TYPE.SET) - && element.hasChild("jingle", Namespace.JINGLE)) { - return JinglePacket.upgrade((IqPacket) element); - } else { - return element; + Log.d(Config.LOGTAG, "[background stanza] " + stanza); } + return stanza; } private void processIq(final Tag currentTag) throws IOException { - final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); - if (!packet.valid()) { + final Iq packet = processPacket(currentTag, Iq.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid iq from='" @@ -1335,9 +1301,9 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + "Not processing iq. Thread was interrupted"); return; } - if (packet instanceof JinglePacket jinglePacket && isBound) { + if (packet.hasExtension(Jingle.class) && packet.getType() == Iq.Type.SET && isBound) { if (this.jingleListener != null) { - this.jingleListener.onJinglePacketReceived(account, jinglePacket); + this.jingleListener.onJinglePacketReceived(account, packet); } } else { final var callback = getIqPacketReceivedCallback(packet); @@ -1352,7 +1318,7 @@ public class XmppConnection implements Runnable { final ScheduledFuture timeoutFuture = callback.second; try { if (timeoutFuture == null || timeoutFuture.cancel(false)) { - callback.first.onIqPacketReceived(account, packet); + callback.first.accept(packet); } } catch (final StateChangingError error) { throw new StateChangingException(error.state); @@ -1360,10 +1326,10 @@ public class XmppConnection implements Runnable { } } - private Pair getIqPacketReceivedCallback(final IqPacket stanza) + private Pair, ScheduledFuture> getIqPacketReceivedCallback(final Iq stanza) throws StateChangingException { final boolean isRequest = - stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET; + stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET; if (isRequest) { if (isBound) { return new Pair<>(this.unregisteredIqListener, null); @@ -1403,8 +1369,9 @@ public class XmppConnection implements Runnable { } private void processMessage(final Tag currentTag) throws IOException { - final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); - if (!packet.valid()) { + final var packet = + processPacket(currentTag, im.conversations.android.xmpp.model.stanza.Message.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid message from='" @@ -1421,12 +1388,12 @@ public class XmppConnection implements Runnable { + "Not processing message. Thread was interrupted"); return; } - this.messageListener.onMessagePacketReceived(account, packet); + this.messageListener.accept(packet); } private void processPresence(final Tag currentTag) throws IOException { - final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); - if (!packet.valid()) { + final var packet = processPacket(currentTag, Presence.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid presence from='" @@ -1443,17 +1410,15 @@ public class XmppConnection implements Runnable { + "Not processing presence. Thread was interrupted"); return; } - this.presenceListener.onPresencePacketReceived(account, packet); + this.presenceListener.accept(packet); } private void sendStartTLS() throws IOException { - final Tag startTLS = Tag.empty("starttls"); - startTLS.setAttribute("xmlns", Namespace.TLS); - tagWriter.writeTag(startTLS); + tagWriter.writeElement(new StartTls()); } - private void switchOverToTls() throws XmlPullParserException, IOException { - tagReader.readTag(); + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { + tagReader.readElement(currentTag, Proceed.class); final Socket socket = this.socket; final SSLSocket sslSocket = upgradeSocketToTls(socket); this.socket = sslSocket; @@ -1525,11 +1490,13 @@ public class XmppConnection implements Runnable { } private void processStreamFeatures(final Tag currentTag) throws IOException { - this.streamFeatures = tagReader.readElement(currentTag); + this.streamFeatures = + tagReader.readElement( + currentTag, im.conversations.android.xmpp.model.streams.Features.class); final boolean isSecure = isSecure(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { - if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + if (this.streamFeatures.hasStreamFeature(Authentication.class)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1538,8 +1505,7 @@ public class XmppConnection implements Runnable { if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { return; } - if (isFastTokenAvailable( - this.streamFeatures.findChild("authentication", Namespace.SASL_2))) { + if (isFastTokenAvailable(this.streamFeatures.getExtension(Authentication.class))) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1557,8 +1523,7 @@ public class XmppConnection implements Runnable { mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - if (this.streamFeatures.hasChild("starttls", Namespace.TLS) - && !features.encryptionEnabled) { + if (this.streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { @@ -1575,15 +1540,15 @@ public class XmppConnection implements Runnable { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2) + } else if (this.streamFeatures.hasStreamFeature(Authentication.class) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); - } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL) + } else if (this.streamFeatures.hasStreamFeature(Mechanisms.class) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) + } else if (this.streamFeatures.streamManagement() && isSecure && LoginInfo.isSuccess(loginInfo) && streamId != null @@ -1595,7 +1560,7 @@ public class XmppConnection implements Runnable { + ": resuming after stanza #" + stanzasReceived); } - final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); + final var resume = new Resume(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); @@ -1623,9 +1588,9 @@ public class XmppConnection implements Runnable { private void authenticate() throws IOException { final boolean isSecure = isSecure(); - if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + if (isSecure && this.streamFeatures.hasStreamFeature(Authentication.class)) { authenticate(SaslMechanism.Version.SASL_2); - } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { + } else if (isSecure && this.streamFeatures.hasStreamFeature(Mechanisms.class)) { authenticate(SaslMechanism.Version.SASL); } else { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); @@ -1637,13 +1602,13 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element authElement; + final AuthenticationStreamFeature authElement; if (version == SaslMechanism.Version.SASL) { - authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + authElement = this.streamFeatures.getExtension(Mechanisms.class); } else { - authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + authElement = this.streamFeatures.getExtension(Authentication.class); } - final Collection mechanisms = SaslMechanism.mechanisms(authElement); + final Collection mechanisms = authElement.getMechanismNames(); final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); final Collection channelBindings = ChannelBinding.of(cbElement); @@ -1655,26 +1620,28 @@ public class XmppConnection implements Runnable { final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); final boolean usingFast = SaslMechanism.hashedToken(saslMechanism); - final Element authenticate; + final AuthenticationRequest authenticate; + final LoginInfo loginInfo; if (version == SaslMechanism.Version.SASL) { - authenticate = new Element("auth", Namespace.SASL); + authenticate = new Auth(); if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.setContent(firstMessage); } quickStartAvailable = false; - this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList()); + loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList()); } else if (version == SaslMechanism.Version.SASL_2) { - final Element inline = authElement.findChild("inline", Namespace.SASL_2); - final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); + final Authentication authentication = (Authentication) authElement; + final var inline = authentication.getInline(); + final boolean sm = inline != null && inline.hasExtension(StreamManagement.class); final HashedToken.Mechanism hashTokenRequest; if (usingFast) { hashTokenRequest = null; - } else { - final Element fast = - inline == null ? null : inline.findChild("fast", Namespace.FAST); - final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + } else if (inline != null) { hashTokenRequest = - HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + HashedToken.Mechanism.best( + inline.getFastMechanisms(), SSLSockets.version(this.socket)); + } else { + hashTokenRequest = null; } final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = @@ -1692,7 +1659,7 @@ public class XmppConnection implements Runnable { return; } } - this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures); + loginInfo = new LoginInfo(saslMechanism, version, bindFeatures); this.hashTokenRequest = hashTokenRequest; authenticate = generateAuthenticationRequest( @@ -1700,7 +1667,7 @@ public class XmppConnection implements Runnable { } else { throw new AssertionError("Missing implementation for " + version); } - + this.loginInfo = loginInfo; if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { mXmppConnectionService.databaseBackend.updateAccount(account); } @@ -1711,17 +1678,17 @@ public class XmppConnection implements Runnable { + ": Authenticating with " + version + "/" - + LoginInfo.mechanism(this.loginInfo).getMechanism()); - authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism()); + + LoginInfo.mechanism(loginInfo).getMechanism()); + authenticate.setMechanism(LoginInfo.mechanism(loginInfo)); synchronized (this.mStanzaQueue) { this.stanzasSentBeforeAuthentication = this.stanzasSent; tagWriter.writeElement(authenticate); } } - private static boolean isFastTokenAvailable(final Element authentication) { - final Element inline = authentication == null ? null : authentication.findChild("inline"); - return inline != null && inline.hasChild("fast", Namespace.FAST); + private static boolean isFastTokenAvailable(final Authentication authentication) { + final var inline = authentication == null ? null : authentication.getInline(); + return inline != null && inline.hasExtension(Fast.class); } private void validate( @@ -1735,7 +1702,7 @@ public class XmppConnection implements Runnable { + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - validateRequireChannelBinding(saslMechanism); + checkRequireChannelBinding(saslMechanism); if (SaslMechanism.hashedToken(saslMechanism)) { return; } @@ -1754,7 +1721,7 @@ public class XmppConnection implements Runnable { } } - private void validateRequireChannelBinding(@NonNull final SaslMechanism mechanism) + private void checkRequireChannelBinding(@NonNull final SaslMechanism mechanism) throws StateChangingException { if (appSettings.isRequireChannelBinding()) { if (mechanism instanceof ChannelBindingMechanism) { @@ -1765,31 +1732,56 @@ public class XmppConnection implements Runnable { } } - private Element generateAuthenticationRequest( + private void checkAssignedDomainOrThrow(final Jid jid) throws StateChangingException { + if (jid == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": bind response is missing jid"); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + final var current = this.account.getJid().getDomain(); + if (jid.getDomain().equals(current)) { + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + jid.getDomain()); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + + private void checkAssignedDomain(final Jid jid) { + try { + checkAssignedDomainOrThrow(jid); + } catch (final StateChangingException e) { + throw new StateChangingError(e.state); + } + } + + private AuthenticationRequest generateAuthenticationRequest( final String firstMessage, final boolean usingFast) { return generateAuthenticationRequest( firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } - private Element generateAuthenticationRequest( + private AuthenticationRequest generateAuthenticationRequest( final String firstMessage, final boolean usingFast, final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { - final Element authenticate = new Element("authenticate", Namespace.SASL_2); + final var authenticate = new Authenticate(); if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } - final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", AccountUtils.publicDeviceId(account)); - userAgent - .addChild("software") - .setContent(mXmppConnectionService.getString(R.string.app_name)); + final var userAgent = + authenticate.addExtension( + new UserAgent( + AccountUtils.publicDeviceId( + account, appSettings.getInstallationId()))); + userAgent.setSoftware( + String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME)); if (!PhoneHelper.isEmulator()) { - userAgent - .addChild("device") - .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); } // do not include bind if 'inlineStreamManagement' is missing and we have a streamId // (because we would rather just do a normal SM/resume) @@ -1798,31 +1790,29 @@ public class XmppConnection implements Runnable { authenticate.addChild(generateBindRequest(bind)); } if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); + final var resume = new Resume(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); - authenticate.addChild(resume); + authenticate.addExtension(resume); } if (hashedTokenRequest != null) { - authenticate - .addChild("request-token", Namespace.FAST) - .setAttribute("mechanism", hashedTokenRequest.name()); + authenticate.addExtension(new RequestToken(hashedTokenRequest)); } if (usingFast) { - authenticate.addChild("fast", Namespace.FAST); + authenticate.addExtension(new Fast()); } return authenticate; } - private Element generateBindRequest(final Collection bindFeatures) { + private Bind generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); - final Element bind = new Element("bind", Namespace.BIND2); - bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); + final var bind = new Bind(); + bind.setTag(BuildConfig.APP_NAME); if (bindFeatures.contains(Namespace.CARBONS)) { - bind.addChild("enable", Namespace.CARBONS); + bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable()); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - bind.addChild(new EnablePacket()); + bind.addExtension(new Enable()); } return bind; } @@ -1830,12 +1820,12 @@ public class XmppConnection implements Runnable { private void register() { final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { - final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); + final Iq preAuthRequest = new Iq(Iq.Type.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); sendUnmodifiedIqPacket( preAuthRequest, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { sendRegistryRequest(); } else { final String error = response.getErrorCondition(); @@ -1854,21 +1844,21 @@ public class XmppConnection implements Runnable { } private void sendRegistryRequest() { - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final Iq register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER); register.setTo(account.getDomain()); sendUnmodifiedIqPacket( register, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + (packet) -> { + if (packet.getType() == Iq.Type.TIMEOUT) { return; } - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() == Iq.Type.ERROR) { throw new StateChangingError(Account.State.REGISTRATION_FAILED); } final Element query = packet.query(Namespace.REGISTER); if (query.hasChild("username") && (query.hasChild("password"))) { - final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); + final Iq register1 = new Iq(Iq.Type.SET); final Element username = new Element("username").setContent(account.getUsername()); final Element password = @@ -1876,7 +1866,7 @@ public class XmppConnection implements Runnable { register1.query(Namespace.REGISTER).addChild(username); register1.query().addChild(password); register1.setFrom(account.getJid().asBareJid()); - sendUnmodifiedIqPacket(register1, registrationResponseListener, true); + sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true); } else if (query.hasChild("x", Namespace.DATA)) { final Data data = Data.parse(query.findChild("x", Namespace.DATA)); final Element blob = query.findChild("data", "urn:xmpp:bob"); @@ -1944,6 +1934,45 @@ public class XmppConnection implements Runnable { true); } + public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) { + final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data); + this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true); + } + + private void processRegistrationResponse(final Iq response) { + if (response.getType() == Iq.Type.RESULT) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully registered new account on server"); + throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); + } else { + final Account.State state = getRegistrationFailedState(response); + throw new StateChangingError(state); + } + } + + @NonNull + private static Account.State getRegistrationFailedState(final Iq response) { + final List PASSWORD_TOO_WEAK_MESSAGES = + Arrays.asList("The password is too weak", "Please use a longer password."); + final var error = response.getError(); + final var condition = error == null ? null : error.getCondition(); + final Account.State state; + if (condition instanceof Condition.Conflict) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (condition instanceof Condition.ResourceConstraint) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (condition instanceof Condition.NotAcceptable + && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } else { + state = Account.State.REGISTRATION_FAILED; + } + return state; + } + private void setAccountCreationFailed(final String url) { final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url); if (httpUrl != null && httpUrl.isHttps()) { @@ -1987,65 +2016,42 @@ public class XmppConnection implements Runnable { } clearIqCallbacks(); if (account.getJid().isBareJid()) { - account.setResource(this.createNewResource()); + account.setResource(createNewResource()); } else { fixResource(mXmppConnectionService, account); } - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); final String resource = - Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); - iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource); + Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND + ? CryptoHelper.random(9) + : account.getResource(); + iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource); this.sendUnmodifiedIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + (packet) -> { + if (packet.getType() == Iq.Type.TIMEOUT) { return; } - final Element bind = packet.findChild("bind"); - if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { + final var bind = + packet.getExtension( + im.conversations.android.xmpp.model.bind.Bind.class); + if (bind != null && packet.getType() == Iq.Type.RESULT) { isBound = true; - final Element jid = bind.findChild("jid"); - if (jid != null && jid.getContent() != null) { - try { - Jid assignedJid = Jid.ofEscaped(jid.getContent()); - if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server tried to re-assign domain to " - + assignedJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } - if (account.setJid(assignedJid)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": jid changed during bind. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (streamFeatures.hasChild("session") - && !streamFeatures - .findChild("session") - .hasChild("optional")) { - sendStartSession(); - } else { - final boolean waitForDisco = enableStreamManagement(); - sendPostBindInitialization(waitForDisco, false); - } - return; - } catch (final IllegalArgumentException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server reported invalid jid (" - + jid.getContent() - + ") on bind"); - } - } else { + final Jid assignedJid = bind.getJid(); + checkAssignedDomain(assignedJid); + if (account.setJid(assignedJid)) { Log.d( Config.LOGTAG, - account.getJid() - + ": disconnecting because of bind failure. (no jid)"); + account.getJid().asBareJid() + + ": jid changed during bind. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (streamFeatures.hasChild("session") + && !streamFeatures.findChild("session").hasChild("optional")) { + sendStartSession(); + } else { + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } } else { Log.d( @@ -2053,23 +2059,24 @@ public class XmppConnection implements Runnable { account.getJid() + ": disconnecting because of bind failure (" + packet); + final var error = packet.getError(); + // TODO error.is(Condition) + if (packet.getType() == Iq.Type.ERROR + && error != null + && error.hasChild("conflict")) { + account.setResource(createNewResource()); + } + throw new StateChangingError(Account.State.BIND_FAILURE); } - final Element error = packet.findChild("error"); - if (packet.getType() == IqPacket.TYPE.ERROR - && error != null - && error.hasChild("conflict")) { - account.setResource(createNewResource()); - } - throw new StateChangingError(Account.State.BIND_FAILURE); }, true); } private void clearIqCallbacks() { - final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT); - final ArrayList callbacks = new ArrayList<>(); + final Iq failurePacket = new Iq(Iq.Type.TIMEOUT); + final ArrayList> callbacks = new ArrayList<>(); synchronized (this.packetCallbacks) { - if (this.packetCallbacks.size() == 0) { + if (this.packetCallbacks.isEmpty()) { return; } Log.d( @@ -2078,19 +2085,18 @@ public class XmppConnection implements Runnable { + ": clearing " + this.packetCallbacks.size() + " iq callbacks"); - final Iterator>> iterator = - this.packetCallbacks.values().iterator(); + final var iterator = this.packetCallbacks.values().iterator(); while (iterator.hasNext()) { - Pair> entry = iterator.next(); + final var entry = iterator.next(); if (entry.second.second == null || entry.second.second.cancel(false)) { callbacks.add(entry.second.first); } iterator.remove(); } } - for (OnIqPacketReceived callback : callbacks) { + for (final var callback : callbacks) { try { - callback.onIqPacketReceived(account, failurePacket); + callback.accept(failurePacket); } catch (StateChangingError error) { Log.d( Config.LOGTAG, @@ -2122,15 +2128,15 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": sending legacy session to outdated server"); - final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); + final Iq startSession = new Iq(Iq.Type.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); this.sendUnmodifiedIqPacket( startSession, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final boolean waitForDisco = enableStreamManagement(); sendPostBindInitialization(waitForDisco, false); - } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + } else if (packet.getType() != Iq.Type.TIMEOUT) { throw new StateChangingError(Account.State.SESSION_FAILURE); } }, @@ -2138,11 +2144,10 @@ public class XmppConnection implements Runnable { } private boolean enableStreamManagement() { - final boolean streamManagement = - this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); + final boolean streamManagement = this.streamFeatures.streamManagement(); if (streamManagement) { synchronized (this.mStanzaQueue) { - final EnablePacket enable = new EnablePacket(); + final var enable = new Enable(); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; mStanzaQueue.clear(); @@ -2199,13 +2204,13 @@ public class XmppConnection implements Runnable { private void sendServiceDiscoveryInfo(final Jid jid) { mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setTo(jid); iq.query("http://jabber.org/protocol/disco#info"); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { boolean advancedStreamFeaturesLoaded; synchronized (XmppConnection.this.disco) { ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); @@ -2223,7 +2228,7 @@ public class XmppConnection implements Runnable { || jid.equals(account.getJid().asBareJid()))) { enableAdvancedStreamFeatures(); } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { + } else if (packet.getType() == Iq.Type.ERROR) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -2247,7 +2252,7 @@ public class XmppConnection implements Runnable { enableAdvancedStreamFeatures(); } } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (packet.getType() != Iq.Type.TIMEOUT) { if (mPendingServiceDiscoveries.decrementAndGet() == 0 && mWaitForDisco.compareAndSet(true, false)) { finalizeBind(); @@ -2257,12 +2262,12 @@ public class XmppConnection implements Runnable { } private void discoverMamPreferences() { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); sendIqPacket( request, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Element prefs = response.findChild( "prefs", MessageArchiveService.Version.MAM_2.namespace); @@ -2277,13 +2282,13 @@ public class XmppConnection implements Runnable { } private void discoverCommands() { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(account.getDomain()); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); sendIqPacket( request, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query = response.findChild("query", Namespace.DISCO_ITEMS); if (query == null) { return; @@ -2312,17 +2317,14 @@ public class XmppConnection implements Runnable { private void finalizeBind() { this.offlineMessagesRetrieved = false; - if (bindListener != null) { - bindListener.onBind(account); - } - changeStatusToOnline(); + this.bindListener.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(), mXmppConnectionService.getIqParser()); + this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener); } for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { @@ -2338,13 +2340,13 @@ public class XmppConnection implements Runnable { private void sendServiceDiscoveryItems(final Jid server) { mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setTo(server.getDomain()); iq.query("http://jabber.org/protocol/disco#items"); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final HashSet items = new HashSet<>(); final List elements = packet.query().getChildren(); for (final Element element : elements) { @@ -2367,7 +2369,7 @@ public class XmppConnection implements Runnable { + ": could not query disco items of " + server); } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (packet.getType() != Iq.Type.TIMEOUT) { if (mPendingServiceDiscoveries.decrementAndGet() == 0 && mWaitForDisco.compareAndSet(true, false)) { finalizeBind(); @@ -2377,12 +2379,12 @@ public class XmppConnection implements Runnable { } private void sendEnableCarbons() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); iq.addChild("enable", Namespace.CARBONS); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons"); @@ -2403,6 +2405,10 @@ public class XmppConnection implements Runnable { return; } if (streamError.hasChild("conflict")) { + final var loginInfo = this.loginInfo; + if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) { + this.appSettings.resetInstallationId(); + } account.setResource(createNewResource()); Log.d( Config.LOGTAG, @@ -2410,7 +2416,7 @@ public class XmppConnection implements Runnable { + ": switching resource due to conflict (" + account.getResource() + ")"); - throw new IOException(); + throw new IOException("Closed stream due to resource conflict"); } else if (streamError.hasChild("host-unknown")) { throw new StateChangingException(Account.State.HOST_UNKNOWN); } else if (streamError.hasChild("policy-violation")) { @@ -2451,8 +2457,8 @@ public class XmppConnection implements Runnable { private void failPendingMessages(final String error) { synchronized (this.mStanzaQueue) { for (int i = 0; i < mStanzaQueue.size(); ++i) { - final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket packet) { + final Stanza stanza = mStanzaQueue.valueAt(i); + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) { final String id = packet.getId(); final Jid to = packet.getTo(); mXmppConnectionService.markMessage( @@ -2486,11 +2492,11 @@ public class XmppConnection implements Runnable { SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES); final boolean usingFast = quickStartMechanism instanceof HashedToken; - final Element authenticate = + final AuthenticationRequest authenticate = generateAuthenticationRequest( quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); - authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); + authenticate.setMechanism(quickStartMechanism); sendStartStream(true, false); synchronized (this.mStanzaQueue) { this.stanzasSentBeforeAuthentication = this.stanzasSent; @@ -2521,35 +2527,28 @@ public class XmppConnection implements Runnable { tagWriter.writeTag(stream, flush); } - private String createNewResource() { - return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true); + private static String createNewResource() { + return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3)); } - private String nextRandomId() { - return nextRandomId(false); - } - - private String nextRandomId(final boolean s) { - return CryptoHelper.random(s ? 3 : 9); - } - - public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + public String sendIqPacket(final Iq packet, final Consumer callback) { return sendIqPacket(packet, callback, null); } - public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback, Long timeout) { + public String sendIqPacket(final Iq packet, final Consumer callback, Long timeout) { packet.setFrom(account.getJid()); return this.sendUnmodifiedIqPacket(packet, callback, false, timeout); } - public String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) { + public String sendUnmodifiedIqPacket(final Iq packet, final Consumer callback, boolean force) { return sendUnmodifiedIqPacket(packet, callback, force, null); } public synchronized String sendUnmodifiedIqPacket( - final IqPacket packet, final OnIqPacketReceived callback, boolean force, Long timeout) { + final Iq packet, final Consumer callback, boolean force, Long timeout) { + // TODO if callback != null verify that type is get or set if (packet.getId() == null) { - packet.setAttribute("id", nextRandomId()); + packet.setId(CryptoHelper.random(9)); } if (callback != null) { synchronized (this.packetCallbacks) { @@ -2557,9 +2556,9 @@ public class XmppConnection implements Runnable { if (timeout != null) { timeoutFuture = SCHEDULER.schedule(() -> { synchronized (this.packetCallbacks) { - final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT); - final Pair> removedCallback = packetCallbacks.remove(packet.getId()); - if (removedCallback != null) removedCallback.second.first.onIqPacketReceived(account, failurePacket); + final var failurePacket = new Iq(Iq.Type.TIMEOUT); + final var removedCallback = packetCallbacks.remove(packet.getId()); + if (removedCallback != null) removedCallback.second.first.accept(failurePacket); } }, timeout, TimeUnit.SECONDS); } @@ -2570,19 +2569,19 @@ public class XmppConnection implements Runnable { return packet.getId(); } - public void sendMessagePacket(final MessagePacket packet) { + public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) { this.sendPacket(packet); } - public void sendPresencePacket(final PresencePacket packet) { + public void sendPresencePacket(final Presence packet) { this.sendPacket(packet); } - private synchronized void sendPacket(final AbstractStanza packet) { + private synchronized void sendPacket(final StreamElement packet) { sendPacket(packet, false); } - private synchronized void sendPacket(final AbstractStanza packet, final boolean force) { + private synchronized void sendPacket(final StreamElement packet, final boolean force) { if (stanzasSent == Integer.MAX_VALUE) { resetStreamId(); disconnect(true); @@ -2598,7 +2597,7 @@ public class XmppConnection implements Runnable { + " do not write stanza to unbound stream " + packet.toString()); } - if (packet instanceof AbstractAcknowledgeableStanza stanza) { + if (packet instanceof Stanza stanza) { if (this.mStanzaQueue.size() != 0) { int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1); if (currentHighestKey != stanzasSent) { @@ -2617,7 +2616,9 @@ public class XmppConnection implements Runnable { + stanzasSent); } this.mStanzaQueue.append(stanzasSent, stanza); - if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message + && stanza.getId() != null + && inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -2625,7 +2626,7 @@ public class XmppConnection implements Runnable { + ": requesting ack for message stanza #" + stanzasSent); } - tagWriter.writeStanzaAsync(new RequestPacket()); + tagWriter.writeStanzaAsync(new Request()); } } } @@ -2633,7 +2634,7 @@ public class XmppConnection implements Runnable { public void sendPing() { if (!r()) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setFrom(account.getJid()); iq.addChild("ping", Namespace.PING); this.sendIqPacket(iq, null); @@ -2641,18 +2642,6 @@ public class XmppConnection implements Runnable { this.lastPingSent = SystemClock.elapsedRealtime(); } - public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) { - this.messageListener = listener; - } - - public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) { - this.unregisteredIqListener = listener; - } - - public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) { - this.presenceListener = listener; - } - public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) { this.jingleListener = listener; } @@ -2661,10 +2650,6 @@ public class XmppConnection implements Runnable { this.statusListener = listener; } - public void setOnBindListener(final OnBindListener listener) { - this.bindListener = listener; - } - public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { this.acknowledgedListener = listener; } @@ -2760,7 +2745,7 @@ public class XmppConnection implements Runnable { public boolean r() { if (getFeatures().sm()) { - this.tagWriter.writeStanzaAsync(new RequestPacket()); + this.tagWriter.writeStanzaAsync(new Request()); return true; } else { return false; @@ -2838,11 +2823,11 @@ public class XmppConnection implements Runnable { } public void sendActive() { - this.sendPacket(new ActivePacket()); + this.sendPacket(new Active()); } public void sendInactive() { - this.sendPacket(new InactivePacket()); + this.sendPacket(new Inactive()); } public void resetAttemptCount(boolean resetConnectTime) { @@ -2862,11 +2847,11 @@ public class XmppConnection implements Runnable { public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) { if (trackOfflineMessageRetrieval) { - final IqPacket iqPing = new IqPacket(IqPacket.TYPE.GET); + final Iq iqPing = new Iq(Iq.Type.GET); iqPing.addChild("ping", Namespace.PING); this.sendIqPacket( iqPing, - (a, response) -> { + (response) -> { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -2882,6 +2867,20 @@ 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); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { @@ -3063,13 +3062,12 @@ public class XmppConnection implements Runnable { public boolean sm() { return streamId != null || (connection.streamFeatures != null - && connection.streamFeatures.hasChild( - "sm", Namespace.STREAM_MANAGEMENT)); + && connection.streamFeatures.streamManagement()); } public boolean csi() { return connection.streamFeatures != null - && connection.streamFeatures.hasChild("csi", Namespace.CSI); + && connection.streamFeatures.clientStateIndication(); } public boolean pep() { @@ -3088,6 +3086,21 @@ public class XmppConnection implements Runnable { } } + public boolean bind2() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty(); + } + + public boolean sasl2() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2; + } + + public String loginMechanism() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism(); + } + public boolean pepPublishOptions() { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index 9f0bd72aef..3a47934854 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -59,8 +59,8 @@ public class Data extends Element { field.setValues(values); } - public void submit(Bundle options) { - for (Field field : getFields()) { + public void submit(final Bundle options) { + for (final Field field : getFields()) { if (options.containsKey(field.getFieldName())) { field.setValue(options.getString(field.getFieldName())); } 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 7a3301bfce..de0d9a502d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -34,14 +34,13 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport; import eu.siacs.conversations.xmpp.jingle.transports.Transport; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import java.lang.ref.WeakReference; import java.security.SecureRandom; @@ -77,9 +76,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); } - public void deliverPacket(final Account account, final JinglePacket packet) { - final String sessionId = packet.getSessionId(); - final JinglePacket.Action action = packet.getAction(); + public void deliverPacket(final Account account, final Iq packet) { + final var jingle = packet.getExtension(Jingle.class); + Preconditions.checkNotNull(jingle,"Passed iq packet w/o jingle extension to Connection Manager"); + final String sessionId = jingle.getSessionId(); + final Jingle.Action action = jingle.getAction(); if (sessionId == null) { respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); return; @@ -88,13 +89,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { respondWithJingleError(account, packet, null, "bad-request", "cancel"); return; } - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet, jingle); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { existingJingleConnection.deliverPacket(packet); - } else if (action == JinglePacket.Action.SESSION_INITIATE) { + } else if (action == Jingle.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); - final Content content = packet.getJingleContent(); + final Content content = jingle.getJingleContent(); final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; @@ -165,14 +166,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { } private void sendSessionTerminate( - final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { + final Account account, final Iq request, final AbstractJingleConnection.Id id) { mXmppConnectionService.sendIqPacket( - account, request.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - sessionTermination.setTo(id.with); + account, request.generateResponse(Iq.Type.RESULT), null); + final var iq = new Iq(Iq.Type.SET); + iq.setTo(id.with); + final var sessionTermination = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId)); sessionTermination.setReason(Reason.BUSY, null); - mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + mXmppConnectionService.sendIqPacket(account, iq, null); } private boolean isUsingClearNet(final Account account) { @@ -265,11 +266,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { void respondWithJingleError( final Account account, - final IqPacket original, + final Iq original, final String jingleCondition, final String condition, final String conditionType) { - final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); + final Iq response = original.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); @@ -440,7 +441,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final int activeDevices = account.activeDevicesWithRtpCapability(); Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices); if (activeDevices == 0) { - final MessagePacket reject = + final var reject = mXmppConnectionService .getMessageGenerator() .sessionReject(from, sessionId); @@ -494,10 +495,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (remoteMsgId == null) { return; } - final MessagePacket errorMessage = new MessagePacket(); + final var errorMessage = + new im.conversations.android.xmpp.model.stanza.Message(); errorMessage.setTo(from); errorMessage.setId(remoteMsgId); - errorMessage.setType(MessagePacket.TYPE_ERROR); + errorMessage.setType(im.conversations.android.xmpp.model.stanza.Message.Type.ERROR); final Element error = errorMessage.addChild("error"); error.setAttribute("code", "404"); error.setAttribute("type", "cancel"); @@ -722,7 +724,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpSessionProposal.sessionId, RtpEndUserState.RETRACTED); } - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); writeLogMissedOutgoing( account, @@ -791,7 +793,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); return proposal; @@ -801,7 +803,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void sendJingleMessageFinish( final Contact contact, final String sessionId, final Reason reason) { final var account = contact.getAccount(); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService .getMessageGenerator() .sessionFinish(contact.getJid(), sessionId, reason); @@ -843,7 +845,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return false; } - public void deliverIbbPacket(final Account account, final IqPacket packet) { + public void deliverIbbPacket(final Account account, final Iq packet) { final String sid; final Element payload; final InbandBytestreamsTransport.PacketType packetType; @@ -869,7 +871,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Config.LOGTAG, account.getJid().asBareJid() + ": unable to deliver ibb packet. missing sid"); account.getXmppConnection() - .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null); return; } for (final AbstractJingleConnection connection : this.connections.values()) { @@ -880,11 +882,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) { account.getXmppConnection() .sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); + packet.generateResponse(Iq.Type.RESULT), null); } else { account.getXmppConnection() .sendIqPacket( - packet.generateResponse(IqPacket.TYPE.ERROR), null); + packet.generateResponse(Iq.Type.ERROR), null); } return; } @@ -895,7 +897,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Config.LOGTAG, account.getJid().asBareJid() + ": unable to deliver ibb packet with sid=" + sid); account.getXmppConnection() - .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null); } public void notifyRebound(final Account account) { @@ -946,7 +948,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getJid().asBareJid() + ": resending session proposal to " + proposal.with); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c1ed7ff507..3be3dc5183 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -45,13 +45,13 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import org.webrtc.DtmfSender; import org.webrtc.EglBase; @@ -145,24 +145,25 @@ public class JingleRtpConnection extends AbstractJingleConnection } @Override - synchronized void deliverPacket(final JinglePacket jinglePacket) { - switch (jinglePacket.getAction()) { - case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket); - case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket); - case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket); - case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket); - case CONTENT_ADD -> receiveContentAdd(jinglePacket); - case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket); - case CONTENT_REJECT -> receiveContentReject(jinglePacket); - case CONTENT_REMOVE -> receiveContentRemove(jinglePacket); - case CONTENT_MODIFY -> receiveContentModify(jinglePacket); + synchronized void deliverPacket(final Iq iq) { + final var jingle = iq.getExtension(Jingle.class); + switch (jingle.getAction()) { + case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle); + case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle); + case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle); + case SESSION_TERMINATE -> receiveSessionTerminate(iq); + case CONTENT_ADD -> receiveContentAdd(iq, jingle); + case CONTENT_ACCEPT -> receiveContentAccept(iq); + case CONTENT_REJECT -> receiveContentReject(iq, jingle); + case CONTENT_REMOVE -> receiveContentRemove(iq, jingle); + case CONTENT_MODIFY -> receiveContentModify(iq, jingle); default -> { - respondOk(jinglePacket); + respondOk(iq); Log.d( Config.LOGTAG, String.format( "%s: received unhandled jingle action %s", - id.account.getJid().asBareJid(), jinglePacket.getAction())); + id.account.getJid().asBareJid(), jingle.getAction())); } } } @@ -193,9 +194,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return webRTCWrapper.applyDtmfTone(tone); } - private void receiveSessionTerminate(final JinglePacket jinglePacket) { + private void receiveSessionTerminate(final Iq jinglePacket) { respondOk(jinglePacket); - final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); + final var jingle = jinglePacket.getExtension(Jingle.class); + final Jingle.ReasonWrapper wrapper = jingle.getReason(); final State previous = this.state; Log.d( Config.LOGTAG, @@ -224,7 +226,7 @@ public class JingleRtpConnection extends AbstractJingleConnection finish(); } - private void receiveTransportInfo(final JinglePacket jinglePacket) { + private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) { // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to // INITIALIZED only after transport-info has been received if (isInState( @@ -235,7 +237,7 @@ public class JingleRtpConnection extends AbstractJingleConnection State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { - contentMap = RtpContentMap.of(jinglePacket); + contentMap = RtpContentMap.of(jingle); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -265,7 +267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveTransportInfo( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { final Set>> candidates = contentMap.contents.entrySet(); final RtpContentMap remote = getRemoteContentMap(); @@ -304,17 +306,17 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveContentAdd(final JinglePacket jinglePacket) { + private void receiveContentAdd(final Iq iq, final Jingle jingle) { final RtpContentMap modification; try { - modification = RtpContentMap.of(jinglePacket); + modification = RtpContentMap.of(jingle); modification.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); - respondOk(jinglePacket); + respondOk(iq); webRTCWrapper.close(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; @@ -330,12 +332,12 @@ public class JingleRtpConnection extends AbstractJingleConnection new FutureCallback<>() { @Override public void onSuccess(final RtpContentMap rtpContentMap) { - receiveContentAdd(jinglePacket, rtpContentMap); + receiveContentAdd(iq, rtpContentMap); } @Override public void onFailure(@NonNull Throwable throwable) { - respondOk(jinglePacket); + respondOk(iq); final Throwable rootCause = Throwables.getRootCause(throwable); Log.d( Config.LOGTAG, @@ -349,12 +351,12 @@ public class JingleRtpConnection extends AbstractJingleConnection }, MoreExecutors.directExecutor()); } else { - terminateWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(iq); } } private void receiveContentAdd( - final JinglePacket jinglePacket, final RtpContentMap modification) { + final Iq jinglePacket, final RtpContentMap modification) { final RtpContentMap remote = getRemoteContentMap(); if (!Collections.disjoint(modification.getNames(), remote.getNames())) { respondOk(jinglePacket); @@ -406,10 +408,11 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveContentAccept(final JinglePacket jinglePacket) { + private void receiveContentAccept(final Iq jinglePacket) { + final var jingle = jinglePacket.getExtension(Jingle.class); final RtpContentMap receivedContentAccept; try { - receivedContentAccept = RtpContentMap.of(jinglePacket); + receivedContentAccept = RtpContentMap.of(jingle); receivedContentAccept.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( @@ -494,14 +497,14 @@ public class JingleRtpConnection extends AbstractJingleConnection updateEndUserState(); } - private void receiveContentModify(final JinglePacket jinglePacket) { + private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) { if (this.state != State.SESSION_ACCEPTED) { terminateWithOutOfOrder(jinglePacket); return; } final Map modification = Maps.transformEntries( - jinglePacket.getJingleContents(), (key, value) -> value.getSenders()); + jingle.getJingleContents(), (key, value) -> value.getSenders()); final boolean isInitiator = isInitiator(); final RtpContentMap currentOutgoing = this.outgoingContentAdd; final RtpContentMap remoteContentMap = this.getRemoteContentMap(); @@ -604,10 +607,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return candidateBuilder.build(); } - private void receiveContentReject(final JinglePacket jinglePacket) { + private void receiveContentReject(final Iq jinglePacket, final Jingle jingle) { final RtpContentMap receivedContentReject; try { - receivedContentReject = RtpContentMap.of(jinglePacket); + receivedContentReject = RtpContentMap.of(jingle); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, @@ -660,10 +663,10 @@ public class JingleRtpConnection extends AbstractJingleConnection + summary); } - private void receiveContentRemove(final JinglePacket jinglePacket) { + private void receiveContentRemove(final Iq jinglePacket, final Jingle jingle) { final RtpContentMap receivedContentRemove; try { - receivedContentRemove = RtpContentMap.of(jinglePacket); + receivedContentRemove = RtpContentMap.of(jingle); receivedContentRemove.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( @@ -697,8 +700,8 @@ public class JingleRtpConnection extends AbstractJingleConnection String.format( "%s only supports %s as a means to retract a not yet accepted %s", BuildConfig.APP_NAME, - JinglePacket.Action.CONTENT_REMOVE, - JinglePacket.Action.CONTENT_ADD)); + Jingle.Action.CONTENT_REMOVE, + Jingle.Action.CONTENT_ADD)); } } @@ -723,10 +726,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } this.outgoingContentAdd = null; - final JinglePacket retract = + final Iq retract = outgoingContentAdd .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_REMOVE, id.sessionId); this.send(retract); Log.d( Config.LOGTAG, @@ -782,16 +785,16 @@ public class JingleRtpConnection extends AbstractJingleConnection "content addition is receive only. we want to upgrade to 'both'"); final RtpContentMap modifiedSenders = incomingContentAdd.modifiedSenders(Content.Senders.BOTH); - final JinglePacket proposedContentModification = + final Iq proposedContentModification = modifiedSenders .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_MODIFY, id.sessionId); proposedContentModification.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, proposedContentModification, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, id.account.getJid().asBareJid() @@ -885,7 +888,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onFailure(@NonNull final Throwable throwable) { - failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable); + failureToPerformAction(Jingle.Action.CONTENT_ACCEPT, throwable); } }, MoreExecutors.directExecutor()); @@ -897,9 +900,9 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void sendContentAccept(final RtpContentMap contentAcceptMap) { - final JinglePacket jinglePacket = - contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); - send(jinglePacket); + final Iq iq = + contentAcceptMap.toJinglePacket(Jingle.Action.CONTENT_ACCEPT, id.sessionId); + send(iq); } public synchronized void rejectContentAdd() { @@ -913,20 +916,20 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void rejectContentAdd(final RtpContentMap contentMap) { - final JinglePacket jinglePacket = + final Iq iq = contentMap .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_REJECT, id.sessionId); Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": rejecting content " + ContentAddition.summary(contentMap)); - send(jinglePacket); + send(iq); } private boolean checkForIceRestart( - final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + final Iq jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); final Set existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; @@ -1005,7 +1008,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private boolean applyIceRestart( - final JinglePacket jinglePacket, + final Iq jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { @@ -1106,7 +1109,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private ListenableFuture receiveRtpContentMap( - final JinglePacket jinglePacket, final boolean expectVerification) { + final Jingle jinglePacket, final boolean expectVerification) { try { return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification); } catch (final Exception e) { @@ -1149,12 +1152,12 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveSessionInitiate(final JinglePacket jinglePacket) { + private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) { if (isInitiator()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE); return; } - final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); + final ListenableFuture future = receiveRtpContentMap(jingle, false); Futures.addCallback( future, new FutureCallback<>() { @@ -1173,7 +1176,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveSessionInitiate( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(true); @@ -1233,13 +1236,13 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveSessionAccept(final JinglePacket jinglePacket) { + private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) { if (isResponder()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT); return; } final ListenableFuture future = - receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + receiveRtpContentMap(jingle, this.omemoVerification.hasFingerprint()); Futures.addCallback( future, new FutureCallback<>() { @@ -1264,7 +1267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveSessionAccept( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); @@ -1409,7 +1412,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void failureToPerformAction( - final JinglePacket.Action action, final Throwable throwable) { + final Jingle.Action action, final Throwable throwable) { if (isTerminated()) { return; } @@ -1480,8 +1483,8 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } transitionOrThrow(State.SESSION_ACCEPTED); - final JinglePacket sessionAccept = - rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + final Iq sessionAccept = + rtpContentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } @@ -1951,8 +1954,8 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } this.transitionOrThrow(targetState); - final JinglePacket sessionInitiate = - rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final Iq sessionInitiate = + rtpContentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } @@ -2020,9 +2023,9 @@ public class JingleRtpConnection extends AbstractJingleConnection + contentName); return; } - final JinglePacket jinglePacket = - transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - send(jinglePacket); + final Iq iq = + transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + send(iq); } public RtpEndUserState getEndUserState() { @@ -2340,7 +2343,8 @@ public class JingleRtpConnection extends AbstractJingleConnection this.jingleConnectionManager.ensureConnectionIsRegistered(this); this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle); - this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled()); + // this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled()); + this.webRTCWrapper.setMicrophoneEnabledOrThrow(true); } private void acceptCallFromProposed() { @@ -2375,8 +2379,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void sendJingleMessage(final String action, final Jid to) { - final MessagePacket messagePacket = new MessagePacket(); - messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those + final var messagePacket = new im.conversations.android.xmpp.model.stanza.Message(); + messagePacket.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); // we want to carbon copy those messagePacket.setTo(to); final Element intent = messagePacket @@ -2397,7 +2401,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void sendJingleMessageFinish(final Reason reason) { final var account = id.getAccount(); - final MessagePacket messagePacket = + final var messagePacket = xmppConnectionService .getMessageGenerator() .sessionFinish(id.with, id.sessionId, reason); @@ -2556,34 +2560,34 @@ public class JingleRtpConnection extends AbstractJingleConnection private void initiateIceRestart(final RtpContentMap rtpContentMap) { final RtpContentMap transportInfo = rtpContentMap.transportInfo(); - final JinglePacket jinglePacket = - transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); - jinglePacket.setTo(id.with); + final Iq iq = + transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "initiating ice restart: " + iq); + iq.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, - jinglePacket, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + iq, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "received success to our ice restart"); setLocalContentMap(rtpContentMap); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); return; } - if (response.getType() == IqPacket.TYPE.ERROR) { + if (response.getType() == Iq.Type.ERROR) { if (isTieBreak(response)) { Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); return; } handleIqErrorResponse(response); } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } }); } - private boolean isTieBreak(final IqPacket response) { + private boolean isTieBreak(final Iq response) { final Element error = response.findChild("error"); return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS); } @@ -2604,7 +2608,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onFailure(@NonNull Throwable throwable) { - failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable); + failureToPerformAction(Jingle.Action.CONTENT_ADD, throwable); } }, MoreExecutors.directExecutor()); @@ -2612,21 +2616,21 @@ public class JingleRtpConnection extends AbstractJingleConnection private void sendContentAdd(final RtpContentMap contentAdd) { - final JinglePacket jinglePacket = - contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId); - jinglePacket.setTo(id.with); + final Iq iq = + contentAdd.toJinglePacket(Jingle.Action.CONTENT_ADD, id.sessionId); + iq.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, - jinglePacket, - (connection, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + iq, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": received ACK to our content-add"); return; } - if (response.getType() == IqPacket.TYPE.ERROR) { + if (response.getType() == Iq.Type.ERROR) { if (isTieBreak(response)) { this.outgoingContentAdd = null; Log.d(Config.LOGTAG, "received tie-break as result of our content-add"); @@ -2634,7 +2638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } handleIqErrorResponse(response); } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } }); @@ -2782,7 +2786,12 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onCallIntegrationMicrophoneEnabled(final boolean enabled) { - this.webRTCWrapper.setMicrophoneEnabled(enabled); + // this is called every time we switch audio devices. Thus it would re-enable a microphone + // that was previous disabled by the user. A proper implementation would probably be to + // track user choice and enable the microphone with a userEnabled() && + // callIntegration.isMicrophoneEnabled() condition + Log.d(Config.LOGTAG, "ignoring onCallIntegrationMicrophoneEnabled(" + enabled + ")"); + // this.webRTCWrapper.setMicrophoneEnabled(enabled); } @Override @@ -2827,13 +2836,13 @@ public class JingleRtpConnection extends AbstractJingleConnection private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) { if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(id.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); xmppConnectionService.sendIqPacket( id.account, request, - (account, response) -> { + (response) -> { final var iceServers = IceServers.parse(response); if (iceServers.isEmpty()) { Log.w( diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index b151af17e0..24e82bc4f6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -18,9 +18,9 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import im.conversations.android.xmpp.model.jingle.Jingle; import java.util.Collection; import java.util.HashMap; @@ -39,7 +39,7 @@ public class RtpContentMap extends AbstractContentMap> contents = of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { @@ -53,7 +53,7 @@ public class RtpContentMap extends AbstractContentMap> contents) { final Collection> values = contents.values(); - if (values.size() == 0) { + if (values.isEmpty()) { return false; } for (final DescriptionTransport descriptionTransport : diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/src/main/java/im/conversations/android/xmpp/Entity.java new file mode 100644 index 0000000000..a578d25078 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Entity.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp; + +import org.jxmpp.jid.Jid; + +public abstract class Entity { + + public final Jid address; + + private Entity(final Jid address) { + this.address = address; + } + + public static class DiscoItem extends Entity { + + private DiscoItem(Jid address) { + super(address); + } + } + + public static class Presence extends Entity { + + private Presence(Jid address) { + super(address); + } + } + + public static Presence presence(final Jid address) { + return new Presence(address); + } + + public static DiscoItem discoItem(final Jid address) { + return new DiscoItem(address); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java new file mode 100644 index 0000000000..b282c07910 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java @@ -0,0 +1,133 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Ordering; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public final class EntityCapabilities { + public static EntityCapsHash hash(final InfoQuery info) { + final StringBuilder s = new StringBuilder(); + final List orderedIdentities = + Ordering.from( + (Comparator) + (a, b) -> + ComparisonChain.start() + .compare( + blankNull(a.getCategory()), + blankNull(b.getCategory())) + .compare( + blankNull(a.getType()), + blankNull(b.getType())) + .compare( + blankNull(a.getLang()), + blankNull(b.getLang())) + .compare( + blankNull(a.getIdentityName()), + blankNull(b.getIdentityName())) + .result()) + .sortedCopy(info.getIdentities()); + + for (final Identity id : orderedIdentities) { + s.append(blankNull(id.getCategory())) + .append("/") + .append(blankNull(id.getType())) + .append("/") + .append(blankNull(id.getLang())) + .append("/") + .append(blankNull(id.getIdentityName())) + .append("<"); + } + + final List features = + Ordering.natural() + .sortedCopy(Collections2.transform(info.getFeatures(), Feature::getVar)); + for (final String feature : features) { + s.append(clean(feature)).append("<"); + } + + final List extensions = + Ordering.from(Comparator.comparing(Data::getFormType)) + .sortedCopy(info.getExtensions(Data.class)); + + for (final Data extension : extensions) { + s.append(clean(extension.getFormType())).append("<"); + final List fields = + Ordering.from( + Comparator.comparing( + (Field lhs) -> Strings.nullToEmpty(lhs.getFieldName()))) + .sortedCopy(extension.getFields()); + for (final Field field : fields) { + s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); + final List values = Ordering.natural().sortedCopy(field.getValues()); + for (final String value : values) { + s.append(blankNull(value)).append("<"); + } + } + } + return new EntityCapsHash( + Hashing.sha1().hashString(s.toString(), StandardCharsets.UTF_8).asBytes()); + } + + private static String clean(String s) { + return s.replace("<", "<"); + } + + private static String blankNull(String s) { + return s == null ? "" : clean(s); + } + + public abstract static class Hash { + public final byte[] hash; + + protected Hash(byte[] hash) { + this.hash = hash; + } + + public String encoded() { + return BaseEncoding.base64().encode(hash); + } + + public abstract String capabilityNode(final String node); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Hash hash1 = (Hash) o; + return Arrays.equals(hash, hash1.hash); + } + + @Override + public int hashCode() { + return Arrays.hashCode(hash); + } + } + + public static class EntityCapsHash extends Hash { + + protected EntityCapsHash(byte[] hash) { + super(hash); + } + + @Override + public String capabilityNode(String node) { + return String.format("%s#%s", node, encoded()); + } + + public static EntityCapsHash of(final String encoded) { + return new EntityCapsHash(BaseEncoding.base64().decode(encoded)); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java new file mode 100644 index 0000000000..1d8a35a68d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -0,0 +1,185 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.Ordering; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Bytes; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.Hash; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.data.Value; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Objects; + +public class EntityCapabilities2 { + + private static final char UNIT_SEPARATOR = 0x1f; + private static final char RECORD_SEPARATOR = 0x1e; + + private static final char GROUP_SEPARATOR = 0x1d; + + private static final char FILE_SEPARATOR = 0x1c; + + public static EntityCaps2Hash hash(final InfoQuery info) { + return hash(Hash.Algorithm.SHA_256, info); + } + + public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) { + final String result = algorithm(info); + final var hashFunction = toHashFunction(algorithm); + return new EntityCaps2Hash( + algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes()); + } + + private static HashFunction toHashFunction(final Hash.Algorithm algorithm) { + switch (algorithm) { + case SHA_1: + return Hashing.sha1(); + case SHA_256: + return Hashing.sha256(); + case SHA_512: + return Hashing.sha512(); + default: + throw new IllegalArgumentException("Unknown hash algorithm"); + } + } + + private static String asHex(final String message) { + return Joiner.on(' ') + .join( + Collections2.transform( + Bytes.asList(message.getBytes(StandardCharsets.UTF_8)), + b -> String.format("%02x", b))); + } + + private static String algorithm(final InfoQuery infoQuery) { + return features(infoQuery.getFeatures()) + + identities(infoQuery.getIdentities()) + + extensions(infoQuery.getExtensions(Data.class)); + } + + private static String identities(final Collection identities) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + identities, EntityCapabilities2::identity))) + + FILE_SEPARATOR; + } + + private static String identity(final Identity identity) { + return Strings.nullToEmpty(identity.getCategory()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getType()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getLang()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getIdentityName()) + + UNIT_SEPARATOR + + RECORD_SEPARATOR; + } + + private static String features(Collection features) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + features, EntityCapabilities2::feature))) + + FILE_SEPARATOR; + } + + private static String feature(final Feature feature) { + return Strings.nullToEmpty(feature.getVar()) + UNIT_SEPARATOR; + } + + private static String value(final Value value) { + return Strings.nullToEmpty(value.getContent()) + UNIT_SEPARATOR; + } + + private static String values(final Collection values) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + values, EntityCapabilities2::value))); + } + + private static String field(final Field field) { + return Strings.nullToEmpty(field.getFieldName()) + + UNIT_SEPARATOR + + values(field.getExtensions(Value.class)) + + RECORD_SEPARATOR; + } + + private static String fields(final Collection fields) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + fields, EntityCapabilities2::field))) + + GROUP_SEPARATOR; + } + + private static String extension(final Data data) { + return fields(data.getExtensions(Field.class)); + } + + private static String extensions(final Collection extensions) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + extensions, + EntityCapabilities2::extension))) + + FILE_SEPARATOR; + } + + public static class EntityCaps2Hash extends EntityCapabilities.Hash { + + public final Hash.Algorithm algorithm; + + protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) { + super(hash); + this.algorithm = algorithm; + } + + public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) { + return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded)); + } + + @Override + public String capabilityNode(String node) { + return String.format( + "%s#%s.%s", Namespace.ENTITY_CAPABILITIES_2, algorithm.toString(), encoded()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + EntityCaps2Hash that = (EntityCaps2Hash) o; + return algorithm == that.algorithm; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), algorithm); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java new file mode 100644 index 0000000000..04352b5593 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java @@ -0,0 +1,78 @@ +package im.conversations.android.xmpp; + + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.xmpp.model.Extension; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public final class ExtensionFactory { + + public static Element create(final String name, final String namespace) { + final Class clazz = of(name, namespace); + if (clazz == null) { + return new Element(name, namespace); + } + final Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(); + } catch (final NoSuchMethodException e) { + throw new IllegalStateException( + String.format("%s has no default constructor", clazz.getName()),e); + } + try { + return constructor.newInstance(); + } catch (final IllegalAccessException + | InstantiationException + | InvocationTargetException e) { + throw new IllegalStateException( + String.format("%s has inaccessible default constructor", clazz.getName()),e); + } + } + + private static Class of(final String name, final String namespace) { + return Extensions.EXTENSION_CLASS_MAP.get(new Id(name, namespace)); + } + + public static Id id(final Class clazz) { + return Extensions.EXTENSION_CLASS_MAP.inverse().get(clazz); + } + + private ExtensionFactory() {} + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("namespace", namespace) + .toString(); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java new file mode 100644 index 0000000000..81a55f18c5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java @@ -0,0 +1,112 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class NodeConfiguration implements Map { + + private static final String PERSIST_ITEMS = "pubsub#persist_items"; + private static final String ACCESS_MODEL = "pubsub#access_model"; + private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item"; + private static final String MAX_ITEMS = "pubsub#max_items"; + private static final String NOTIFY_DELETE = "pubsub#notify_delete"; + private static final String NOTIFY_RETRACT = "pubsub#notify_retract"; + + public static final NodeConfiguration OPEN = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "open") + .build()); + public static final NodeConfiguration PRESENCE = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "presence") + .build()); + public static final NodeConfiguration WHITELIST_MAX_ITEMS = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "whitelist") + .put(SEND_LAST_PUBLISHED_ITEM, "never") + .put(MAX_ITEMS, "max") + .put(NOTIFY_DELETE, Boolean.TRUE) + .put(NOTIFY_RETRACT, Boolean.TRUE) + .build()); + private final Map delegate; + + private NodeConfiguration(Map map) { + this.delegate = map; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object o) { + return this.delegate.containsKey(o); + } + + @Override + public boolean containsValue(@Nullable Object o) { + return this.delegate.containsValue(o); + } + + @Nullable + @Override + public Object get(@Nullable Object o) { + return this.delegate.get(o); + } + + @Nullable + @Override + public Object put(String s, Object o) { + return this.delegate.put(s, o); + } + + @Nullable + @Override + public Object remove(@Nullable Object o) { + return this.delegate.remove(o); + } + + @Override + public void putAll(@NonNull Map map) { + this.delegate.putAll(map); + } + + @Override + public void clear() { + this.delegate.clear(); + } + + @NonNull + @Override + public Set keySet() { + return this.delegate.keySet(); + } + + @NonNull + @Override + public Collection values() { + return this.delegate.values(); + } + + @NonNull + @Override + public Set> entrySet() { + return this.delegate.entrySet(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Page.java b/src/main/java/im/conversations/android/xmpp/Page.java new file mode 100644 index 0000000000..21aa219a44 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Page.java @@ -0,0 +1,31 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; + +public class Page { + + public final String first; + public final String last; + public final Integer count; + + public Page(String first, String last, Integer count) { + this.first = first; + this.last = last; + this.count = count; + } + + public static Page emptyWithCount(final String id, final Integer count) { + return new Page(id, id, count); + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("first", first) + .add("last", last) + .add("count", count) + .toString(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Range.java b/src/main/java/im/conversations/android/xmpp/Range.java new file mode 100644 index 0000000000..8aff5094e9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Range.java @@ -0,0 +1,40 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +public class Range { + + public final Order order; + public final String id; + + public Range(final Order order, final String id) { + this.order = order; + this.id = id; + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range range = (Range) o; + return order == range.order && Objects.equal(id, range.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(order, id); + } + + public enum Order { + NORMAL, + REVERSE + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Timestamps.java b/src/main/java/im/conversations/android/xmpp/Timestamps.java new file mode 100644 index 0000000000..0135901abf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Timestamps.java @@ -0,0 +1,44 @@ +package im.conversations.android.xmpp; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public final class Timestamps { + + private Timestamps() { + throw new IllegalStateException("Do not instantiate me"); + } + + public static long parse(final String input) throws ParseException { + if (input == null) { + throw new IllegalArgumentException("timestamp should not be null"); + } + final String timestamp = input.replace("Z", "+0000"); + final SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + final long milliseconds = getMilliseconds(timestamp); + final String formatted = + timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5); + final Date date = simpleDateFormat.parse(formatted); + if (date == null) { + throw new IllegalArgumentException("Date was null"); + } + return date.getTime() + milliseconds; + } + + private static long getMilliseconds(final String timestamp) { + if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') { + final String millis = timestamp.substring(19, timestamp.length() - 5); + try { + double fractions = Double.parseDouble("0" + millis); + return Math.round(1000 * fractions); + } catch (final NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java new file mode 100644 index 0000000000..7d6790a644 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model; + +import im.conversations.android.xmpp.model.sasl.SaslError; + +public abstract class AuthenticationFailure extends StreamElement { + + protected AuthenticationFailure(Class clazz) { + super(clazz); + } + + public SaslError getErrorCondition() { + return this.getExtension(SaslError.class); + } + + public String getText() { + return this.findChildContent("text"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java new file mode 100644 index 0000000000..b2122ab863 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; + +public abstract class AuthenticationRequest extends StreamElement{ + + + protected AuthenticationRequest(Class clazz) { + super(clazz); + } + + public abstract void setMechanism(final SaslMechanism mechanism); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java new file mode 100644 index 0000000000..3b9a037611 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model; + +import java.util.Collection; + +public abstract class AuthenticationStreamFeature extends StreamFeature{ + + public AuthenticationStreamFeature(final Class clazz) { + super(clazz); + } + + public abstract Collection getMechanismNames(); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ByteContent.java b/src/main/java/im/conversations/android/xmpp/model/ByteContent.java new file mode 100644 index 0000000000..0ca6212ff7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ByteContent.java @@ -0,0 +1,33 @@ +package im.conversations.android.xmpp.model; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Element; + +public interface ByteContent { + + String getContent(); + + default byte[] asBytes() { + final var content = this.getContent(); + if (Strings.isNullOrEmpty(content)) { + throw new IllegalStateException( + String.format("%s element is lacking content", getClass().getName())); + } + final var contentCleaned = CharMatcher.whitespace().removeFrom(content); + if (BaseEncoding.base64().canDecode(contentCleaned)) { + return BaseEncoding.base64().decode(contentCleaned); + } else { + throw new IllegalStateException( + String.format("%s element contains invalid base64", getClass().getName())); + } + } + + default void setContent(final byte[] bytes) { + setContent(BaseEncoding.base64().encode(bytes)); + } + + Element setContent(final String content); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java new file mode 100644 index 0000000000..00e2b652aa --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model; + +public abstract class DeliveryReceipt extends Extension { + + protected DeliveryReceipt(Class clazz) { + super(clazz); + } + + public abstract String getId(); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java new file mode 100644 index 0000000000..a5a7533bbc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class DeliveryReceiptRequest extends Extension { + + protected DeliveryReceiptRequest(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/Extension.java b/src/main/java/im/conversations/android/xmpp/model/Extension.java new file mode 100644 index 0000000000..0d4c50eef3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/Extension.java @@ -0,0 +1,62 @@ +package im.conversations.android.xmpp.model; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.xmpp.ExtensionFactory; + +import java.util.Collection; + +public class Extension extends Element { + + private Extension(final ExtensionFactory.Id id) { + super(id.name, id.namespace); + } + + public Extension(final Class clazz) { + this( + Preconditions.checkNotNull( + ExtensionFactory.id(clazz), + String.format( + "%s does not seem to be annotated with @XmlElement", + clazz.getName()))); + Preconditions.checkArgument( + getClass().equals(clazz), "clazz passed in constructor must match class"); + } + + public boolean hasExtension(final Class clazz) { + return Iterables.any(getChildren(), clazz::isInstance); + } + + public E getExtension(final Class clazz) { + final var extension = Iterables.find(getChildren(), clazz::isInstance, null); + if (extension == null) { + return null; + } + return clazz.cast(extension); + } + + public Collection getExtensions(final Class clazz) { + return Collections2.transform( + Collections2.filter(getChildren(), clazz::isInstance), clazz::cast); + } + + public Collection getExtensionIds() { + return Collections2.transform( + getChildren(), c -> new ExtensionFactory.Id(c.getName(), c.getNamespace())); + } + + public T addExtension(T child) { + this.addChild(child); + return child; + } + + public void addExtensions(final Collection extensions) { + for (final Extension extension : extensions) { + addExtension(extension); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/Hash.java b/src/main/java/im/conversations/android/xmpp/model/Hash.java new file mode 100644 index 0000000000..8c41add8d9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/Hash.java @@ -0,0 +1,46 @@ +package im.conversations.android.xmpp.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; + +@XmlElement(namespace = Namespace.HASHES) +public class Hash extends Extension { + public Hash() { + super(Hash.class); + } + + public Algorithm getAlgorithm() { + return Algorithm.tryParse(this.getAttribute("algo")); + } + + public void setAlgorithm(final Algorithm algorithm) { + this.setAttribute("algo", algorithm.toString()); + } + + public enum Algorithm { + SHA_1, + SHA_256, + SHA_512; + + public static Algorithm tryParse(@Nullable final String name) { + try { + return valueOf( + CaseFormat.LOWER_HYPHEN.to( + CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name))); + } catch (final IllegalArgumentException e) { + return null; + } + } + + @NonNull + @Override + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/StreamElement.java b/src/main/java/im/conversations/android/xmpp/model/StreamElement.java new file mode 100644 index 0000000000..ca5fd0053b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/StreamElement.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class StreamElement extends Extension { + + protected StreamElement(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java b/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java new file mode 100644 index 0000000000..eadd8d8c44 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class StreamFeature extends Extension{ + + public StreamFeature(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java b/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java new file mode 100644 index 0000000000..f812ec53be --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.addressing; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Address extends Extension { + public Address() { + super(Address.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java b/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java new file mode 100644 index 0000000000..3ecafc5309 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.addressing; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Addresses extends Extension { + public Addresses() { + super(Addresses.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java b/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java new file mode 100644 index 0000000000..6c504489e2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java @@ -0,0 +1,6 @@ +@XmlPackage(namespace = Namespace.ADDRESSING) +package im.conversations.android.xmpp.model.addressing; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java new file mode 100644 index 0000000000..b661bca3a3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_DATA) +public class Data extends Extension implements ByteContent { + + public Data() { + super(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java new file mode 100644 index 0000000000..f544af72fc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java @@ -0,0 +1,37 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_METADATA) +public class Info extends Extension { + + public Info() { + super(Info.class); + } + + public long getHeight() { + return this.getLongAttribute("height"); + } + + public long getWidth() { + return this.getLongAttribute("width"); + } + + public long getBytes() { + return this.getLongAttribute("bytes"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getUrl() { + return this.getAttribute("url"); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java new file mode 100644 index 0000000000..400f989572 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_METADATA) +public class Metadata extends Extension { + + public Metadata() { + super(Metadata.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java new file mode 100644 index 0000000000..2321c2e49b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java @@ -0,0 +1,60 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.PreKeyRecord; + +@XmlElement +public class Bundle extends Extension { + + public Bundle() { + super(Bundle.class); + } + + public SignedPreKey getSignedPreKey() { + return this.getExtension(SignedPreKey.class); + } + + public SignedPreKeySignature getSignedPreKeySignature() { + return this.getExtension(SignedPreKeySignature.class); + } + + public IdentityKey getIdentityKey() { + return this.getExtension(IdentityKey.class); + } + + public PreKey getRandomPreKey() { + final var preKeys = this.getExtension(PreKeys.class); + final Collection preKeyList = + preKeys == null ? Collections.emptyList() : preKeys.getExtensions(PreKey.class); + return Iterables.get(preKeyList, (int) (preKeyList.size() * Math.random()), null); + } + + public void setIdentityKey(final ECPublicKey ecPublicKey) { + final var identityKey = this.addExtension(new IdentityKey()); + identityKey.setContent(ecPublicKey); + } + + public void setSignedPreKey( + final int id, final ECPublicKey ecPublicKey, final byte[] signature) { + final var signedPreKey = this.addExtension(new SignedPreKey()); + signedPreKey.setId(id); + signedPreKey.setContent(ecPublicKey); + final var signedPreKeySignature = this.addExtension(new SignedPreKeySignature()); + signedPreKeySignature.setContent(signature); + } + + public void addPreKeys(final List preKeyRecords) { + final var preKeys = this.addExtension(new PreKeys()); + for (final PreKeyRecord preKeyRecord : preKeyRecords) { + final var preKey = preKeys.addExtension(new PreKey()); + preKey.setId(preKeyRecord.getId()); + preKey.setContent(preKeyRecord.getKeyPair().getPublicKey()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java new file mode 100644 index 0000000000..0ad10d7025 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Device extends Extension { + + public Device() { + super(Device.class); + } + + public Integer getDeviceId() { + return Ints.tryParse(Strings.nullToEmpty(this.getAttribute("id"))); + } + + public void setDeviceId(int deviceId) { + this.setAttribute("id", deviceId); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java new file mode 100644 index 0000000000..ec4fce4694 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +@XmlElement(name = "list") +public class DeviceList extends Extension { + + public DeviceList() { + super(DeviceList.class); + } + + public Collection getDevices() { + return this.getExtensions(Device.class); + } + + public Set getDeviceIds() { + return ImmutableSet.copyOf( + Collections2.filter( + Collections2.transform(getDevices(), Device::getDeviceId), + Objects::nonNull)); + } + + public void setDeviceIds(Collection deviceIds) { + for (final Integer deviceId : deviceIds) { + final var device = this.addExtension(new Device()); + device.setDeviceId(deviceId); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java new file mode 100644 index 0000000000..2008fb017d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.xmpp.model.ByteContent; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; + +public interface ECPublicKeyContent extends ByteContent { + + default ECPublicKey asECPublicKey() { + try { + return Curve.decodePoint(asBytes(), 0); + } catch (InvalidKeyException e) { + throw new IllegalStateException( + String.format("%s does not contain a valid ECPublicKey", getClass().getName()), + e); + } + } + + default void setContent(final ECPublicKey ecPublicKey) { + setContent(ecPublicKey.serialize()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java new file mode 100644 index 0000000000..1a98068ab7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Encrypted extends Extension { + + public Encrypted() { + super(Encrypted.class); + } + + public boolean hasPayload() { + return hasExtension(Payload.class); + } + + public Header getHeader() { + return getExtension(Header.class); + } + + public Payload getPayload() { + return getExtension(Payload.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java new file mode 100644 index 0000000000..91e2bd87ba --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java @@ -0,0 +1,45 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Header extends Extension { + + public Header() { + super(Header.class); + } + + public void addIv(byte[] iv) { + this.addExtension(new IV()).setContent(iv); + } + + public void setSourceDevice(long sourceDeviceId) { + this.setAttribute("sid", sourceDeviceId); + } + + public Optional getSourceDevice() { + return getOptionalIntAttribute("sid"); + } + + public Collection getKeys() { + return this.getExtensions(Key.class); + } + + public Key getKey(final int deviceId) { + return Iterables.find( + getKeys(), key -> Objects.equals(key.getRemoteDeviceId(), deviceId), null); + } + + public byte[] getIv() { + final IV iv = this.getExtension(IV.class); + if (iv == null) { + throw new IllegalStateException("No IV in header"); + } + return iv.asBytes(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java new file mode 100644 index 0000000000..22164976a9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "iv") +public class IV extends Extension implements ByteContent { + + public IV() { + super(IV.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java new file mode 100644 index 0000000000..f48fcbd7cb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "identityKey") +public class IdentityKey extends Extension implements ECPublicKeyContent { + + public IdentityKey() { + super(IdentityKey.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java new file mode 100644 index 0000000000..3ad7357b8d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Key extends Extension implements ByteContent { + + public Key() { + super(Key.class); + } + + public void setIsPreKey(boolean isPreKey) { + this.setAttribute("prekey", isPreKey); + } + + public boolean isPreKey() { + return this.getAttributeAsBoolean("prekey"); + } + + public void setRemoteDeviceId(final int remoteDeviceId) { + this.setAttribute("rid", remoteDeviceId); + } + + public Integer getRemoteDeviceId() { + return getOptionalIntAttribute("rid").orNull(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java new file mode 100644 index 0000000000..9c58701100 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Payload extends Extension implements ByteContent { + + public Payload() { + super(Payload.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java new file mode 100644 index 0000000000..a7d39c1daf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "preKeyPublic") +public class PreKey extends Extension implements ECPublicKeyContent { + + public PreKey() { + super(PreKey.class); + } + + public int getId() { + return Ints.saturatedCast(this.getLongAttribute("preKeyId")); + } + + public void setId(int id) { + this.setAttribute("preKeyId", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java new file mode 100644 index 0000000000..3613b8aa8a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "prekeys") +public class PreKeys extends Extension { + + public PreKeys() { + super(PreKeys.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java new file mode 100644 index 0000000000..0e0ca72823 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "signedPreKeyPublic") +public class SignedPreKey extends Extension implements ECPublicKeyContent { + + public SignedPreKey() { + super(SignedPreKey.class); + } + + public int getId() { + return Ints.saturatedCast(this.getLongAttribute("signedPreKeyId")); + } + + public void setId(final int id) { + this.setAttribute("signedPreKeyId", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java new file mode 100644 index 0000000000..5051cb1b14 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "signedPreKeySignature") +public class SignedPreKeySignature extends Extension implements ByteContent { + + public SignedPreKeySignature() { + super(SignedPreKeySignature.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java new file mode 100644 index 0000000000..ad3d21c16c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.AXOLOTL) +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java b/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java new file mode 100644 index 0000000000..27264f7545 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp.model.bind; + +import com.google.common.base.Strings; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bind extends Extension { + + public Bind() { + super(Bind.class); + } + + public void setResource(final String resource) { + this.addExtension(new Resource(resource)); + } + + public eu.siacs.conversations.xmpp.Jid getJid() { + final var jidExtension = this.getExtension(Jid.class); + if (jidExtension == null) { + return null; + } + final var content = jidExtension.getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } + try { + return eu.siacs.conversations.xmpp.Jid.ofEscaped(content); + } catch (final IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java b/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java new file mode 100644 index 0000000000..04633a0099 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.bind; + + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Jid extends Extension { + + public Jid() { + super(Jid.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java b/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java new file mode 100644 index 0000000000..b3fd1e5c17 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.bind; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Resource extends Extension { + public Resource() { + super(Resource.class); + } + + public Resource(final String resource) { + this(); + this.setContent(resource); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java new file mode 100644 index 0000000000..aebaeeb729 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BIND) +package im.conversations.android.xmpp.model.bind; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java new file mode 100644 index 0000000000..3af1441054 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java @@ -0,0 +1,28 @@ +package im.conversations.android.xmpp.model.bind2; + +import java.util.Collection; +import java.util.Collections; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bind extends Extension { + + public Bind() { + super(Bind.class); + } + + public Inline getInline() { + return this.getExtension(Inline.class); + } + + public Collection getInlineFeatures() { + final var inline = getInline(); + return inline == null ? Collections.emptyList() : inline.getExtensions(Feature.class); + } + + public void setTag(final String tag) { + this.addExtension(new Tag(tag)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java new file mode 100644 index 0000000000..0144edb914 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bound extends Extension { + public Bound() { + super(Bound.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java new file mode 100644 index 0000000000..66720abbc6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Feature extends Extension { + + public Feature() { + super(Feature.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java new file mode 100644 index 0000000000..641a9d4f45 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Inline extends Extension { + + public Inline() { + super(Inline.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java new file mode 100644 index 0000000000..5fac1d9e37 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Tag extends Extension { + + public Tag() { + super(Tag.class); + } + + public Tag(final String tag) { + this(); + setContent(tag); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java new file mode 100644 index 0000000000..2d8c5e92cb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BIND2) +package im.conversations.android.xmpp.model.bind2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java new file mode 100644 index 0000000000..6f5d00b3e5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Block extends Extension { + + public Block() { + super(Block.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java new file mode 100644 index 0000000000..a56662d771 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Blocklist extends Extension { + public Blocklist() { + super(Blocklist.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java new file mode 100644 index 0000000000..647b0ae991 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.blocking; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Item extends Extension { + + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java new file mode 100644 index 0000000000..90cec110c0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Unblock extends Extension { + + public Unblock() { + super(Unblock.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java b/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java new file mode 100644 index 0000000000..22d8f0e1f0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BLOCKING) +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java new file mode 100644 index 0000000000..0f924e8883 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java @@ -0,0 +1,32 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Conference extends Extension { + + public Conference() { + super(Conference.class); + } + + public boolean isAutoJoin() { + return this.getAttributeAsBoolean("autojoin"); + } + + public String getConferenceName() { + return this.getAttribute("name"); + } + + public void setAutoJoin(boolean autoJoin) { + setAttribute("autojoin", autoJoin); + } + + public Nick getNick() { + return this.getExtension(Nick.class); + } + + public Extensions getExtensions() { + return this.getExtension(Extensions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java new file mode 100644 index 0000000000..b9385cf547 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Extensions extends Extension { + + public Extensions() { + super(Extensions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java new file mode 100644 index 0000000000..ee5efa3864 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Nick extends Extension { + + public Nick() { + super(Nick.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java new file mode 100644 index 0000000000..1bb963be84 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BOOKMARKS2) +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java new file mode 100644 index 0000000000..d0f23b2833 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java @@ -0,0 +1,43 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.Hash; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES_2) +public class Capabilities extends Extension { + + public Capabilities() { + super(Capabilities.class); + } + + public EntityCapabilities2.EntityCaps2Hash getHash() { + final Optional sha256Hash = + Iterables.tryFind( + getExtensions(Hash.class), h -> h.getAlgorithm() == Hash.Algorithm.SHA_256); + if (sha256Hash.isPresent()) { + final String content = sha256Hash.get().getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } + if (BaseEncoding.base64().canDecode(content)) { + return EntityCapabilities2.EntityCaps2Hash.of(Hash.Algorithm.SHA_256, content); + } + } + return null; + } + + public void setHash(final EntityCapabilities2.EntityCaps2Hash caps2Hash) { + final Hash hash = new Hash(); + hash.setAlgorithm(caps2Hash.algorithm); + hash.setContent(caps2Hash.encoded()); + this.addExtension(hash); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java new file mode 100644 index 0000000000..f8ed4ef661 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java @@ -0,0 +1,39 @@ +package im.conversations.android.xmpp.model.capabilties; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import im.conversations.android.xmpp.model.Extension; + +public interface EntityCapabilities { + + E getExtension(final Class clazz); + + default NodeHash getCapabilities() { + final String node; + final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + final var capabilities = this.getExtension(Capabilities.class); + final var legacyCapabilities = this.getExtension(LegacyCapabilities.class); + if (capabilities != null) { + node = null; + hash = capabilities.getHash(); + } else if (legacyCapabilities != null) { + node = legacyCapabilities.getNode(); + hash = legacyCapabilities.getHash(); + } else { + return null; + } + return hash == null ? null : new NodeHash(node, hash); + } + + class NodeHash { + public final String node; + public final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + + private NodeHash( + @Nullable String node, + @NonNull final im.conversations.android.xmpp.EntityCapabilities.Hash hash) { + this.node = node; + this.hash = hash; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java new file mode 100644 index 0000000000..797d627cf1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java @@ -0,0 +1,45 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES) +public class LegacyCapabilities extends Extension { + + private static final String HASH_ALGORITHM = "sha-1"; + + public LegacyCapabilities() { + super(LegacyCapabilities.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public EntityCapabilities.EntityCapsHash getHash() { + final String hash = getAttribute("hash"); + final String ver = getAttribute("ver"); + if (Strings.isNullOrEmpty(ver) || Strings.isNullOrEmpty(hash)) { + return null; + } + if (HASH_ALGORITHM.equals(hash) && BaseEncoding.base64().canDecode(ver)) { + return EntityCapabilities.EntityCapsHash.of(ver); + } else { + return null; + } + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public void setHash(final EntityCapabilities.EntityCapsHash hash) { + this.setAttribute("hash", HASH_ALGORITHM); + this.setAttribute("ver", hash.encoded()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java new file mode 100644 index 0000000000..38b740e8c0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Enable extends Extension { + + public Enable() { + super(Enable.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java new file mode 100644 index 0000000000..507869a60c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Received extends Extension { + + public Received() { + super(Received.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java new file mode 100644 index 0000000000..0201c53c63 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Sent extends Extension { + + public Sent() { + super(Sent.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java b/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java new file mode 100644 index 0000000000..f4c76376a3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CARBONS) +package im.conversations.android.xmpp.model.carbons; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java b/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java new file mode 100644 index 0000000000..dbd7395572 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.correction; + +import androidx.annotation.NonNull; +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.LAST_MESSAGE_CORRECTION) +public class Replace extends Extension { + + public Replace() { + super(Replace.class); + } + + public String getId() { + return Strings.emptyToNull(this.getAttribute("id")); + } + + public void setId(@NonNull final String id) { + this.setAttribute("id", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Active.java b/src/main/java/im/conversations/android/xmpp/model/csi/Active.java new file mode 100644 index 0000000000..21fb65bb4a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/Active.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Active extends StreamElement { + + public Active() { + super(Active.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java b/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java new file mode 100644 index 0000000000..60bd59edb1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement(name = "csi") +public class ClientStateIndication extends StreamFeature { + + public ClientStateIndication() { + super(ClientStateIndication.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java b/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java new file mode 100644 index 0000000000..7c36b593d5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Inactive extends StreamElement { + + public Inactive() { + super(Inactive.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java b/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java new file mode 100644 index 0000000000..58d26b1f13 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CSI) +package im.conversations.android.xmpp.model.csi; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java new file mode 100644 index 0000000000..c754ee48de --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -0,0 +1,110 @@ +package im.conversations.android.xmpp.model.data; + +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Map; + +@XmlElement(name = "x") +public class Data extends Extension { + + private static final String FORM_TYPE = "FORM_TYPE"; + private static final String FIELD_TYPE_HIDDEN = "hidden"; + private static final String FORM_TYPE_SUBMIT = "submit"; + + public Data() { + super(Data.class); + } + + public String getFormType() { + final var fields = this.getExtensions(Field.class); + final var formTypeField = Iterables.find(fields, f -> FORM_TYPE.equals(f.getFieldName())); + return Iterables.getFirst(formTypeField.getValues(), null); + } + + public Collection getFields() { + return Collections2.filter( + this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); + } + + private void addField(final String name, final Object value) { + addField(name, value, null); + } + + private void addField(final String name, final Object value, final String type) { + if (value == null) { + throw new IllegalArgumentException("Null values are not supported on data fields"); + } + final var field = this.addExtension(new Field()); + field.setFieldName(name); + if (type != null) { + field.setType(type); + } + if (value instanceof Collection) { + for (final Object subValue : (Collection) value) { + if (subValue instanceof String) { + final var valueExtension = field.addExtension(new Value()); + valueExtension.setContent((String) subValue); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not a supported field value", + subValue.getClass().getSimpleName())); + } + } + } else { + final var valueExtension = field.addExtension(new Value()); + if (value instanceof String) { + valueExtension.setContent((String) value); + } else if (value instanceof Integer) { + valueExtension.setContent(String.valueOf(value)); + } else if (value instanceof Boolean) { + valueExtension.setContent(Boolean.TRUE.equals(value) ? "1" : "0"); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not a supported field value", + value.getClass().getSimpleName())); + } + } + } + + private void setFormType(final String formType) { + this.addField(FORM_TYPE, formType, FIELD_TYPE_HIDDEN); + } + + public static Data of(final String formType, final Map values) { + final var data = new Data(); + data.setType(FORM_TYPE_SUBMIT); + data.setFormType(formType); + for (final Map.Entry entry : values.entrySet()) { + data.addField(entry.getKey(), entry.getValue()); + } + return data; + } + + public Data submit(final Map values) { + final String formType = this.getFormType(); + final var submit = new Data(); + submit.setType(FORM_TYPE_SUBMIT); + if (formType != null) { + submit.setFormType(formType); + } + for (final Field existingField : this.getFields()) { + final var fieldName = existingField.getFieldName(); + final Object submittedValue = values.get(fieldName); + if (submittedValue != null) { + submit.addField(fieldName, submittedValue); + } else { + submit.addField(fieldName, existingField.getValues()); + } + } + return submit; + } + + private void setType(final String type) { + this.setAttribute("type", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java new file mode 100644 index 0000000000..f3f72fab86 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.data; +import eu.siacs.conversations.xml.Element; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; + +@XmlElement +public class Field extends Extension { + public Field() { + super(Field.class); + } + + public String getFieldName() { + return getAttribute("var"); + } + + public Collection getValues() { + return Collections2.transform(getExtensions(Value.class), Element::getContent); + } + + public void setFieldName(String name) { + this.setAttribute("var", name); + } + + public void setType(String type) { + this.setAttribute("type", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Option.java b/src/main/java/im/conversations/android/xmpp/model/data/Option.java new file mode 100644 index 0000000000..b9c3e9aae4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Option.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.data; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Option extends Extension { + + public Option() { + super(Option.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Value.java b/src/main/java/im/conversations/android/xmpp/model/data/Value.java new file mode 100644 index 0000000000..8e9eccc4d7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Value.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.data; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Value extends Extension { + + public Value() { + super(Value.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/package-info.java b/src/main/java/im/conversations/android/xmpp/model/data/package-info.java new file mode 100644 index 0000000000..fcc0e1f790 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DATA) +package im.conversations.android.xmpp.model.data; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java b/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java new file mode 100644 index 0000000000..b294f83d45 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java @@ -0,0 +1,30 @@ +package im.conversations.android.xmpp.model.delay; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.Timestamps; +import im.conversations.android.xmpp.model.Extension; +import java.text.ParseException; +import java.time.Instant; + +@XmlElement(namespace = Namespace.DELAY) +public class Delay extends Extension { + + public Delay() { + super(Delay.class); + } + + public Instant getStamp() { + final var stamp = this.getAttribute("stamp"); + if (Strings.isNullOrEmpty(stamp)) { + return null; + } + try { + return Instant.ofEpochMilli(Timestamps.parse(stamp)); + } catch (final IllegalArgumentException | ParseException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java new file mode 100644 index 0000000000..86a93af0dc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.disco.external; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Service extends Extension { + + public Service() { + super(Service.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java new file mode 100644 index 0000000000..36338083da --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.disco.external; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Services extends Extension { + + public Services() { + super(Services.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java new file mode 100644 index 0000000000..868a5a1753 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.EXTERNAL_SERVICE_DISCOVERY) +package im.conversations.android.xmpp.model.disco.external; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java new file mode 100644 index 0000000000..dd288918cb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.disco.info; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Feature extends Extension { + public Feature() { + super(Feature.class); + } + + public String getVar() { + return this.getAttribute("var"); + } + + public void setVar(final String feature) { + this.setAttribute("var", feature); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java new file mode 100644 index 0000000000..6da0a4aa22 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java @@ -0,0 +1,39 @@ +package im.conversations.android.xmpp.model.disco.info; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Identity extends Extension { + public Identity() { + super(Identity.class); + } + + public String getCategory() { + return this.getAttribute("category"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getLang() { + return this.getAttribute("xml:lang"); + } + + public String getIdentityName() { + return this.getAttribute("name"); + } + + public void setIdentityName(final String name) { + this.setAttribute("name", name); + } + + public void setType(final String type) { + this.setAttribute("type", type); + } + + public void setCategory(final String category) { + this.setAttribute("category", category); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java new file mode 100644 index 0000000000..55f104e25b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -0,0 +1,38 @@ +package im.conversations.android.xmpp.model.disco.info; + +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; + +@XmlElement(name = "query") +public class InfoQuery extends Extension { + + public InfoQuery() { + super(InfoQuery.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getFeatures() { + return this.getExtensions(Feature.class); + } + + public boolean hasFeature(final String feature) { + return Iterables.any(getFeatures(), f -> feature.equals(f.getVar())); + } + + public Collection getIdentities() { + return this.getExtensions(Identity.class); + } + + public boolean hasIdentityWithCategory(final String category) { + return Iterables.any(getIdentities(), i -> category.equals(i.getCategory())); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java new file mode 100644 index 0000000000..60eb24a598 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DISCO_INFO) +package im.conversations.android.xmpp.model.disco.info; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java new file mode 100644 index 0000000000..f5bf2b9840 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.disco.items; + +import androidx.annotation.Nullable; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Item extends Extension { + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } + + public @Nullable String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java new file mode 100644 index 0000000000..981132ed69 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.disco.items; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query") +public class ItemsQuery extends Extension { + public ItemsQuery() { + super(ItemsQuery.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java new file mode 100644 index 0000000000..a170e5cee3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DISCO_ITEMS) +package im.conversations.android.xmpp.model.disco.items; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Condition.java b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java new file mode 100644 index 0000000000..bd68c2c433 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java @@ -0,0 +1,188 @@ +package im.conversations.android.xmpp.model.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public abstract class Condition extends Extension { + + private Condition(Class clazz) { + super(clazz); + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class BadRequest extends Condition { + + public BadRequest() { + super(BadRequest.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Conflict extends Condition { + + public Conflict() { + super(Conflict.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class FeatureNotImplemented extends Condition { + + public FeatureNotImplemented() { + super(FeatureNotImplemented.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Forbidden extends Condition { + + public Forbidden() { + super(Forbidden.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Gone extends Condition { + + public Gone() { + super(Gone.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class InternalServerError extends Condition { + + public InternalServerError() { + super(InternalServerError.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ItemNotFound extends Condition { + + public ItemNotFound() { + super(ItemNotFound.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class JidMalformed extends Condition { + + public JidMalformed() { + super(JidMalformed.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAcceptable extends Condition { + + public NotAcceptable() { + super(NotAcceptable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAllowed extends Condition { + + public NotAllowed() { + super(NotAllowed.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAuthorized extends Condition { + + public NotAuthorized() { + super(NotAuthorized.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class PaymentRequired extends Condition { + + public PaymentRequired() { + super(PaymentRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RecipientUnavailable extends Condition { + + public RecipientUnavailable() { + super(RecipientUnavailable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Redirect extends Condition { + + public Redirect() { + super(Redirect.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RegistrationRequired extends Condition { + + public RegistrationRequired() { + super(RegistrationRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RemoteServerNotFound extends Condition { + + public RemoteServerNotFound() { + super(RemoteServerNotFound.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RemoteServerTimeout extends Condition { + + public RemoteServerTimeout() { + super(RemoteServerTimeout.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ResourceConstraint extends Condition { + + public ResourceConstraint() { + super(ResourceConstraint.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ServiceUnavailable extends Condition { + + public ServiceUnavailable() { + super(ServiceUnavailable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class SubscriptionRequired extends Condition { + + public SubscriptionRequired() { + super(SubscriptionRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class UndefinedCondition extends Condition { + + public UndefinedCondition() { + super(UndefinedCondition.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class UnexpectedRequest extends Condition { + + public UnexpectedRequest() { + super(UnexpectedRequest.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Error.java b/src/main/java/im/conversations/android/xmpp/model/error/Error.java new file mode 100644 index 0000000000..0a07e73f9d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Error.java @@ -0,0 +1,55 @@ +package im.conversations.android.xmpp.model.error; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Locale; +import eu.siacs.conversations.xml.Namespace; + +@XmlElement(namespace = Namespace.JABBER_CLIENT) +public class Error extends Extension { + + public Error() { + super(Error.class); + } + + public Condition getCondition() { + return this.getExtension(Condition.class); + } + + public void setCondition(final Condition condition) { + this.addExtension(condition); + } + + public Text getText() { + return this.getExtension(Text.class); + } + + public String getTextAsString() { + final var text = getText(); + return text == null ? null : text.getContent(); + } + + public void setType(final Type type) { + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + + public void addExtensions(final Extension[] extensions) { + for (final Extension extension : extensions) { + this.addExtension(extension); + } + } + + public enum Type { + MODIFY, + CANCEL, + AUTH, + WAIT + } + + public static class Extension extends im.conversations.android.xmpp.model.Extension { + + public Extension(Class clazz) { + super(clazz); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Text.java b/src/main/java/im/conversations/android/xmpp/model/error/Text.java new file mode 100644 index 0000000000..478b1f5cd5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Text.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.STANZAS) +public class Text extends Extension { + + public Text() { + super(Text.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java b/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java new file mode 100644 index 0000000000..1291d8ea06 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Fast extends Extension { + public Fast() { + super(Fast.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java new file mode 100644 index 0000000000..240f5de0e1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java b/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java new file mode 100644 index 0000000000..4ac5a9205f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.fast; + +import eu.siacs.conversations.crypto.sasl.HashedToken; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class RequestToken extends Extension { + public RequestToken() { + super(RequestToken.class); + } + + public RequestToken(final HashedToken.Mechanism mechanism) { + this(); + this.setAttribute("mechanism", mechanism.name()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Token.java b/src/main/java/im/conversations/android/xmpp/model/fast/Token.java new file mode 100644 index 0000000000..258cd9abad --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Token.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Token extends Extension { + + public Token() { + super(Token.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java b/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java new file mode 100644 index 0000000000..effc9e511c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.FAST) +package im.conversations.android.xmpp.model.fast; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java new file mode 100644 index 0000000000..80a646a41d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.forward; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.stanza.Message; + +@XmlElement(namespace = Namespace.FORWARD) +public class Forwarded extends Extension { + + public Forwarded() { + super(Forwarded.class); + } + + public Message getMessage() { + return this.getExtension(Message.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/Store.java b/src/main/java/im/conversations/android/xmpp/model/hints/Store.java new file mode 100644 index 0000000000..fe82612adc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/Store.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.hints; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Store extends Extension { + + public Store() { + super(Store.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java b/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java new file mode 100644 index 0000000000..76c25d6551 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java @@ -0,0 +1,6 @@ +@XmlPackage(namespace = Namespace.HINTS) +package im.conversations.android.xmpp.model.hints; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java new file mode 100644 index 0000000000..5857f05851 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Body extends Extension { + + public Body() { + super(Body.class); + } + + public Body(final String content) { + this(); + setContent(content); + } + + public String getLang() { + return this.getAttribute("xml:lang"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java new file mode 100644 index 0000000000..7c5b3bdc98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Priority extends Extension { + + public Priority() { + super(Priority.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java new file mode 100644 index 0000000000..44dc512be2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Show extends Extension { + public Show() { + super(Show.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java new file mode 100644 index 0000000000..3175230d71 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Status extends Extension { + + + public Status() { + super(Status.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java new file mode 100644 index 0000000000..4ae3b8ed51 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Subject extends Extension { + + public Subject() { + super(Subject.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java new file mode 100644 index 0000000000..703429ef0b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Thread extends Extension { + + public Thread() { + super(Thread.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java new file mode 100644 index 0000000000..448804d7c8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JABBER_CLIENT) +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java b/src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java new file mode 100644 index 0000000000..aeb79ffd2f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java @@ -0,0 +1,156 @@ +package im.conversations.android.xmpp.model.jingle; + +import androidx.annotation.NonNull; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +import java.util.Map; + +@XmlElement +public class Jingle extends Extension { + + public Jingle() { + super(Jingle.class); + } + + public Jingle(final Action action, final String sessionId) { + this(); + this.setAttribute("sid", sessionId); + this.setAttribute("action", action.toString()); + } + + public String getSessionId() { + return this.getAttribute("sid"); + } + + public Action getAction() { + return Action.of(this.getAttribute("action")); + } + + public ReasonWrapper getReason() { + final Element reasonElement = this.findChild("reason"); + if (reasonElement == null) { + return new ReasonWrapper(Reason.UNKNOWN, null); + } + String text = null; + Reason reason = Reason.UNKNOWN; + for (Element child : reasonElement.getChildren()) { + if ("text".equals(child.getName())) { + text = child.getContent(); + } else { + reason = Reason.of(child.getName()); + } + } + return new ReasonWrapper(reason, text); + } + + public void setReason(final Reason reason, final String text) { + final Element reasonElement = this.addChild("reason"); + reasonElement.addChild(reason.toString()); + if (!Strings.isNullOrEmpty(text)) { + reasonElement.addChild("text").setContent(text); + } + } + + // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise + public void setInitiator(final Jid initiator) { + Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID"); + this.setAttribute("initiator", initiator); + } + + // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise + public void setResponder(final Jid responder) { + Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID"); + this.setAttribute("responder", responder); + } + + public Group getGroup() { + final Element group = this.findChild("group", Namespace.JINGLE_APPS_GROUPING); + return group == null ? null : Group.upgrade(group); + } + + public void addGroup(final Group group) { + this.addChild(group); + } + + // TODO deprecate this somehow and make file transfer fail if there are multiple (or something) + public Content getJingleContent() { + final Element content = this.findChild("content"); + return content == null ? null : Content.upgrade(content); + } + + public void addJingleContent(final Content content) { // take content interface + this.addChild(content); + } + + + public Map getJingleContents() { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final Element child : this.getChildren()) { + if ("content".equals(child.getName())) { + final Content content = Content.upgrade(child); + builder.put(content.getContentName(), content); + } + } + return builder.build(); + } + + public enum Action { + CONTENT_ACCEPT, + CONTENT_ADD, + CONTENT_MODIFY, + CONTENT_REJECT, + CONTENT_REMOVE, + DESCRIPTION_INFO, + SECURITY_INFO, + SESSION_ACCEPT, + SESSION_INFO, + SESSION_INITIATE, + SESSION_TERMINATE, + TRANSPORT_ACCEPT, + TRANSPORT_INFO, + TRANSPORT_REJECT, + TRANSPORT_REPLACE; + + public static Action of(final String value) { + if (Strings.isNullOrEmpty(value)) { + return null; + } + try { + return Action.valueOf( + CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + } catch (final IllegalArgumentException e) { + return null; + } + } + + @Override + @NonNull + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } + } + + public static class ReasonWrapper { + public final Reason reason; + public final String text; + + public ReasonWrapper(Reason reason, String text) { + this.reason = reason; + this.text = text; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java b/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java new file mode 100644 index 0000000000..a0c6dfbbd7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java @@ -0,0 +1,44 @@ +package im.conversations.android.xmpp.model.jingle.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.error.Error; + +public abstract class JingleCondition extends Error.Extension { + + private JingleCondition(Class clazz) { + super(clazz); + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class OutOfOrder extends JingleCondition { + + public OutOfOrder() { + super(OutOfOrder.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class TieBreak extends JingleCondition { + + public TieBreak() { + super(TieBreak.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class UnknownSession extends JingleCondition { + + public UnknownSession() { + super(UnknownSession.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class UnsupportedInfo extends JingleCondition { + + public UnsupportedInfo() { + super(UnsupportedInfo.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java new file mode 100644 index 0000000000..6af2511e8d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JINGLE) +package im.conversations.android.xmpp.model.jingle; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java new file mode 100644 index 0000000000..20ae15aee3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Accept extends JingleMessage { + + public Accept() { + super(Accept.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java b/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java new file mode 100644 index 0000000000..0045844f0a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class JingleMessage extends Extension { + + public JingleMessage(Class clazz) { + super(clazz); + } + + public String getSessionId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java new file mode 100644 index 0000000000..b6be44ee09 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.jmi; + +import com.google.common.primitives.Ints; + +import eu.siacs.conversations.xml.Element; +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Proceed extends JingleMessage { + + public Proceed() { + super(Proceed.class); + } + + public Integer getDeviceId() { + // TODO use proper namespace and create extension + final Element device = this.findChild("device"); + final String id = device == null ? null : device.getAttribute("id"); + if (id == null) { + return null; + } + return Ints.tryParse(id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java new file mode 100644 index 0000000000..e54f9c612b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java @@ -0,0 +1,38 @@ +package im.conversations.android.xmpp.model.jmi; + +import com.google.common.collect.ImmutableList; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import im.conversations.android.annotation.XmlElement; + +import java.util.List; + +@XmlElement +public class Propose extends JingleMessage { + + public Propose() { + super(Propose.class); + } + + public List getDescriptions() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + // TODO create proper extension for description + for (final Element child : getChildren()) { + if ("description".equals(child.getName())) { + final String namespace = child.getNamespace(); + if (Namespace.JINGLE_APPS_FILE_TRANSFER.contains(namespace)) { + builder.add(FileTransferDescription.upgrade(child)); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + builder.add(RtpDescription.upgrade(child)); + } else { + builder.add(GenericDescription.upgrade(child)); + } + } + } + return builder.build(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java new file mode 100644 index 0000000000..e71206fd61 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Reject extends JingleMessage { + + public Reject() { + super(Reject.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java new file mode 100644 index 0000000000..7c507156d8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Retract extends JingleMessage { + + public Retract() { + super(Retract.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java new file mode 100644 index 0000000000..9ce640b1f9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JINGLE_MESSAGE) +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/End.java b/src/main/java/im/conversations/android/xmpp/model/mam/End.java new file mode 100644 index 0000000000..757ed60c69 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/End.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class End extends Extension { + public End() { + super(End.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java b/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java new file mode 100644 index 0000000000..5340726475 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Fin extends Extension { + + public Fin() { + super(Fin.class); + } + + public boolean isComplete() { + return this.getAttributeAsBoolean("complete"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java b/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java new file mode 100644 index 0000000000..9f05e08fc2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Metadata extends Extension { + + public Metadata() { + super(Metadata.class); + } + + public Start getStart() { + return this.getExtension(Start.class); + } + + public End getEnd() { + return this.getExtension(End.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Query.java b/src/main/java/im/conversations/android/xmpp/model/mam/Query.java new file mode 100644 index 0000000000..d8f701d91d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Query.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Query extends Extension { + + public Query() { + super(Query.class); + } + + public void setQueryId(final String id) { + this.setAttribute("queryid", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Result.java b/src/main/java/im/conversations/android/xmpp/model/mam/Result.java new file mode 100644 index 0000000000..253499756b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Result.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Result extends Extension { + + public Result() { + super(Result.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } + + public String getId() { + return this.getAttribute("id"); + } + + public String getQueryId() { + return this.getAttribute("queryid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Start.java b/src/main/java/im/conversations/android/xmpp/model/mam/Start.java new file mode 100644 index 0000000000..9ff84b2564 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Start.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Start extends Extension { + + public Start() { + super(Start.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java b/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java new file mode 100644 index 0000000000..1aa4982e6a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MESSAGE_ARCHIVE_MANAGEMENT) +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java new file mode 100644 index 0000000000..be31df35d6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Displayed extends Extension { + + public Displayed() { + super(Displayed.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java b/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java new file mode 100644 index 0000000000..08161af709 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceiptRequest; + +@XmlElement +public class Markable extends DeliveryReceiptRequest { + + public Markable() { + super(Markable.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Received.java b/src/main/java/im/conversations/android/xmpp/model/markers/Received.java new file mode 100644 index 0000000000..7007cd1762 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Received.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceipt; + +@XmlElement +public class Received extends DeliveryReceipt { + + public Received() { + super(Received.class); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java b/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java new file mode 100644 index 0000000000..950963d4f0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CHAT_MARKERS) +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java new file mode 100644 index 0000000000..9f5275371c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.mds; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.MDS_DISPLAYED) +public class Displayed extends Extension { + public Displayed() { + super(Displayed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java b/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java new file mode 100644 index 0000000000..6502a16e72 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java @@ -0,0 +1,9 @@ +package im.conversations.android.xmpp.model.muc; + +public enum Affiliation { + OWNER, + ADMIN, + MEMBER, + OUTCAST, + NONE; +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/History.java b/src/main/java/im/conversations/android/xmpp/model/muc/History.java new file mode 100644 index 0000000000..e09210e602 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/History.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class History extends Extension { + + public History() { + super(History.class); + } + + public void setMaxChars(final int maxChars) { + this.setAttribute("maxchars", maxChars); + } + + public void setMaxStanzas(final int maxStanzas) { + this.setAttribute("maxstanzas", maxStanzas); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java b/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java new file mode 100644 index 0000000000..33da7b9af3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class MultiUserChat extends Extension { + + public MultiUserChat() { + super(MultiUserChat.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Role.java b/src/main/java/im/conversations/android/xmpp/model/muc/Role.java new file mode 100644 index 0000000000..9e9d3d1656 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Role.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model.muc; + +public enum Role { + MODERATOR, + VISITOR, + PARTICIPANT, + NONE +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java new file mode 100644 index 0000000000..41d652f204 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC) +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java new file mode 100644 index 0000000000..7ff712aeab --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java @@ -0,0 +1,58 @@ +package im.conversations.android.xmpp.model.muc.user; + +import android.util.Log; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; + +import java.util.Locale; + +@XmlElement +public class Item extends Extension { + + + public Item() { + super(Item.class); + } + + public Affiliation getAffiliation() { + final var affiliation = this.getAttribute("affiliation"); + if (Strings.isNullOrEmpty(affiliation)) { + return Affiliation.NONE; + } + try { + return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation); + return Affiliation.NONE; + } + } + + public Role getRole() { + final var role = this.getAttribute("role"); + if (Strings.isNullOrEmpty(role)) { + return Role.NONE; + } + try { + return Role.valueOf(role.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,"could not parse role "+ role); + return Role.NONE; + } + } + + public String getNick() { + return this.getAttribute("nick"); + } + + public Jid getJid() { + return this.getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java new file mode 100644 index 0000000000..5496c3ef20 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java @@ -0,0 +1,27 @@ +package im.conversations.android.xmpp.model.muc.user; + +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement(name = "x") +public class MucUser extends Extension { + + public static final int STATUS_CODE_SELF_PRESENCE = 110; + + public MucUser() { + super(MucUser.class); + } + + public Item getItem() { + return this.getExtension(Item.class); + } + + public Collection getStatus() { + return Collections2.filter( + Collections2.transform(getExtensions(Status.class), Status::getCode), + Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java new file mode 100644 index 0000000000..0706585af4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.muc.user; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Status extends Extension { + + public Status() { + super(Status.class); + } + + public Integer getCode() { + return this.getOptionalIntAttribute("code").orNull(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java new file mode 100644 index 0000000000..f5bfcaeda0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC_USER) +package im.conversations.android.xmpp.model.muc.user; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java new file mode 100644 index 0000000000..e9a9851282 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.nick; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.NICK) +public class Nick extends Extension { + + public Nick() { + super(Nick.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java b/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java new file mode 100644 index 0000000000..29ffc739f6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.occupant; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.OCCUPANT_ID) +public class OccupantId extends Extension { + + public OccupantId() { + super(OccupantId.class); + } + + public String getId() { + return Strings.emptyToNull(this.getAttribute("id")); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java b/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java new file mode 100644 index 0000000000..b324332a93 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.oob; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class OutOfBandData extends Extension { + + public OutOfBandData() { + super(OutOfBandData.class); + } + + public String getURL() { + final URL url = this.getExtension(URL.class); + return url == null ? null : Strings.emptyToNull(url.getContent()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/URL.java b/src/main/java/im/conversations/android/xmpp/model/oob/URL.java new file mode 100644 index 0000000000..008b084800 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/URL.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.oob; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "url") +public class URL extends Extension { + + public URL() { + super(URL.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java b/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java new file mode 100644 index 0000000000..aec4dee246 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.OOB) +package im.conversations.android.xmpp.model.oob; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java new file mode 100644 index 0000000000..d3d4b39103 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.pars; + +import im.conversations.android.annotation.XmlElement; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.PARS) +public class PreAuth extends Extension { + + public PreAuth() { + super(PreAuth.class); + } + + public void setToken(final String token) { + this.setAttribute("token", token); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java b/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java new file mode 100644 index 0000000000..43e4e2354b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.pgp; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x", namespace = Namespace.PGP_ENCRYPTED) +public class Encrypted extends Extension { + + public Encrypted() { + super(Encrypted.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java new file mode 100644 index 0000000000..c75413972f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.pgp; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED) +public class Signed extends Extension { + + + public Signed() { + super(Signed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java b/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java new file mode 100644 index 0000000000..7f8f1c3a05 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.ping; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.PING) +public class Ping extends Extension { + + public Ping() { + super(Ping.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java new file mode 100644 index 0000000000..dbf2c3c239 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.xmpp.model.Extension; + +public interface Item { + + T getExtension(final Class clazz); + + String getId(); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java new file mode 100644 index 0000000000..ceb1931ca3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java @@ -0,0 +1,52 @@ +package im.conversations.android.xmpp.model.pubsub; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.event.Retract; +import java.util.Collection; +import java.util.Map; +import java.util.NoSuchElementException; + +public interface Items { + + Collection getItems(); + + String getNode(); + + Collection getRetractions(); + + default Map getItemMap(final Class clazz) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (final Item item : getItems()) { + final var id = item.getId(); + final T extension = item.getExtension(clazz); + if (extension == null || Strings.isNullOrEmpty(id)) { + continue; + } + builder.put(id, extension); + } + return builder.buildKeepingLast(); + } + + default T getItemOrThrow(final String id, final Class clazz) { + final var map = getItemMap(clazz); + final var item = map.get(id); + if (item == null) { + throw new NoSuchElementException( + String.format("An item with id %s does not exist", id)); + } + return item; + } + + default T getFirstItem(final Class clazz) { + final var map = getItemMap(clazz); + return Iterables.getFirst(map.values(), null); + } + + default T getOnlyItem(final Class clazz) { + final var map = getItemMap(clazz); + return Iterables.getOnlyElement(map.values()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java new file mode 100644 index 0000000000..a4fc1ee8ec --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java @@ -0,0 +1,64 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.event.Retract; +import java.util.Collection; + +@XmlElement(name = "pubsub") +public class PubSub extends Extension { + + public PubSub() { + super(PubSub.class); + } + + public Items getItems() { + return this.getExtension(ItemsWrapper.class); + } + + @XmlElement(name = "items") + public static class ItemsWrapper extends Extension implements Items { + + public ItemsWrapper() { + super(ItemsWrapper.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getItems() { + return this.getExtensions(Item.class); + } + + public Collection getRetractions() { + return this.getExtensions(Retract.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } + + public void setMaxItems(final int maxItems) { + this.setAttribute("max_items", maxItems); + } + } + + @XmlElement(name = "item") + public static class Item extends Extension + implements im.conversations.android.xmpp.model.pubsub.Item { + + public Item() { + super(Item.class); + } + + @Override + public String getId() { + return this.getAttribute("id"); + } + + public void setId(String itemId) { + this.setAttribute("id", itemId); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java new file mode 100644 index 0000000000..7a384f5489 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Publish extends Extension { + + public Publish() { + super(Publish.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java new file mode 100644 index 0000000000..ec94f0604d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.pubsub; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement +public class PublishOptions extends Extension { + + public PublishOptions() { + super(PublishOptions.class); + } + + public static PublishOptions of(NodeConfiguration nodeConfiguration) { + final var publishOptions = new PublishOptions(); + publishOptions.addExtension(Data.of(Namespace.PUBSUB_PUBLISH_OPTIONS, nodeConfiguration)); + return publishOptions; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java new file mode 100644 index 0000000000..309381197e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Retract extends Extension { + + public Retract() { + super(Retract.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } + + public void setNotify(boolean notify) { + this.setAttribute("notify", notify ? 1 : 0); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java new file mode 100644 index 0000000000..a1c81a659d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.pubsub.error; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public abstract class PubSubError extends Extension { + + private PubSubError(Class clazz) { + super(clazz); + } + + @XmlElement + public static class PreconditionNotMet extends PubSubError { + + public PreconditionNotMet() { + super(PreconditionNotMet.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java new file mode 100644 index 0000000000..49d45f8c59 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_ERROR) +package im.conversations.android.xmpp.model.pubsub.error; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java new file mode 100644 index 0000000000..1e180c4600 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java @@ -0,0 +1,56 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Collection; + +@XmlElement +public class Event extends Extension { + + public Event() { + super(Event.class); + } + + public Items getItems() { + return this.getExtension(ItemsWrapper.class); + } + + public Purge getPurge() { + return this.getExtension(Purge.class); + } + + @XmlElement(name = "items") + public static class ItemsWrapper extends Extension implements Items { + + public ItemsWrapper() { + super(ItemsWrapper.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getItems() { + return this.getExtensions(Item.class); + } + + public Collection getRetractions() { + return this.getExtensions(Retract.class); + } + } + + @XmlElement(name = "item") + public static class Item extends Extension + implements im.conversations.android.xmpp.model.pubsub.Item { + + public Item() { + super(Item.class); + } + + @Override + public String getId() { + return this.getAttribute("id"); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java new file mode 100644 index 0000000000..64550e0b77 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Purge extends Extension { + + public Purge() { + super(Purge.class); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java new file mode 100644 index 0000000000..139a49522c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Retract extends Extension { + + public Retract() { + super(Retract.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java new file mode 100644 index 0000000000..223345c68b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_EVENT) +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java new file mode 100644 index 0000000000..53b987f53c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement +public class Configure extends Extension { + + public Configure() { + super(Configure.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public Data getData() { + return this.getExtension(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java new file mode 100644 index 0000000000..c3a61e6195 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "pubsub") +public class PubSubOwner extends Extension { + + public PubSubOwner() { + super(PubSubOwner.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java new file mode 100644 index 0000000000..d3ecb89aa9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_OWNER) +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java new file mode 100644 index 0000000000..a68a021fd5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB) +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java new file mode 100644 index 0000000000..1d854a83a4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.reactions; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Reaction extends Extension { + + public Reaction() { + super(Reaction.class); + } + + public Reaction(final String reaction) { + this(); + setContent(reaction); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java new file mode 100644 index 0000000000..ec3ae98917 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java @@ -0,0 +1,36 @@ +package im.conversations.android.xmpp.model.reactions; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Reactions extends Extension { + + public Reactions() { + super(Reactions.class); + } + + public Collection getReactions() { + return Collections2.filter( + Collections2.transform(getExtensions(Reaction.class), Reaction::getContent), + r -> Objects.nonNull(Strings.nullToEmpty(r))); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public static Reactions to(final String id) { + final var reactions = new Reactions(); + reactions.setId(id); + return reactions; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java new file mode 100644 index 0000000000..bdb8a8dca2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.REACTIONS) +package im.conversations.android.xmpp.model.reactions; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java new file mode 100644 index 0000000000..71fe922c15 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceipt; + +@XmlElement +public class Received extends DeliveryReceipt { + + public Received() { + super(Received.class); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java b/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java new file mode 100644 index 0000000000..684477af35 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceiptRequest; + +@XmlElement +public class Request extends DeliveryReceiptRequest { + + public Request() { + super(Request.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java b/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java new file mode 100644 index 0000000000..8e3de2cad5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DELIVERY_RECEIPTS) +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java new file mode 100644 index 0000000000..cd22f2a3a8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Instructions extends Extension { + + public Instructions() { + super(Instructions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Password.java b/src/main/java/im/conversations/android/xmpp/model/register/Password.java new file mode 100644 index 0000000000..9da687c213 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Password.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Password extends Extension { + + public Password() { + super(Password.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Register.java b/src/main/java/im/conversations/android/xmpp/model/register/Register.java new file mode 100644 index 0000000000..4a48bd8d15 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Register.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import org.jxmpp.jid.parts.Localpart; + +@XmlElement(name = "query") +public class Register extends Extension { + + public Register() { + super(Register.class); + } + + public void addUsername(final Localpart username) { + this.addExtension(new Username()).setContent(username.toString()); + } + + public void addPassword(final String password) { + this.addExtension(new Password()).setContent(password); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Remove.java b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java new file mode 100644 index 0000000000..bbd327bfd6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Remove extends Extension { + + public Remove() { + super(Remove.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Username.java b/src/main/java/im/conversations/android/xmpp/model/register/Username.java new file mode 100644 index 0000000000..bc93581b66 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Username.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Username extends Extension { + + public Username() { + super(Username.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/package-info.java b/src/main/java/im/conversations/android/xmpp/model/register/package-info.java new file mode 100644 index 0000000000..9e7a3e8f33 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.REGISTER) +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Group.java b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java new file mode 100644 index 0000000000..9f36efae7c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.roster; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Group extends Extension { + + public Group() { + super(Group.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java new file mode 100644 index 0000000000..0a2e0ef54a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java @@ -0,0 +1,61 @@ +package im.conversations.android.xmpp.model.roster; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +@XmlElement +public class Item extends Extension { + + public static final List RESULT_SUBSCRIPTIONS = + Arrays.asList(Subscription.NONE, Subscription.TO, Subscription.FROM, Subscription.BOTH); + + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } + + public String getItemName() { + return this.getAttribute("name"); + } + + public boolean isPendingOut() { + return "subscribe".equalsIgnoreCase(this.getAttribute("ask")); + } + + public Subscription getSubscription() { + final String value = this.getAttribute("subscription"); + try { + return value == null ? null : Subscription.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + + public Collection getGroups() { + return Collections2.filter( + Collections2.transform(getExtensions(Group.class), Element::getContent), + Objects::nonNull); + } + + public enum Subscription { + NONE, + TO, + FROM, + BOTH, + REMOVE + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java new file mode 100644 index 0000000000..616f6ae0b6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.roster; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query", namespace = Namespace.ROSTER) +public class Query extends Extension { + + public Query() { + super(Query.class); + } + + public void setVersion(final String rosterVersion) { + this.setAttribute("ver", rosterVersion); + } + + public String getVersion() { + return this.getAttribute("ver"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java b/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java new file mode 100644 index 0000000000..eea0703fd6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.ROSTER) +package im.conversations.android.xmpp.model.roster; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/After.java b/src/main/java/im/conversations/android/xmpp/model/rsm/After.java new file mode 100644 index 0000000000..90179bff0a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/After.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class After extends Extension { + + public After() { + super(After.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java new file mode 100644 index 0000000000..c3c6ac1a89 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Before extends Extension { + + public Before() { + super(Before.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java new file mode 100644 index 0000000000..c54f9d5e08 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Count extends Extension { + + public Count() { + super(Count.class); + } + + public Integer getCount() { + final var content = getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } else { + return Ints.tryParse(content); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/First.java b/src/main/java/im/conversations/android/xmpp/model/rsm/First.java new file mode 100644 index 0000000000..b976632e43 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/First.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class First extends Extension { + + public First() { + super(First.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java new file mode 100644 index 0000000000..01d53e0730 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Last extends Extension { + + public Last() { + super(Last.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java new file mode 100644 index 0000000000..06908be8b8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Max extends Extension { + + public Max() { + super(Max.class); + } + + public void setMax(final int max) { + this.setContent(String.valueOf(max)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java new file mode 100644 index 0000000000..6f428565c4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java @@ -0,0 +1,55 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.Page; +import im.conversations.android.xmpp.Range; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Set extends Extension { + + public Set() { + super(Set.class); + } + + public static Set of(final Range range, final Integer max) { + final var set = new Set(); + if (range.order == Range.Order.NORMAL) { + final var after = set.addExtension(new After()); + after.setContent(range.id); + } else if (range.order == Range.Order.REVERSE) { + final var before = set.addExtension(new Before()); + before.setContent(range.id); + } else { + throw new IllegalArgumentException("Invalid order"); + } + if (max != null) { + set.addExtension(new Max()).setMax(max); + } + return set; + } + + public Page asPage() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + + final var firstId = first == null ? null : first.getContent(); + final var lastId = last == null ? null : last.getContent(); + if (Strings.isNullOrEmpty(firstId) || Strings.isNullOrEmpty(lastId)) { + throw new IllegalStateException("Invalid page. Missing first or last"); + } + return new Page(firstId, lastId, this.getCount()); + } + + public boolean isEmpty() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + return first == null && last == null; + } + + public Integer getCount() { + final var count = this.getExtension(Count.class); + return count == null ? null : count.getCount(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java b/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java new file mode 100644 index 0000000000..c00fd37c9b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.RESULT_SET_MANAGEMENT) +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java new file mode 100644 index 0000000000..e9dd801f23 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.sasl; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationRequest; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Auth extends AuthenticationRequest { + + public Auth() { + super(Auth.class); + } + + @Override + public void setMechanism(final SaslMechanism mechanism) { + this.setAttribute("mechanism", mechanism.getMechanism()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java new file mode 100644 index 0000000000..1db7029b50 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationFailure; + +@XmlElement +public class Failure extends AuthenticationFailure { + public Failure() { + super(Failure.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java new file mode 100644 index 0000000000..e23087d892 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java new file mode 100644 index 0000000000..7612ba3583 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.sasl; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamFeature; + +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Mechanisms extends AuthenticationStreamFeature { + + + public Mechanisms() { + super(Mechanisms.class); + } + + public Collection getMechanisms() { + return getExtensions(Mechanism.class); + } + + public Collection getMechanismNames() { + return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java new file mode 100644 index 0000000000..5e2ab626e1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Response extends StreamElement { + + public Response() { + super(Response.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java b/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java new file mode 100644 index 0000000000..54a26709bc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java @@ -0,0 +1,89 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public class SaslError extends Extension { + + private SaslError(final Class clazz) { + super(clazz); + } + + @XmlElement + public static class Aborted extends SaslError { + public Aborted() { + super(Aborted.class); + } + } + + @XmlElement + public static class AccountDisabled extends SaslError { + public AccountDisabled() { + super(AccountDisabled.class); + } + } + + @XmlElement + public static class CredentialsExpired extends SaslError { + public CredentialsExpired() { + super(CredentialsExpired.class); + } + } + + @XmlElement + public static class EncryptionRequired extends SaslError { + public EncryptionRequired() { + super(EncryptionRequired.class); + } + } + + @XmlElement + public static class IncorrectEncoding extends SaslError { + public IncorrectEncoding() { + super(IncorrectEncoding.class); + } + } + + @XmlElement + public static class InvalidAuthzid extends SaslError { + public InvalidAuthzid() { + super(InvalidAuthzid.class); + } + } + + @XmlElement + public static class InvalidMechanism extends SaslError { + public InvalidMechanism() { + super(InvalidMechanism.class); + } + } + + @XmlElement + public static class MalformedRequest extends SaslError { + public MalformedRequest() { + super(MalformedRequest.class); + } + } + + @XmlElement + public static class MechanismTooWeak extends SaslError { + public MechanismTooWeak() { + super(MechanismTooWeak.class); + } + } + + @XmlElement + public static class NotAuthorized extends SaslError { + + public NotAuthorized() { + super(NotAuthorized.class); + } + } + + @XmlElement + public static class TemporaryAuthFailure extends SaslError { + public TemporaryAuthFailure() { + super(TemporaryAuthFailure.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java new file mode 100644 index 0000000000..d7323e4789 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Success extends StreamElement { + + + public Success() { + super(Success.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java new file mode 100644 index 0000000000..3b0de4f4a8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.SASL) +package im.conversations.android.xmpp.model.sasl; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java new file mode 100644 index 0000000000..ac99ed346d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationRequest; + +@XmlElement +public class Authenticate extends AuthenticationRequest { + + public Authenticate() { + super(Authenticate.class); + } + + @Override + public void setMechanism(final SaslMechanism mechanism) { + this.setAttribute("mechanism", mechanism.getMechanism()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java new file mode 100644 index 0000000000..ad26d37e6e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java @@ -0,0 +1,30 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamFeature; + +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Authentication extends AuthenticationStreamFeature { + public Authentication() { + super(Authentication.class); + } + + public Collection getMechanisms() { + return getExtensions(Mechanism.class); + } + + public Collection getMechanismNames() { + return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull); + } + + public Inline getInline() { + return this.getExtension(Inline.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java new file mode 100644 index 0000000000..e29ae7dea3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java @@ -0,0 +1,28 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class AuthorizationIdentifier extends Extension { + + + public AuthorizationIdentifier() { + super(AuthorizationIdentifier.class); + } + + public Jid get() { + final var content = getContent(); + if ( Strings.isNullOrEmpty(content)) { + return null; + } + try { + return Jid.ofEscaped(content); + } catch (final IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java new file mode 100644 index 0000000000..2594f5874b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Device extends Extension { + + public Device() { + super(Device.class); + } + + public Device(final String device) { + this(); + this.setContent(device); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java new file mode 100644 index 0000000000..bb0e327d9b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationFailure; + +@XmlElement +public class Failure extends AuthenticationFailure { + + public Failure() { + super(Failure.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java new file mode 100644 index 0000000000..6a6ad0dd8b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.fast.Fast; +import im.conversations.android.xmpp.model.fast.Mechanism; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +@XmlElement +public class Inline extends Extension { + + public Inline() { + super(Inline.class); + } + + public Fast getFast() { + return this.getExtension(Fast.class); + } + + public Collection getFastMechanisms() { + final var fast = getFast(); + final Collection mechanisms = + fast == null ? Collections.emptyList() : fast.getExtensions(Mechanism.class); + return Collections2.filter( + Collections2.transform(mechanisms, Element::getContent), Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java new file mode 100644 index 0000000000..d0a615777b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java new file mode 100644 index 0000000000..91f1b7dab6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Response extends StreamElement { + + public Response() { + super(Response.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java new file mode 100644 index 0000000000..8685ed3ff6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Software extends Extension { + + public Software() { + super(Software.class); + } + + public Software(final String software) { + this(); + this.setContent(software); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java new file mode 100644 index 0000000000..17673b35a7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Success extends StreamElement { + + + public Success() { + super(Success.class); + } + + public Jid getAuthorizationIdentifier() { + final var id = this.getExtension(AuthorizationIdentifier.class); + if (id == null) { + return null; + } + return id.get(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java new file mode 100644 index 0000000000..bb2a0c68cf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class UserAgent extends Extension { + + public UserAgent() { + super(UserAgent.class); + } + + public UserAgent(final String userAgentId) { + this(); + this.setAttribute("id", userAgentId); + } + + public void setSoftware(final String software) { + this.addExtension(new Software(software)); + } + + public void setDevice(final String device) { + this.addExtension(new Device(device)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java new file mode 100644 index 0000000000..10a61d1098 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.SASL_2) +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java b/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java new file mode 100644 index 0000000000..5cafc8c1d4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "a") +public class Ack extends StreamElement { + + public Ack() { + super(Ack.class); + } + + public Ack(final int sequence) { + super(Ack.class); + this.setAttribute("h", sequence); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java b/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java new file mode 100644 index 0000000000..9b80a93baa --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Enable extends StreamElement { + + public Enable() { + super(Enable.class); + this.setAttribute("resume", "true"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java b/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java new file mode 100644 index 0000000000..b900d435cd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Enabled extends StreamElement { + + public Enabled() { + super(Enabled.class); + } + + public boolean isResume() { + return this.getAttributeAsBoolean("resume"); + } + + public String getLocation() { + return this.getAttribute("location"); + } + + public Optional getResumeId() { + final var id = this.getAttribute("id"); + if (Strings.isNullOrEmpty(id)) { + return Optional.absent(); + } + if (isResume()) { + return Optional.of(id); + } else { + return Optional.absent(); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java b/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java new file mode 100644 index 0000000000..1e15bfe6c0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Failed extends StreamElement { + public Failed() { + super(Failed.class); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Request.java b/src/main/java/im/conversations/android/xmpp/model/sm/Request.java new file mode 100644 index 0000000000..ad1de61bc8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Request.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "r") +public class Request extends StreamElement { + + public Request() { + super(Request.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java b/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java new file mode 100644 index 0000000000..e47b19966c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Resume extends StreamElement { + + public Resume() { + super(Resume.class); + } + + public Resume(final String id, final int sequence) { + super(Resume.class); + this.setAttribute("previd", id); + this.setAttribute("h", sequence); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java b/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java new file mode 100644 index 0000000000..eb240745fd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Resumed extends StreamElement { + + public Resumed() { + super(Resumed.class); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java b/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java new file mode 100644 index 0000000000..48103755a1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement(name = "sm") +public class StreamManagement extends StreamFeature { + + public StreamManagement() { + super(StreamManagement.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java new file mode 100644 index 0000000000..dd2e036fcc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STREAM_MANAGEMENT) +package im.conversations.android.xmpp.model.sm; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java new file mode 100644 index 0000000000..9f94400c32 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java @@ -0,0 +1,77 @@ +package im.conversations.android.xmpp.model.stanza; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.error.Error; + +import java.util.Locale; + +@XmlElement +public class Iq extends Stanza { + + public static Iq TIMEOUT = new Iq(Type.TIMEOUT); + + public Iq() { + super(Iq.class); + } + + public Iq(final Type type) { + super(Iq.class); + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + + // TODO get rid of timeout + public enum Type { + SET, + GET, + ERROR, + RESULT, + TIMEOUT + } + + public Type getType() { + return Type.valueOf( + Strings.nullToEmpty(this.getAttribute("type")).toUpperCase(Locale.ROOT)); + } + + @Override + public boolean isInvalid() { + final var id = getId(); + if (Strings.isNullOrEmpty(id)) { + return true; + } + return super.isInvalid(); + } + + // Legacy methods that need to be refactored: + + public Element query() { + final Element query = findChild("query"); + if (query != null) { + return query; + } + return addChild("query"); + } + + public Element query(final String xmlns) { + final Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public Iq generateResponse(final Iq.Type type) { + final var packet = new Iq(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + + public String getErrorCondition() { + final Error error = getError(); + final var condition = error == null ? null : error.getCondition(); + return condition == null ? null : condition.getName(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java new file mode 100644 index 0000000000..9b12bffb38 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -0,0 +1,64 @@ +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.LocalizedContent; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.jabber.Body; + +import java.util.Locale; + +@XmlElement +public class Message extends Stanza { + + public Message() { + super(Message.class); + } + + public Message(Type type) { + this(); + this.setType(type); + } + + public LocalizedContent getBody() { + return findInternationalizedChildContentInDefaultNamespace("body"); + } + + public Type getType() { + final var value = this.getAttribute("type"); + if (value == null) { + return Type.NORMAL; + } else { + try { + return Type.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + } + + public void setType(final Type type) { + if (type == null || type == Type.NORMAL) { + this.removeAttribute("type"); + } else { + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + } + + public void setBody(final String text) { + this.addExtension(new Body(text)); + } + + public void setAxolotlMessage(Element axolotlMessage) { + removeChild(findChild("body")); + prependChild(axolotlMessage); + } + + public enum Type { + ERROR, + NORMAL, + GROUPCHAT, + HEADLINE, + CHAT + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java new file mode 100644 index 0000000000..129660b000 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.stanza; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; + +@XmlElement +public class Presence extends Stanza implements EntityCapabilities { + + public Presence() { + super(Presence.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java new file mode 100644 index 0000000000..82a8ce3dfb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java @@ -0,0 +1,74 @@ +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.InvalidJid; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.error.Error; + +public abstract class Stanza extends StreamElement { + + protected Stanza(final Class clazz) { + super(clazz); + } + + public Jid getTo() { + return this.getAttributeAsJid("to"); + } + + public Jid getFrom() { + return this.getAttributeAsJid("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setId(final String id) { + this.setAttribute("id", id); + } + + public void setFrom(final Jid from) { + this.setAttribute("from", from); + } + + public void setTo(final Jid to) { + this.setAttribute("to", to); + } + + public Error getError() { + return this.getExtension(Error.class); + } + + public boolean isInvalid() { + final var to = getTo(); + final var from = getFrom(); + if (to instanceof InvalidJid || from instanceof InvalidJid) { + return true; + } + return false; + } + + public boolean fromServer(final Account account) { + final Jid from = getFrom(); + return from == null + || from.equals(account.getDomain()) + || from.equals(account.getJid().asBareJid()) + || from.equals(account.getJid()); + } + + public boolean toServer(final Account account) { + final Jid to = getTo(); + return to == null + || to.equals(account.getDomain()) + || to.equals(account.getJid().asBareJid()) + || to.equals(account.getJid()); + } + + public boolean fromAccount(final Account account) { + final Jid from = getFrom(); + return from != null && from.asBareJid().equals(account.getJid().asBareJid()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java b/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java new file mode 100644 index 0000000000..d12fe56dbe --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JABBER_CLIENT) +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Active.java b/src/main/java/im/conversations/android/xmpp/model/state/Active.java new file mode 100644 index 0000000000..15970bc5bf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Active.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Active extends ChatStateNotification { + + public Active() { + super(Active.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java b/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java new file mode 100644 index 0000000000..642ed519d1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class ChatStateNotification extends Extension { + + protected ChatStateNotification(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Composing.java b/src/main/java/im/conversations/android/xmpp/model/state/Composing.java new file mode 100644 index 0000000000..9871952e0d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Composing.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Composing extends ChatStateNotification { + + public Composing() { + super(Composing.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Gone.java b/src/main/java/im/conversations/android/xmpp/model/state/Gone.java new file mode 100644 index 0000000000..a0a74e788c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Gone.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Gone extends ChatStateNotification { + + public Gone() { + super(Gone.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java b/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java new file mode 100644 index 0000000000..4a3670308a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Inactive extends ChatStateNotification { + + public Inactive() { + super(Inactive.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Paused.java b/src/main/java/im/conversations/android/xmpp/model/state/Paused.java new file mode 100644 index 0000000000..f97f3e5045 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Paused.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Paused extends ChatStateNotification { + + public Paused() { + super(Paused.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/package-info.java b/src/main/java/im/conversations/android/xmpp/model/state/package-info.java new file mode 100644 index 0000000000..a0cc97debf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CHAT_STATES) +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java new file mode 100644 index 0000000000..0597c2241c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java @@ -0,0 +1,33 @@ +package im.conversations.android.xmpp.model.streams; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.StreamFeature; +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.sm.StreamManagement; + +@XmlElement +public class Features extends StreamElement implements EntityCapabilities { + public Features() { + super(Features.class); + } + + public boolean streamManagement() { + return hasStreamFeature(StreamManagement.class); + } + + public boolean invite() { + return this.hasChild("register", Namespace.INVITE); + } + + public boolean clientStateIndication() { + return this.hasChild("csi", Namespace.CSI); + } + + + public boolean hasStreamFeature(final Class clazz) { + return hasExtension(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java new file mode 100644 index 0000000000..56900532c4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STREAMS) +package im.conversations.android.xmpp.model.streams; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java b/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java new file mode 100644 index 0000000000..3e2cf454c9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Proceed extends StreamElement { + + public Proceed() { + super(Proceed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/Required.java b/src/main/java/im/conversations/android/xmpp/model/tls/Required.java new file mode 100644 index 0000000000..60f4652ba8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/Required.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Required extends Extension { + public Required() { + super(Required.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java b/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java new file mode 100644 index 0000000000..337371c7b6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "starttls") +public class StartTls extends StreamElement { + public StartTls() { + super(StartTls.class); + } + + public boolean isRequired() { + return hasExtension(Required.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java b/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java new file mode 100644 index 0000000000..de3ed3ecdb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.TLS) +package im.conversations.android.xmpp.model.tls; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java b/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java new file mode 100644 index 0000000000..31a9396210 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.unique; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class OriginId extends Extension { + + public OriginId() { + super(OriginId.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java new file mode 100644 index 0000000000..23b0fdcac8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.unique; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class StanzaId extends Extension { + + public StanzaId() { + super(StanzaId.class); + } + + public Jid getBy() { + return this.getAttributeAsJid("by"); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java b/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java new file mode 100644 index 0000000000..31209ee24f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STANZA_IDS) +package im.conversations.android.xmpp.model.unique; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Get.java b/src/main/java/im/conversations/android/xmpp/model/upload/Get.java new file mode 100644 index 0000000000..5fad9afd40 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Get.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.upload; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import okhttp3.HttpUrl; + +@XmlElement +public class Get extends Extension { + + public Get() { + super(Get.class); + } + + public HttpUrl getUrl() { + final var url = this.getAttribute("url"); + if (Strings.isNullOrEmpty(url)) { + return null; + } + return HttpUrl.parse(url); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Header.java b/src/main/java/im/conversations/android/xmpp/model/upload/Header.java new file mode 100644 index 0000000000..00546d0d98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Header.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Header extends Extension { + + public Header() { + super(Header.class); + } + + public String getHeaderName() { + return this.getAttribute("name"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Put.java b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java new file mode 100644 index 0000000000..1b52a495c5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java @@ -0,0 +1,27 @@ +package im.conversations.android.xmpp.model.upload; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import okhttp3.HttpUrl; + +@XmlElement +public class Put extends Extension { + + public Put() { + super(Put.class); + } + + public HttpUrl getUrl() { + final var url = this.getAttribute("url"); + if (Strings.isNullOrEmpty(url)) { + return null; + } + return HttpUrl.parse(url); + } + + public Collection
getHeaders() { + return this.getExtensions(Header.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Request.java b/src/main/java/im/conversations/android/xmpp/model/upload/Request.java new file mode 100644 index 0000000000..bbf8a98c16 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Request.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Request extends Extension { + + public Request() { + super(Request.class); + } + + public void setFilename(String filename) { + this.setAttribute("filename", filename); + } + + public void setSize(long size) { + this.setAttribute("size", size); + } + + public void setContentType(String type) { + this.setAttribute("content-ype", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java b/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java new file mode 100644 index 0000000000..df90157812 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Slot extends Extension { + + public Slot() { + super(Slot.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java b/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java new file mode 100644 index 0000000000..e4ccf3d8dc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.HTTP_UPLOAD) +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java b/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java new file mode 100644 index 0000000000..273dcfb25f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "BINVAL") +public class BinaryValue extends Extension implements ByteContent { + + public BinaryValue() { + super(BinaryValue.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java new file mode 100644 index 0000000000..92adc6831c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "PHOTO") +public class Photo extends Extension { + public Photo() { + super(Photo.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java new file mode 100644 index 0000000000..20a6949775 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "vCard") +public class VCard extends Extension { + + public VCard() { + super(VCard.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java b/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java new file mode 100644 index 0000000000..7ee576ca27 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP) +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java new file mode 100644 index 0000000000..cb1f86d053 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Photo extends Extension { + + public Photo() { + super(Photo.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java new file mode 100644 index 0000000000..0be3f94b9f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class VCardUpdate extends Extension { + + public VCardUpdate() { + super(VCardUpdate.class); + } + + public Photo getPhoto() { + return this.getExtension(Photo.class); + } + + public String getHash() { + final var photo = getPhoto(); + return photo == null ? null : photo.getContent(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java new file mode 100644 index 0000000000..efed153607 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP_UPDATE) +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/version/Version.java b/src/main/java/im/conversations/android/xmpp/model/version/Version.java new file mode 100644 index 0000000000..7cbd5d22a8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/version/Version.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.version; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import eu.siacs.conversations.xml.Namespace; + +@XmlElement(name = "query", namespace = Namespace.VERSION) +public class Version extends Extension { + + public Version() { + super(Version.class); + } + + public void setSoftwareName(final String name) { + this.addChild("name").setContent(name); + } + + public void setVersion(final String version) { + this.addChild("version").setContent(version); + } + + public void setOs(final String os) { + this.addChild("os").setContent(os); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java new file mode 100644 index 0000000000..bc8097fda6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -0,0 +1,90 @@ +package im.conversations.android.xmpp.processor; + +import android.text.TextUtils; +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.generator.IqGenerator; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; + +import im.conversations.android.xmpp.model.stanza.Iq; + +public class BindProcessor implements Runnable { + + + private final XmppConnectionService service; + private final Account account; + + public BindProcessor(XmppConnectionService service, Account account) { + this.service = service; + this.account = account; + } + + @Override + public void run() { + final XmppConnection connection = account.getXmppConnection(); + service.cancelAvatarFetches(account); + final boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); + final boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, connection.getFeatures().httpUpload(0)); + if (loggedInSuccessfully || gainedFeature) { + service.databaseBackend.updateAccount(account); + } + + if (loggedInSuccessfully) { + if (!TextUtils.isEmpty(account.getDisplayName())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); + service.publishDisplayName(account); + } + } + + account.getRoster().clearPresences(); + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.clear(); + } + synchronized (account.inProgressConferencePings) { + account.inProgressConferencePings.clear(); + } + service.getJingleConnectionManager().notifyRebound(account); + service.getQuickConversationsService().considerSyncBackground(false); + + + connection.fetchRoster(); + + if (connection.getFeatures().bookmarks2()) { + service.fetchBookmarks2(account); + } else if (!connection.getFeatures().bookmarksConversion()) { + service.fetchBookmarks(account); + } + + if (connection.getFeatures().mds()) { + service.fetchMessageDisplayedSynchronization(account); + } else { + Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); + } + final boolean flexible = connection.getFeatures().flexibleOfflineMessageRetrieval(); + final boolean catchup = service.getMessageArchiveService().inCatchup(account); + final boolean trackOfflineMessageRetrieval; + if (flexible && catchup && connection.isMamPreferenceAlways()) { + trackOfflineMessageRetrieval = false; + connection.sendIqPacket(IqGenerator.purgeOfflineMessages(), (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully purged offline messages"); + } + }); + } else { + trackOfflineMessageRetrieval = true; + } + service.sendPresence(account); + connection.trackOfflineMessageRetrieval(trackOfflineMessageRetrieval); + if (service.getPushManagementService().available(account)) { + service.getPushManagementService().registerPushTokenOnServer(account); + } + service.connectMultiModeConversations(account); + service.syncDirtyContacts(account); + + service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account); + + } +} diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 6a18f62747..6cee1142e0 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -106,8 +106,8 @@ android:id="@+id/hostname_layout" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="0.7" android:layout_marginEnd="4sp" + android:layout_weight="0.7" android:hint="@string/account_settings_hostname"> + android:visibility="gone" + tools:visibility="visible"> + android:visibility="gone" + tools:visibility="visible"> + + + + + + + + + + + @@ -271,8 +302,7 @@ android:id="@+id/server_info_pep" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -293,8 +323,7 @@ android:id="@+id/server_info_blocking" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -315,8 +344,7 @@ android:id="@+id/server_info_sm" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -337,8 +365,7 @@ android:id="@+id/server_info_external_service" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -359,8 +386,7 @@ android:id="@+id/server_info_roster_version" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -381,8 +407,7 @@ android:id="@+id/server_info_carbons" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -403,8 +428,7 @@ android:id="@+id/server_info_mam" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -425,8 +449,7 @@ android:id="@+id/server_info_csi" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -448,8 +471,7 @@ android:id="@+id/server_info_push" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" /> @@ -458,7 +480,6 @@ android:layout_height="wrap_content"> + + + + + + + + + + + + + + + @@ -727,6 +789,7 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/delete_pgp_key" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_delete_24dp" android:visibility="visible" /> @@ -773,7 +836,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/copy_omemo_clipboard_description" + android:contentDescription="@string/show_qr_code" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_qr_code_24dp" android:visibility="visible" /> @@ -801,7 +864,8 @@ android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginBottom="@dimen/activity_vertical_margin" - android:visibility="gone"> + android:visibility="gone" + tools:visibility="visible"> @@ -355,6 +356,7 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/edit_nick" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_edit_24dp" /> @@ -382,6 +384,7 @@ android:layout_centerVertical="true" android:layout_gravity="center_horizontal" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/change_notification_settings" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_notifications_24dp" /> diff --git a/src/main/res/layout/item_message_content.xml b/src/main/res/layout/item_message_content.xml index acec7ee561..523b5d137d 100644 --- a/src/main/res/layout/item_message_content.xml +++ b/src/main/res/layout/item_message_content.xml @@ -85,6 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" + android:layout_marginHorizontal="10dp" android:divider="@android:color/transparent" android:dividerHeight="0dp"> diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index c12dfa9d06..e07ba24034 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -1,24 +1,15 @@ - - @string/never - 256 KiB - 512 KiB - 1 MiB - 3.5 MiB - 5 MiB - 10 MiB - - + 0 - 262144 524288 1048576 3490000 5242880 10485760 - + 52428800 + 1800 diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 4ee39f5864..88581e0f02 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -38,8 +38,6 @@ 4dp 8dp - 1200dp - 0.12 256dp diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 858470e557..7506f6775c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -203,6 +203,8 @@ XEP-0215: External Service Discovery XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload + XEP-0386: Bind 2 + XEP-0388: Extensible SASL Profile XEP-0357: Push available unavailable @@ -974,6 +976,7 @@ All chats This chat Your avatar + Your avatar. Tap to select new avatar from gallery. Avatar for %s Encrypted with OMEMO Encrypted with OpenPGP @@ -1074,6 +1077,22 @@ Unsupported operation Send later Options + Allow private messages + Edit nick + Delete OpenPGP key + Edit name and topic + Change configuration + Change notification settings + Call is using earpiece. Tap to switch to speaker. + Call is using earpiece. + Call is using wired headset + Call is using speaker. Tap to switch to earpiece. + Call is using speaker. + Call is using bluetooth. + Flip camera + Video is enabled. Tap to disable. + Video is disabled. Tap to enable. + Login mechanism Welcome to monocles chat monocles chat is an app that connects you to a global network called Jabber. This network includes services, called gateways, for chatting with other networks such as SMS, IRC, Matrix, and more. How the XMPP network works diff --git a/src/main/res/xml/preferences_attachments.xml b/src/main/res/xml/preferences_attachments.xml index e102814240..ed1f5dbc48 100644 --- a/src/main/res/xml/preferences_attachments.xml +++ b/src/main/res/xml/preferences_attachments.xml @@ -22,8 +22,6 @@ { + xmppConnectionService.sendIqPacket(account, request, (packet) -> { final Element data = packet.findChild("data", "urn:xmpp:bob"); - if (packet.getType() == IqPacket.TYPE.ERROR || data == null) { + if (packet.getType() == Iq.Type.ERROR || data == null) { Log.d(Config.LOGTAG, "BobTransfer failed: " + packet); finish(null); } else { diff --git a/src/monocleschat/java/de/monocles/chat/FinishOnboarding.java b/src/monocleschat/java/de/monocles/chat/FinishOnboarding.java index 44b7843e27..d34e0bdf72 100644 --- a/src/monocleschat/java/de/monocles/chat/FinishOnboarding.java +++ b/src/monocleschat/java/de/monocles/chat/FinishOnboarding.java @@ -16,7 +16,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; public class FinishOnboarding { private static final AtomicBoolean WORKING = new AtomicBoolean(false); @@ -32,14 +33,14 @@ public class FinishOnboarding { public static void finish(final XmppConnectionService xmppConnectionService, final XmppActivity activity, final Account onboardAccount, final Account newAccount) { if (!WORKING.compareAndSet(false, true)) return; - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(Jid.of("cheogram.com")); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", "change jabber id"); c.setAttribute("action", "execute"); Log.d(Config.LOGTAG, "" + packet); - xmppConnectionService.sendIqPacket(onboardAccount, packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(onboardAccount, packet, (iq) -> { Element command = iq.findChild("command", "http://jabber.org/protocol/commands"); if (command == null) { Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq); @@ -62,15 +63,15 @@ public class FinishOnboarding { iq.setAttribute("type", "set"); iq.removeAttribute("from"); iq.removeAttribute("id"); - xmppConnectionService.sendIqPacket(a, iq, (a2, iq2) -> { + xmppConnectionService.sendIqPacket(onboardAccount, iq, (iq2) -> { Element command2 = iq2.findChild("command", "http://jabber.org/protocol/commands"); if (command2 != null && command2.getAttribute("status") != null && command2.getAttribute("status").equals("completed")) { - final IqPacket regPacket = new IqPacket(IqPacket.TYPE.SET); + final var regPacket = new Iq(Iq.Type.SET); regPacket.setTo(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register")); final Element c2 = regPacket.addChild("command", Namespace.COMMANDS); c2.setAttribute("node", "jabber:iq:register"); c2.setAttribute("action", "execute"); - xmppConnectionService.sendIqPacket(newAccount, regPacket, (a3, iq3) -> { + xmppConnectionService.sendIqPacket(newAccount, regPacket, (iq3) -> { Element command3 = iq3.findChild("command", "http://jabber.org/protocol/commands"); if (command3 == null) { Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3); @@ -93,15 +94,15 @@ public class FinishOnboarding { iq3.setAttribute("type", "set"); iq3.removeAttribute("from"); iq3.removeAttribute("id"); - xmppConnectionService.sendIqPacket(newAccount, iq3, (a4, iq4) -> { + xmppConnectionService.sendIqPacket(newAccount, iq3, (iq4) -> { Element command4 = iq4.findChild("command", "http://jabber.org/protocol/commands"); if (command4 != null && command4.getAttribute("status") != null && command4.getAttribute("status").equals("completed")) { xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()), true); - Conversation withmonocles = xmppConnectionService.findOrCreateConversation(newAccount, iq4.getFrom().asBareJid(), true, true, true); - xmppConnectionService.markRead(withmonocles); - xmppConnectionService.clearConversationHistory(withmonocles); + Conversation withCheogram = xmppConnectionService.findOrCreateConversation(newAccount, iq4.getFrom().asBareJid(), true, true, true); + xmppConnectionService.markRead(withCheogram); + xmppConnectionService.clearConversationHistory(withCheogram); xmppConnectionService.deleteAccount(onboardAccount); - activity.switchToConversation(withmonocles, null, false, null, false, false, "command"); + activity.switchToConversation(withCheogram, null, false, null, false, false, "command"); // We don't set WORKING back to false because we suceeded so it should never run again anyway } else { Log.e(Config.LOGTAG, "Error confirming jid switch, got: " + iq4);