1
0
Fork 1
This commit is contained in:
Arne 2024-09-10 08:14:55 +02:00
parent 49ea241655
commit 07535bdb8a
320 changed files with 11298 additions and 2963 deletions

View file

@ -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'

View file

@ -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'
}

View file

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

View file

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

View file

@ -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 "";
}

View file

@ -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();
}

1
proguard-rules.pro vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -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;
}
}

View file

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

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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) {
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
Element item = mXmppConnectionService.getIqParser().getItem(packet);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
final Element item = IqParser.getItem(response);
final Set<Integer> deviceIds = IqParser.deviceIds(item);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds);
registerDevices(account.getJid().asBareJid(), deviceIds);
}
}
});
}
@ -455,12 +451,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null;
final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet);
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() {
@ -480,17 +474,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
mXmppConnectionService.databaseBackend.updateAccount(account);
}
if (packet.getType() == IqPacket.TYPE.ERROR) {
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" + packet.findChild("error"));
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error"));
}
}
}
}
});
}
@ -506,11 +499,9 @@ 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) {
mXmppConnectionService.sendIqPacket(account, packet, response -> {
String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
@Override
@ -525,7 +516,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
}
});
}
});
} catch (Exception e) {
e.printStackTrace();
@ -549,34 +539,32 @@ 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) {
if (response.getType() == Iq.Type.TIMEOUT) {
return; //ignore timeout. do nothing
}
if (packet.getType() == IqPacket.TYPE.ERROR) {
Element error = packet.findChild("error");
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" + packet);
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response);
return;
}
}
PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
PreKeyBundle bundle = IqParser.bundle(response);
final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
boolean flush = false;
if (bundle == null) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
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:" + packet);
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response);
}
try {
boolean changed = false;
@ -652,7 +640,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
}
}
});
}
@ -669,14 +656,12 @@ 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);
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();
@ -691,7 +676,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
}
});
} else if (packet.getType() == IqPacket.TYPE.RESULT) {
} else if (response.getType() == Iq.Type.RESULT) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
if (wipe) {
wipeOtherPepDevices();
@ -699,15 +684,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
publishOwnDeviceIdIfNeeded();
}
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
} 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: " + packet.toString());
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString());
}
pepBroken = true;
}
}
});
}
@ -759,9 +743,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return Futures.immediateFuture(session);
}
final SettableFuture<XmppAxolotlSession> future = SettableFuture.create();
final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
Pair<X509Certificate[], byte[]> verification = mXmppConnectionService.getIqParser().verification(response);
final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, packet, response -> {
Pair<X509Certificate[], byte[]> verification = IqParser.verification(response);
if (verification != null) {
try {
Signature verifier = Signature.getInstance("sha256WithRSA");
@ -846,7 +830,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) {
IqPacket packet;
final Iq packet;
synchronized (this.fetchDeviceIdsMap) {
List<OnDeviceIdsFetched> callbacks = this.fetchDeviceIdsMap.get(jid);
if (callbacks != null) {
@ -866,11 +850,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
}
if (packet != null) {
mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
mXmppConnectionService.sendIqPacket(account, packet, response -> {
if (response.getType() == Iq.Type.RESULT) {
fetchDeviceListStatus.put(jid, true);
Element item = mXmppConnectionService.getIqParser().getItem(response);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
final Element item = IqParser.getItem(response);
final Set<Integer> deviceIds = IqParser.deviceIds(item);
registerDevices(jid, deviceIds);
final List<OnDeviceIdsFetched> callbacks;
synchronized (fetchDeviceIdsMap) {
@ -882,7 +866,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
}
} else {
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
if (response.getType() == Iq.Type.TIMEOUT) {
fetchDeviceListStatus.remove(jid);
} else {
fetchDeviceListStatus.put(jid, false);
@ -929,16 +913,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
final Jid jid = Jid.of(address.getName());
final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid());
IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> {
if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId());
mXmppConnectionService.sendIqPacket(account, bundlesPacket, packet -> {
if (packet.getType() == Iq.Type.TIMEOUT) {
fetchStatusMap.put(address, FetchStatus.TIMEOUT);
sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout"));
} else if (packet.getType() == IqPacket.TYPE.RESULT) {
} else if (packet.getType() == Iq.Type.RESULT) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
final IqParser parser = mXmppConnectionService.getIqParser();
final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
final PreKeyBundle bundle = parser.bundle(packet);
final List<PreKeyBundle> preKeyBundleList = IqParser.preKeys(packet);
final PreKeyBundle bundle = IqParser.bundle(packet);
if (preKeyBundleList.isEmpty() || bundle == null) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
fetchStatusMap.put(address, FetchStatus.ERROR);
@ -1544,7 +1527,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
axolotlMessage.addDevice(session, true);
try {
final Jid jid = Jid.of(session.getRemoteAddress().getName());
MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
mXmppConnectionService.sendMessagePacket(account, packet);
} catch (IllegalArgumentException e) {
throw new Error("Remote addresses are created from jid and should convert back to jid", e);

View file

@ -146,10 +146,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Option;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
import im.conversations.android.xmpp.model.stanza.Iq;
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
public static final String TABLENAME = "conversations";
@ -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<Field> 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);

View file

@ -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();
}

View file

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

View file

@ -45,7 +45,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import im.conversations.android.xmpp.model.stanza.Iq;
public class IqGenerator extends AbstractGenerator {
@ -53,8 +53,8 @@ public class IqGenerator extends AbstractGenerator {
super(service);
}
public IqPacket discoResponse(final Account account, final IqPacket request) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT);
public Iq discoResponse(final Account account, final Iq request) {
final var packet = new Iq(Iq.Type.RESULT);
packet.setId(request.getId());
packet.setTo(request.getFrom());
final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
@ -69,8 +69,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket versionResponse(final IqPacket request) {
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
public Iq versionResponse(final Iq request) {
final var packet = request.generateResponse(Iq.Type.RESULT);
Element query = packet.query("jabber:iq:version");
query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
query.addChild("version").setContent(getIdentityVersion());
@ -93,8 +93,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket entityTimeResponse(IqPacket request) {
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
public Iq entityTimeResponse(final Iq request) {
final Iq packet = request.generateResponse(Iq.Type.RESULT);
Element time = packet.addChild("time", "urn:xmpp:time");
final long now = System.currentTimeMillis();
time.addChild("utc").setContent(getTimestamp(now));
@ -113,14 +113,14 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket purgeOfflineMessages() {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public static Iq purgeOfflineMessages() {
final Iq packet = new Iq(Iq.Type.SET);
packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
return packet;
}
protected IqPacket publish(final String node, final Element item, final Bundle options) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
protected Iq publish(final String node, final Element item, final Bundle options) {
final var packet = new Iq(Iq.Type.SET);
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
final Element publish = pubsub.addChild("publish");
publish.setAttribute("node", node);
@ -132,12 +132,12 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
protected IqPacket publish(final String node, final Element item) {
protected Iq publish(final String node, final Element item) {
return publish(node, item, null);
}
private IqPacket retrieve(String node, Element item) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
private Iq retrieve(String node, Element item) {
final var packet = new Iq(Iq.Type.GET);
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
final Element items = pubsub.addChild("items");
items.setAttribute("node", node);
@ -147,36 +147,36 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket retrieveVcard4(final Jid jid) {
final IqPacket packet = retrieve("urn:xmpp:vcard4", null);
public Iq retrieveVcard4(final Jid jid) {
final var packet = retrieve("urn:xmpp:vcard4", null);
packet.setTo(jid);
return packet;
}
public IqPacket retrieveBookmarks() {
public Iq retrieveBookmarks() {
return retrieve(Namespace.BOOKMARKS2, null);
}
public IqPacket retrieveMds() {
public Iq retrieveMds() {
return retrieve(Namespace.MDS_DISPLAYED, null);
}
public IqPacket publishNick(String nick) {
public Iq publishNick(String nick) {
final Element item = new Element("item");
item.setAttribute("id", "current");
item.addChild("nick", Namespace.NICK).setContent(nick);
return publish(Namespace.NICK, item);
}
public IqPacket deleteNode(final String node) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq deleteNode(final String node) {
final var packet = new Iq(Iq.Type.SET);
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
pubsub.addChild("delete").setAttribute("node", node);
return packet;
}
public IqPacket deleteItem(final String node, final String id) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq deleteItem(final String node, final String id) {
final var packet = new Iq(Iq.Type.SET);
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
final Element retract = pubsub.addChild("retract");
retract.setAttribute("node", node);
@ -185,7 +185,7 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket publishAvatar(Avatar avatar, Bundle options) {
public Iq publishAvatar(Avatar avatar, Bundle options) {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
final Element data = item.addChild("data", Namespace.AVATAR_DATA);
@ -193,14 +193,14 @@ public class IqGenerator extends AbstractGenerator {
return publish(Namespace.AVATAR_DATA, item, options);
}
public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) {
public Iq publishElement(final String namespace, final Element element, String id, final Bundle options) {
final Element item = new Element("item");
item.setAttribute("id", id);
item.addChild(element);
return publish(namespace, item, options);
}
public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) {
public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
final Element metadata = item
@ -214,57 +214,57 @@ public class IqGenerator extends AbstractGenerator {
return publish(Namespace.AVATAR_METADATA, item, options);
}
public IqPacket retrievePepAvatar(final Avatar avatar) {
public Iq retrievePepAvatar(final Avatar avatar) {
final Element item = new Element("item");
item.setAttribute("id", avatar.sha1sum);
final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item);
final var packet = retrieve(Namespace.AVATAR_DATA, item);
packet.setTo(avatar.owner);
return packet;
}
public IqPacket retrieveVcardAvatar(final Avatar avatar) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq retrieveVcardAvatar(final Avatar avatar) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(avatar.owner);
packet.addChild("vCard", "vcard-temp");
return packet;
}
public IqPacket retrieveVcardAvatar(final Jid to) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq retrieveVcardAvatar(final Jid to) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(to);
packet.addChild("vCard", "vcard-temp");
return packet;
}
public IqPacket retrieveAvatarMetaData(final Jid to) {
final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null);
public Iq retrieveAvatarMetaData(final Jid to) {
final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
if (to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket retrieveDeviceIds(final Jid to) {
final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
public Iq retrieveDeviceIds(final Jid to) {
final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if (to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
packet.setTo(to);
return packet;
}
public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
packet.setTo(to);
return packet;
}
public IqPacket publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
final Element item = new Element("item");
item.setAttribute("id", "current");
final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
@ -314,7 +314,7 @@ public class IqGenerator extends AbstractGenerator {
return displayed;
}
public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
public Iq publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) {
final Element item = new Element("item");
item.setAttribute("id", "current");
@ -338,7 +338,7 @@ public class IqGenerator extends AbstractGenerator {
return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
}
public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
public Iq publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) {
final Element item = new Element("item");
item.setAttribute("id", "current");
final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
@ -356,8 +356,8 @@ public class IqGenerator extends AbstractGenerator {
return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
}
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final Iq packet = new Iq(Iq.Type.SET);
final Element query = packet.query(mam.version.namespace);
query.setAttribute("queryid", mam.getQueryId());
final Data data = new Data();
@ -387,15 +387,15 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket generateGetBlockList() {
final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
public Iq generateGetBlockList() {
final Iq iq = new Iq(Iq.Type.GET);
iq.addChild("blocklist", Namespace.BLOCKING);
return iq;
}
public IqPacket generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
public Iq generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) {
final Iq iq = new Iq(Iq.Type.SET);
final Element block = iq.addChild("block", Namespace.BLOCKING);
final Element item = block.addChild("item").setAttribute("jid", jid);
if (reportSpam) {
@ -411,15 +411,15 @@ public class IqGenerator extends AbstractGenerator {
return iq;
}
public IqPacket generateSetUnblockRequest(final Jid jid) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
public Iq generateSetUnblockRequest(final Jid jid) {
final Iq iq = new Iq(Iq.Type.SET);
final Element block = iq.addChild("unblock", Namespace.BLOCKING);
block.addChild("item").setAttribute("jid", jid);
return iq;
}
public IqPacket generateSetPassword(final Account account, final String newPassword) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq generateSetPassword(final Account account, final String newPassword) {
final Iq packet = new Iq(Iq.Type.SET);
packet.setTo(account.getDomain());
final Element query = packet.addChild("query", Namespace.REGISTER);
final Jid jid = account.getJid();
@ -428,14 +428,14 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) {
public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
List<Jid> jids = new ArrayList<>();
jids.add(jid);
return changeAffiliation(conference, jids, affiliation);
}
public IqPacket changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
final Iq packet = new Iq(Iq.Type.SET);
packet.setTo(conference.getJid().asBareJid());
packet.setFrom(conference.getAccount().getJid());
Element query = packet.query("http://jabber.org/protocol/muc#admin");
@ -447,8 +447,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket changeRole(Conversation conference, String nick, String role) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq changeRole(Conversation conference, String nick, String role) {
final Iq packet = new Iq(Iq.Type.SET);
packet.setTo(conference.getJid().asBareJid());
packet.setFrom(conference.getAccount().getJid());
Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
@ -457,11 +457,11 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket moderateMessage(Account account, Message m, String reason) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq moderateMessage(Account account, Message m, String reason) {
final var packet = new Iq(Iq.Type.SET);
packet.setTo(m.getConversation().getJid().asBareJid());
packet.setFrom(account.getJid());
Element moderate =
final var moderate =
packet.addChild("apply-to", "urn:xmpp:fasten:0")
.setAttribute("id", m.getServerMsgId())
.addChild("moderate", "urn:xmpp:message-moderate:0");
@ -470,8 +470,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(host);
Element request = packet.addChild("request", Namespace.HTTP_UPLOAD);
request.setAttribute("filename", name == null ? convertFilename(file.getName()) : name);
@ -480,8 +480,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(host);
Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY);
request.addChild("filename").setContent(convertFilename(file.getName()));
@ -507,8 +507,8 @@ public class IqGenerator extends AbstractGenerator {
}
}
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
public static Iq generateCreateAccountWithCaptcha(final Account account, final String id, final Data data) {
final Iq register = new Iq(Iq.Type.SET);
register.setFrom(account.getJid().asBareJid());
register.setTo(account.getDomain());
register.setId(id);
@ -519,12 +519,12 @@ public class IqGenerator extends AbstractGenerator {
return register;
}
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
return pushTokenToAppServer(appServer, token, deviceId, null);
}
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
final Iq packet = new Iq(Iq.Type.SET);
packet.setTo(appServer);
final Element command = packet.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", "register-push-fcm");
@ -540,8 +540,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
final Iq packet = new Iq(Iq.Type.SET);
packet.setTo(appServer);
final Element command = packet.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", "unregister-push-fcm");
@ -554,8 +554,8 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket enablePush(final Jid jid, final String node, final String secret) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq enablePush(final Jid jid, final String node, final String secret) {
final Iq packet = new Iq(Iq.Type.SET);
Element enable = packet.addChild("enable", Namespace.PUSH);
enable.setAttribute("jid", jid);
enable.setAttribute("node", node);
@ -569,16 +569,16 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket disablePush(final Jid jid, final String node) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
public Iq disablePush(final Jid jid, final String node) {
Iq packet = new Iq(Iq.Type.SET);
Element disable = packet.addChild("disable", Namespace.PUSH);
disable.setAttribute("jid", jid);
disable.setAttribute("node", node);
return packet;
}
public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq queryAffiliation(Conversation conversation, String affiliation) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(conversation.getJid().asBareJid());
packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation);
return packet;
@ -611,16 +611,16 @@ public class IqGenerator extends AbstractGenerator {
return options;
}
public IqPacket requestPubsubConfiguration(Jid jid, String node) {
public Iq requestPubsubConfiguration(Jid jid, String node) {
return pubsubConfiguration(jid, node, null);
}
public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) {
public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
return pubsubConfiguration(jid, node, data);
}
private IqPacket pubsubConfiguration(Jid jid, String node, Data data) {
IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
private Iq pubsubConfiguration(Jid jid, String node, Data data) {
final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
packet.setTo(jid);
Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
Element configure = pubsub.addChild("configure").setAttribute("node", node);
@ -630,43 +630,43 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket queryDiscoItems(Jid jid) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq queryDiscoItems(final Jid jid) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(jid);
packet.query(Namespace.DISCO_ITEMS);
return packet;
}
public IqPacket queryDiscoItems(Jid jid, String node) {
IqPacket packet = queryDiscoItems(jid);
final Element query = packet.query(Namespace.DISCO_ITEMS);
public Iq queryDiscoItems(Jid jid, String node) {
final var packet = queryDiscoItems(jid);
final var query = packet.query(Namespace.DISCO_ITEMS);
query.setAttribute("node", node);
return packet;
}
public IqPacket queryDiscoInfo(Jid jid) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
public Iq queryDiscoInfo(final Jid jid) {
final Iq packet = new Iq(Iq.Type.GET);
packet.setTo(jid);
packet.addChild("query",Namespace.DISCO_INFO);
return packet;
}
public IqPacket bobResponse(IqPacket request) {
public Iq bobResponse(Iq request) {
try {
String bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
Cid cid = BobTransfer.cid(bobCid);
DownloadableFile f = mXmppConnectionService.getFileForCid(cid);
final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
final var cid = BobTransfer.cid(bobCid);
final var f = mXmppConnectionService.getFileForCid(cid);
if (f == null || !f.canRead()) {
throw new IOException("No such file");
} else if (f.getSize() > 129000) {
final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
final var response = request.generateResponse(Iq.Type.ERROR);
final var error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas");
return response;
} else {
final IqPacket response = request.generateResponse(IqPacket.TYPE.RESULT);
final Element data = response.addChild("data", "urn:xmpp:bob");
final var response = request.generateResponse(Iq.Type.RESULT);
final var data = response.addChild("data", "urn:xmpp:bob");
data.setAttribute("cid", bobCid);
data.setAttribute("type", f.getMimeType());
ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2);
@ -678,8 +678,8 @@ public class IqGenerator extends AbstractGenerator {
return response;
}
} catch (final IOException | IllegalStateException e) {
final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
final var response = request.generateResponse(Iq.Type.ERROR);
final var error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
return response;

View file

@ -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 doesnt seem to support that. Find more information on https://conversations.im/omemo";
@ -33,25 +32,25 @@ public class MessageGenerator extends AbstractGenerator {
super(service);
}
private MessagePacket preparePacket(Message message, boolean legacyEncryption) {
private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) {
Conversation conversation = (Conversation) message.getConversation();
Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
final boolean isWithSelf = conversation.getContact().isSelf();
if (conversation.getMode() == Conversation.MODE_SINGLE) {
packet.setTo(message.getCounterpart());
packet.setType(MessagePacket.TYPE_CHAT);
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
if (!isWithSelf) {
packet.addChild("request", "urn:xmpp:receipts");
}
} else if (message.isPrivateMessage()) {
packet.setTo(message.getCounterpart());
packet.setType(MessagePacket.TYPE_CHAT);
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.addChild("x", "http://jabber.org/protocol/muc#user");
packet.addChild("request", "urn:xmpp:receipts");
} else {
packet.setTo(message.getCounterpart().asBareJid());
packet.setType(MessagePacket.TYPE_GROUPCHAT);
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
}
if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) {
packet.addChild("markable", "urn:xmpp:chat-markers:0");
@ -78,7 +77,7 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public void addDelay(MessagePacket packet, long timestamp) {
public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) {
final SimpleDateFormat mDateFormat = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@ -87,8 +86,8 @@ public class MessageGenerator extends AbstractGenerator {
delay.setAttribute("stamp", mDateFormat.format(date));
}
public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
MessagePacket packet = preparePacket(message, true);
public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
if (axolotlMessage == null) {
return null;
}
@ -101,17 +100,18 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT);
public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(to);
packet.setAxolotlMessage(axolotlMessage.toElement());
packet.addChild("store", "urn:xmpp:hints");
return packet;
}
public MessagePacket generateChat(Message message) {
MessagePacket packet = preparePacket(message, false);
public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) {
im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false);
String content;
if (message.hasFileOnRemoteHost()) {
final Message.FileParams fileParams = message.getFileParams();
@ -139,8 +139,8 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket generatePgpChat(Message message) {
MessagePacket packet = preparePacket(message, true);
public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) {
final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true);
if (message.hasFileOnRemoteHost()) {
Message.FileParams fileParams = message.getFileParams();
final String url = fileParams.url;
@ -163,10 +163,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket generateChatState(Conversation conversation) {
public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) {
final Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
@ -175,11 +175,11 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket confirm(final Message message) {
public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) {
final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
final Jid to = message.getCounterpart();
final MessagePacket packet = new MessagePacket();
packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(groupChat ? to.asBareJid() : to);
final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
if (groupChat) {
@ -197,20 +197,20 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket conferenceSubject(Conversation conversation, String subject) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_GROUPCHAT);
public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT);
packet.setTo(conversation.getJid().asBareJid());
packet.addChild("subject").setContent(subject);
packet.setFrom(conversation.getAccount().getJid().asBareJid());
return packet;
}
public MessagePacket requestVoice(Jid jid) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_NORMAL);
public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) {
final var packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
packet.setTo(jid.asBareJid());
Data form = new Data();
final var form = new Data();
form.setFormType("http://jabber.org/protocol/muc#request");
form.put("muc#role", "participant");
form.submit();
@ -218,9 +218,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket directInvite(final Conversation conversation, final Jid contact) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_NORMAL);
public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL);
packet.setTo(contact);
packet.setFrom(conversation.getAccount().getJid());
Element x = packet.addChild("x", "jabber:x:conference");
@ -236,8 +236,8 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket invite(final Conversation conversation, final Jid contact) {
final MessagePacket packet = new MessagePacket();
public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) {
final var packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setTo(conversation.getJid().asBareJid());
packet.setFrom(conversation.getAccount().getJid());
Element x = new Element("x");
@ -249,8 +249,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
final MessagePacket receivedPacket = new MessagePacket();
public im.conversations.android.xmpp.model.stanza.Message received(Account account, final Jid from, final String id, ArrayList<String> namespaces, im.conversations.android.xmpp.model.stanza.Message.Type type) {
final var receivedPacket =
new im.conversations.android.xmpp.model.stanza.Message();
receivedPacket.setType(type);
receivedPacket.setTo(from);
receivedPacket.setFrom(account.getJid());
@ -261,8 +262,8 @@ public class MessageGenerator extends AbstractGenerator {
return receivedPacket;
}
public MessagePacket received(Account account, Jid to, String id) {
MessagePacket packet = new MessagePacket();
public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) {
im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setFrom(account.getJid());
packet.setTo(to);
packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id);
@ -270,10 +271,10 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket sessionFinish(
public im.conversations.android.xmpp.model.stanza.Message sessionFinish(
final Jid with, final String sessionId, final Reason reason) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT);
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT);
packet.setTo(with);
final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE);
finish.setAttribute("id", sessionId);
@ -283,9 +284,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
packet.setTo(proposal.with);
packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId);
final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE);
@ -298,9 +299,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
packet.setTo(proposal.with);
final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", proposal.sessionId);
@ -309,9 +310,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket sessionReject(final Jid with, final String sessionId) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) {
final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message();
packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those
packet.setTo(with);
final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE);
propose.setAttribute("id", sessionId);

View file

@ -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;

View file

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

View file

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

View file

@ -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;

View file

@ -26,6 +26,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@ -38,18 +39,17 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import im.conversations.android.xmpp.model.stanza.Iq;
public class IqParser extends AbstractParser implements OnIqPacketReceived {
public class IqParser extends AbstractParser implements Consumer<Iq> {
public IqParser(final XmppConnectionService service) {
super(service);
public IqParser(final XmppConnectionService service, final Account account) {
super(service, account);
}
public static List<Jid> items(IqPacket packet) {
public static List<Jid> items(final Iq packet) {
ArrayList<Jid> items = new ArrayList<>();
final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
if (query == null) {
@ -66,7 +66,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return items;
}
public static Room parseRoom(IqPacket packet) {
public static Room parseRoom(Iq packet) {
final Element query = packet.findChild("query", Namespace.DISCO_INFO);
if (query == null) {
return null;
@ -144,7 +144,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
mXmppConnectionService.syncRoster(account);
}
public String avatarData(final IqPacket packet) {
public static String avatarData(final Iq packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) {
return null;
@ -153,10 +153,10 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
if (items == null) {
return null;
}
return super.avatarData(items);
return AbstractParser.avatarData(items);
}
public Element getItem(final IqPacket packet) {
public static Element getItem(final Iq packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) {
return null;
@ -169,7 +169,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
@NonNull
public Set<Integer> deviceIds(final Element item) {
public static Set<Integer> deviceIds(final Element item) {
Set<Integer> deviceIds = new HashSet<>();
if (item != null) {
final Element list = item.findChild("list");
@ -190,7 +190,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return deviceIds;
}
private Integer signedPreKeyId(final Element bundle) {
private static Integer signedPreKeyId(final Element bundle) {
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if (signedPreKeyPublic == null) {
return null;
@ -202,7 +202,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
private ECPublicKey signedPreKeyPublic(final Element bundle) {
private static ECPublicKey signedPreKeyPublic(final Element bundle) {
ECPublicKey publicKey = null;
final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic");
if (signedPreKeyPublic == null) {
@ -216,7 +216,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return publicKey;
}
private byte[] signedPreKeySignature(final Element bundle) {
private static byte[] signedPreKeySignature(final Element bundle) {
final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature");
if (signedPreKeySignature == null) {
return null;
@ -229,7 +229,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
private IdentityKey identityKey(final Element bundle) {
private static IdentityKey identityKey(final Element bundle) {
final String identityKey = bundle.findChildContent("identityKey");
if (identityKey == null) {
return null;
@ -242,7 +242,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
public static Map<Integer, ECPublicKey> preKeyPublics(final Iq packet) {
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
Element item = getItem(packet);
if (item == null) {
@ -285,7 +285,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input));
}
public Pair<X509Certificate[], byte[]> verification(final IqPacket packet) {
public static Pair<X509Certificate[], byte[]> verification(final Iq packet) {
Element item = getItem(packet);
Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
Element chain = verification != null ? verification.findChild("chain") : null;
@ -313,7 +313,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
public PreKeyBundle bundle(final IqPacket bundle) {
public static PreKeyBundle bundle(final Iq bundle) {
final Element bundleItem = getItem(bundle);
if (bundleItem == null) {
return null;
@ -337,7 +337,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
}
public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
public static List<PreKeyBundle> preKeys(final Iq preKeys) {
List<PreKeyBundle> bundles = new ArrayList<>();
Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
if (preKeyPublics != null) {
@ -352,15 +352,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
public void accept(final Iq packet) {
final boolean isGet = packet.getType() == Iq.Type.GET;
if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
return;
}
if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
final Element query = packet.findChild("query");
// If this is in response to a query for the whole roster:
if (packet.getType() == IqPacket.TYPE.RESULT) {
if (packet.getType() == Iq.Type.RESULT) {
account.getRoster().markAllAsNotInRoster();
}
this.rosterItems(account, query);
@ -374,7 +374,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
(block != null ? block.getChildren() : null);
// If this is a response to a blocklist query, clear the block list and replace with the new one.
// Otherwise, just update the existing blocklist.
if (packet.getType() == IqPacket.TYPE.RESULT) {
if (packet.getType() == Iq.Type.RESULT) {
account.clearBlocklist();
account.getXmppConnection().getFeatures().setBlockListRequested(true);
}
@ -390,7 +390,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
account.getBlocklist().addAll(jids);
if (packet.getType() == IqPacket.TYPE.SET) {
if (packet.getType() == Iq.Type.SET) {
boolean removed = false;
for (Jid jid : jids) {
removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
@ -402,15 +402,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
// Update the UI
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
if (packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
if (packet.getType() == Iq.Type.SET) {
final Iq response = packet.generateResponse(Iq.Type.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
}
} else if (packet.hasChild("unblock", Namespace.BLOCKING) &&
packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
packet.fromServer(account) && packet.getType() == Iq.Type.SET) {
Log.d(Config.LOGTAG, "Received unblock update from server");
final Collection<Element> items = packet.findChild("unblock", Namespace.BLOCKING).getChildren();
if (items.size() == 0) {
if (items.isEmpty()) {
// No children to unblock == unblock all
account.getBlocklist().clear();
} else {
@ -426,7 +426,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
account.getBlocklist().removeAll(jids);
}
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
final Iq response = packet.generateResponse(Iq.Type.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")
@ -434,18 +434,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
mXmppConnectionService.getJingleConnectionManager()
.deliverIbbPacket(account, packet);
} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
final Iq response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
final Iq response = packet.generateResponse(Iq.Type.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
final IqPacket response;
final Iq response;
if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
response = packet.generateResponse(IqPacket.TYPE.ERROR);
response = packet.generateResponse(Iq.Type.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
@ -453,18 +453,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
}
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) {
} else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == Iq.Type.SET) {
final Jid transport = packet.getFrom();
final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
final boolean success =
push != null
&& mXmppConnectionService.processUnifiedPushMessage(
account, transport, push);
final IqPacket response;
final Iq response;
if (success) {
response = packet.generateResponse(IqPacket.TYPE.RESULT);
response = packet.generateResponse(Iq.Type.RESULT);
} else {
response = packet.generateResponse(IqPacket.TYPE.ERROR);
response = packet.generateResponse(Iq.Type.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.setAttribute("code", "404");
@ -476,8 +476,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
final Conversation conversation = mXmppConnectionService.find(account, packet.getFrom());
if (packet.hasChild("data", "urn:xmpp:bob") && isGet && (conversation == null ? contact != null && contact.canInferPresence() : conversation.canInferPresence())) {
mXmppConnectionService.sendIqPacket(account, mXmppConnectionService.getIqGenerator().bobResponse(packet), null);
} else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
} else if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) {
final var response = packet.generateResponse(Iq.Type.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas");

View file

@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import io.ipfs.cid.Cid;
@ -60,17 +61,20 @@ import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent;
import im.conversations.android.xmpp.model.forward.Forwarded;
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
public class MessageParser extends AbstractParser implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
public MessageParser(XmppConnectionService service) {
super(service);
public MessageParser(final XmppConnectionService service, final Account account) {
super(service, account);
}
private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) {
@ -109,7 +113,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return result != null ? result : fallback;
}
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) {
ChatState state = ChatState.parse(packet);
if (state != null && c != null) {
final Account account = c.getAccount();
@ -251,7 +255,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
final Set<Integer> deviceIds = IqParser.deviceIds(item);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... ");
final AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
@ -358,10 +362,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
mXmppConnectionService.updateAccountUi();
}
private boolean handleErrorMessage(final Account account, final MessagePacket packet) {
if (packet.getType() == MessagePacket.TYPE_ERROR) {
private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) {
if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) {
if (packet.fromServer(account)) {
final Pair<MessagePacket, Long> forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS);
final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS);
if (forwarded != null) {
return handleErrorMessage(account, forwarded.first);
}
@ -404,11 +408,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
@Override
public void onMessagePacketReceived(Account account, MessagePacket original) {
public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
if (handleErrorMessage(account, original)) {
return;
}
final MessagePacket packet;
final im.conversations.android.xmpp.model.stanza.Message packet;
Long timestamp = null;
boolean isCarbon = false;
String serverMsgId = null;
@ -422,7 +426,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved();
if (query != null && query.validFrom(original.getFrom())) {
final Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", query.version.namespace);
final var f = getForwardedMessagePacket(original,"result", query.version.namespace);
if (f == null) {
return;
}
@ -442,9 +446,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")");
return;
} else if (original.fromServer(account)) {
Pair<MessagePacket, Long> f;
f = original.getForwardedMessagePacket("received", Namespace.CARBONS);
f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f;
Pair<im.conversations.android.xmpp.model.stanza.Message, Long> f;
f = getForwardedMessagePacket(original, Received.class);
f = f == null ? getForwardedMessagePacket(original, Sent.class) : f;
packet = f != null ? f.first : original;
if (handleErrorMessage(account, packet)) {
return;
@ -514,7 +518,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return;
}
boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
if (query != null && !query.muc() && isTypeGroupChat) {
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
return;
@ -1300,6 +1304,34 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class<? extends Extension> clazz) {
final var extension = original.getExtension(clazz);
final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class);
if (forwarded == null) {
return null;
}
final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
final var forwardedMessage = forwarded.getMessage();
if (forwardedMessage == null) {
return null;
}
return new Pair<>(forwardedMessage,timestamp);
}
private static Pair<im.conversations.android.xmpp.model.stanza.Message,Long> getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) {
final Element wrapper = original.findChild(name, namespace);
final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD);
if (forwardedElement instanceof Forwarded forwarded) {
final Long timestamp = AbstractParser.parseTimestamp(forwarded, null);
final var forwardedMessage = forwarded.getMessage();
if (forwardedMessage == null) {
return null;
}
return new Pair<>(forwardedMessage,timestamp);
}
return null;
}
private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) {
final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
if (conversation != null && (query == null || query.isCatchup())) {
@ -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,

View file

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

View file

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

View file

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

View file

@ -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 =
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(

View file

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

View file

@ -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;

View file

@ -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) {

View file

@ -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) {

View file

@ -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));

View file

@ -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);
}
}

View file

@ -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,
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull 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);
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()));

View file

@ -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) {
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) {
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) {

View file

@ -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,19 +146,24 @@ 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(() -> {
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);
InputMethodManager imm =
(InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
imm.showSoftInput(
mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
}
});
if (binding.speedDial.isOpen()) {
@ -161,7 +173,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
public boolean onMenuItemActionCollapse(@NonNull final MenuItem item) {
SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
mSearchEditText.setText("");
filter(null);
@ -169,7 +181,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
return true;
}
};
private final TextWatcher mSearchTextWatcher = new TextWatcher() {
private final TextWatcher mSearchTextWatcher =
new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
@ -177,15 +190,14 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {}
};
private MenuItem mMenuSearchView;
private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener =
new ListItemAdapter.OnTagClickedListener() {
@Override
public void onTagClicked(String tag) {
if (mMenuSearchView != null) {
@ -198,10 +210,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
};
private Pair<Integer, Intent> mPostponedActivityResult;
private Toast mToast;
private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
private final UiCallback<Conversation> mAdhocConferenceCallback =
new UiCallback<>() {
@Override
public void success(final Conversation conversation) {
runOnUiThread(() -> {
runOnUiThread(
() -> {
hideToast();
switchToConversation(conversation);
});
@ -213,12 +227,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {
}
public void userInputRequired(PendingIntent pi, Conversation object) {}
};
private ActivityStartConversationBinding binding;
private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
private final TextView.OnEditorActionListener mSearchDone =
new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
int pos = binding.startConversationViewPager.getCurrentItem();
@ -226,7 +239,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (contacts.size() == 1) {
openConversation(contacts.get(0));
return true;
} else if (contacts.size() == 0 && conferences.size() == 1) {
} else if (contacts.isEmpty() && conferences.size() == 1) {
openConversationsForBookmark((Bookmark) conferences.get(0));
return true;
}
@ -234,7 +247,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
if (conferences.size() == 1) {
openConversationsForBookmark((Bookmark) conferences.get(0));
return true;
} else if (conferences.size() == 0 && contacts.size() == 1) {
} else if (conferences.isEmpty() && contacts.size() == 1) {
openConversation(contacts.get(0));
return true;
}
@ -245,20 +258,26 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
};
public static void populateAccountSpinner(final Context context, final List<String> accounts, final AutoCompleteTextView spinner) {
public static void populateAccountSpinner(
final Context context,
final List<String> accounts,
final AutoCompleteTextView spinner) {
if (accounts.isEmpty()) {
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
ArrayAdapter<String> adapter =
new ArrayAdapter<>(
context,
R.layout.item_autocomplete,
Collections.singletonList(context.getString(R.string.no_accounts)));
adapter.setDropDownViewResource(R.layout.item_autocomplete);
spinner.setAdapter(adapter);
spinner.setEnabled(false);
} else {
final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
final ArrayAdapter<String> adapter =
new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts);
adapter.setDropDownViewResource(R.layout.item_autocomplete);
spinner.setAdapter(adapter);
spinner.setEnabled(true);
spinner.setText(Iterables.getFirst(accounts,null),false);
spinner.setText(Iterables.getFirst(accounts, null), false);
}
}
@ -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,7 +327,8 @@ 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() {
binding.startConversationViewPager.addOnPageChangeListener(
new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
updateSearchViewHint();
@ -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))
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,8 +674,12 @@ 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) -> {
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());
});
@ -567,11 +694,19 @@ 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) -> {
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);
@ -581,7 +716,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
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<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
if (account != null && jids.size() > 0) {
// This hardcodes cheogram.com and is in general a terrible hack
@ -916,11 +1059,21 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
super.onActivityResult(requestCode, requestCode, intent);
}
private void askForContactsPermissions() {
if (QuickConversationsService.isContactListIntegration(this)) {
private boolean askForContactsPermissions() {
if (!QuickConversationsService.isContactListIntegration(this)) {
return false;
}
if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
== PackageManager.PERMISSION_GRANTED) {
return false;
}
if (mRequestedContactsPermission.compareAndSet(false, true)) {
final ImmutableList.Builder<String> permissionBuilder = new ImmutableList.Builder<>();
permissionBuilder.add(Manifest.permission.READ_CONTACTS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS);
}
final String[] permission = permissionBuilder.build().toArray(new String[0]);
final String consent =
PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
.getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
@ -929,19 +1082,19 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
|| 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;
Log.d(
Config.LOGTAG,
"not asking for contacts permission because consent has been declined");
return false;
}
if (requiresConsent
|| shouldShowRequestPermissionRationale(
Manifest.permission.READ_CONTACTS)) {
|| 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)));
Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
} else {
builder.setTitle(R.string.sync_with_contacts);
builder.setMessage(
@ -962,32 +1115,29 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
PreferenceManager.getDefaultSharedPreferences(
getApplicationContext())
.edit()
.putString(
PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
.putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
.apply();
}
if (requestPermission.compareAndSet(false, true)) {
requestPermissions(
new String[] {Manifest.permission.READ_CONTACTS},
REQUEST_SYNC_CONTACTS);
requestPermissions(permission, REQUEST_SYNC_CONTACTS);
}
});
if (requiresConsent) {
builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
builder.setNegativeButton(
R.string.decline,
(dialog, which) ->
PreferenceManager.getDefaultSharedPreferences(
getApplicationContext())
.edit()
.putString(
PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
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);
requestPermissions(permission, REQUEST_SYNC_CONTACTS);
}
});
}
@ -1003,17 +1153,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
});
dialog.show();
} else {
requestPermissions(
new String[] {Manifest.permission.READ_CONTACTS},
REQUEST_SYNC_CONTACTS);
}
}
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<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
List<Contact> contacts =
xmppConnectionService.findContacts(invite.getJid(), invite.account);
final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account);
if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) {
if (muc != null && !invite.forceDialog) {
@ -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) -> {
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.setNegativeButton(
R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
@ -1235,7 +1399,8 @@ 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)) {
@ -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,28 +1602,31 @@ 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<Conversation>() {
xmppConnectionService.createPublicChannel(
account,
name,
address,
new UiCallback<Conversation>() {
@Override
public void success(Conversation conversation) {
runOnUiThread(() -> {
runOnUiThread(
() -> {
hideToast();
switchToConversation(conversation);
});
}
@Override
public void error(int errorCode, Conversation conversation) {
runOnUiThread(() -> {
runOnUiThread(
() -> {
replaceToast(getString(errorCode));
switchToConversation(conversation);
});
}
@Override
public void userInputRequired(PendingIntent pi, Conversation object) {
}
public void userInputRequired(PendingIntent pi, Conversation object) {}
});
}
@ -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) {

View file

@ -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");

View file

@ -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) {

File diff suppressed because it is too large Load diff

View file

@ -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());

View file

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

View file

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

View file

@ -196,7 +196,12 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
uri = appSettings().getRingtone();
}
Log.i(Config.LOGTAG, "current ringtone: " + 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() {

View file

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

View file

@ -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,8 +41,8 @@ public class ExceptionHelper {
return false;
} catch (final ClassNotFoundException e) { }
try {
final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
final XmppConnectionService service =
activity == null ? null : activity.xmppConnectionService;
if (service == null) {
return false;
}
@ -58,59 +54,52 @@ public class ExceptionHelper {
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) {
final var file = new File(activity.getCacheDir(), FILENAME);
if (!file.exists()) {
return false;
}
String line;
while ((line = stacktrace.readLine()) != null) {
report.append(line);
report.append('\n');
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");
}
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);
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.setNegativeButton(
activity.getText(R.string.send_never),
(dialog, which) -> appSettings.setSendCrashReports(false));
builder.create().show();
return true;
} catch (final IOException ignored) {
return false;
}
}
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);
}
}
}

View file

@ -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");

View file

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

View file

@ -18,6 +18,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.ForegroundInfo;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
@ -35,7 +36,6 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.receiver.WorkManagerEventReceiver;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
@ -99,6 +99,7 @@ public class ExportBackupWorker extends Worker {
@NonNull
@Override
public Result doWork() {
setForegroundAsync(getForegroundInfo());
final List<File> files;
try {
files = export();
@ -227,18 +228,14 @@ public class ExportBackupWorker extends Worker {
IV,
salt);
final var notification = getNotification();
if (!recurringBackup) {
final var cancel = new Intent(context, WorkManagerEventReceiver.class);
cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP);
final var cancelPendingIntent =
PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS);
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()) {

View file

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

View file

@ -37,7 +37,7 @@ public class LocalizedContent {
}
}
}
if (contents.size() == 0) {
if (contents.isEmpty()) {
return null;
}
final String userLanguage = Locale.getDefault().getLanguage();

View file

@ -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";
@ -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";
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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()));
}

View file

@ -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);
}

View file

@ -45,13 +45,13 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed;
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.webrtc.DtmfSender;
import org.webrtc.EglBase;
@ -145,24 +145,25 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
@Override
synchronized void deliverPacket(final JinglePacket jinglePacket) {
switch (jinglePacket.getAction()) {
case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
case CONTENT_ADD -> receiveContentAdd(jinglePacket);
case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket);
case CONTENT_REJECT -> receiveContentReject(jinglePacket);
case CONTENT_REMOVE -> receiveContentRemove(jinglePacket);
case CONTENT_MODIFY -> receiveContentModify(jinglePacket);
synchronized void deliverPacket(final Iq iq) {
final var jingle = iq.getExtension(Jingle.class);
switch (jingle.getAction()) {
case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle);
case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle);
case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle);
case SESSION_TERMINATE -> receiveSessionTerminate(iq);
case CONTENT_ADD -> receiveContentAdd(iq, jingle);
case CONTENT_ACCEPT -> receiveContentAccept(iq);
case CONTENT_REJECT -> receiveContentReject(iq, jingle);
case CONTENT_REMOVE -> receiveContentRemove(iq, jingle);
case CONTENT_MODIFY -> receiveContentModify(iq, jingle);
default -> {
respondOk(jinglePacket);
respondOk(iq);
Log.d(
Config.LOGTAG,
String.format(
"%s: received unhandled jingle action %s",
id.account.getJid().asBareJid(), jinglePacket.getAction()));
id.account.getJid().asBareJid(), jingle.getAction()));
}
}
}
@ -193,9 +194,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
return webRTCWrapper.applyDtmfTone(tone);
}
private void receiveSessionTerminate(final JinglePacket jinglePacket) {
private void receiveSessionTerminate(final Iq jinglePacket) {
respondOk(jinglePacket);
final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
final var jingle = jinglePacket.getExtension(Jingle.class);
final Jingle.ReasonWrapper wrapper = jingle.getReason();
final State previous = this.state;
Log.d(
Config.LOGTAG,
@ -224,7 +226,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
finish();
}
private void receiveTransportInfo(final JinglePacket jinglePacket) {
private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) {
// Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to
// INITIALIZED only after transport-info has been received
if (isInState(
@ -235,7 +237,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
State.SESSION_ACCEPTED)) {
final RtpContentMap contentMap;
try {
contentMap = RtpContentMap.of(jinglePacket);
contentMap = RtpContentMap.of(jingle);
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(
Config.LOGTAG,
@ -265,7 +267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void receiveTransportInfo(
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
final Iq jinglePacket, final RtpContentMap contentMap) {
final Set<Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>>
candidates = contentMap.contents.entrySet();
final RtpContentMap remote = getRemoteContentMap();
@ -304,17 +306,17 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void receiveContentAdd(final JinglePacket jinglePacket) {
private void receiveContentAdd(final Iq iq, final Jingle jingle) {
final RtpContentMap modification;
try {
modification = RtpContentMap.of(jinglePacket);
modification = RtpContentMap.of(jingle);
modification.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
Throwables.getRootCause(e));
respondOk(jinglePacket);
respondOk(iq);
webRTCWrapper.close();
sendSessionTerminate(Reason.of(e), e.getMessage());
return;
@ -330,12 +332,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
new FutureCallback<>() {
@Override
public void onSuccess(final RtpContentMap rtpContentMap) {
receiveContentAdd(jinglePacket, rtpContentMap);
receiveContentAdd(iq, rtpContentMap);
}
@Override
public void onFailure(@NonNull Throwable throwable) {
respondOk(jinglePacket);
respondOk(iq);
final Throwable rootCause = Throwables.getRootCause(throwable);
Log.d(
Config.LOGTAG,
@ -349,12 +351,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
},
MoreExecutors.directExecutor());
} else {
terminateWithOutOfOrder(jinglePacket);
terminateWithOutOfOrder(iq);
}
}
private void receiveContentAdd(
final JinglePacket jinglePacket, final RtpContentMap modification) {
final Iq jinglePacket, final RtpContentMap modification) {
final RtpContentMap remote = getRemoteContentMap();
if (!Collections.disjoint(modification.getNames(), remote.getNames())) {
respondOk(jinglePacket);
@ -406,10 +408,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void receiveContentAccept(final JinglePacket jinglePacket) {
private void receiveContentAccept(final Iq jinglePacket) {
final var jingle = jinglePacket.getExtension(Jingle.class);
final RtpContentMap receivedContentAccept;
try {
receivedContentAccept = RtpContentMap.of(jinglePacket);
receivedContentAccept = RtpContentMap.of(jingle);
receivedContentAccept.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
@ -494,14 +497,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
updateEndUserState();
}
private void receiveContentModify(final JinglePacket jinglePacket) {
private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) {
if (this.state != State.SESSION_ACCEPTED) {
terminateWithOutOfOrder(jinglePacket);
return;
}
final Map<String, Content.Senders> modification =
Maps.transformEntries(
jinglePacket.getJingleContents(), (key, value) -> value.getSenders());
jingle.getJingleContents(), (key, value) -> value.getSenders());
final boolean isInitiator = isInitiator();
final RtpContentMap currentOutgoing = this.outgoingContentAdd;
final RtpContentMap remoteContentMap = this.getRemoteContentMap();
@ -604,10 +607,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
return candidateBuilder.build();
}
private void receiveContentReject(final JinglePacket jinglePacket) {
private void receiveContentReject(final Iq jinglePacket, final Jingle jingle) {
final RtpContentMap receivedContentReject;
try {
receivedContentReject = RtpContentMap.of(jinglePacket);
receivedContentReject = RtpContentMap.of(jingle);
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
@ -660,10 +663,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ summary);
}
private void receiveContentRemove(final JinglePacket jinglePacket) {
private void receiveContentRemove(final Iq jinglePacket, final Jingle jingle) {
final RtpContentMap receivedContentRemove;
try {
receivedContentRemove = RtpContentMap.of(jinglePacket);
receivedContentRemove = RtpContentMap.of(jingle);
receivedContentRemove.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
@ -697,8 +700,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
String.format(
"%s only supports %s as a means to retract a not yet accepted %s",
BuildConfig.APP_NAME,
JinglePacket.Action.CONTENT_REMOVE,
JinglePacket.Action.CONTENT_ADD));
Jingle.Action.CONTENT_REMOVE,
Jingle.Action.CONTENT_ADD));
}
}
@ -723,10 +726,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
this.outgoingContentAdd = null;
final JinglePacket retract =
final Iq retract =
outgoingContentAdd
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId);
.toJinglePacket(Jingle.Action.CONTENT_REMOVE, id.sessionId);
this.send(retract);
Log.d(
Config.LOGTAG,
@ -782,16 +785,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
"content addition is receive only. we want to upgrade to 'both'");
final RtpContentMap modifiedSenders =
incomingContentAdd.modifiedSenders(Content.Senders.BOTH);
final JinglePacket proposedContentModification =
final Iq proposedContentModification =
modifiedSenders
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId);
.toJinglePacket(Jingle.Action.CONTENT_MODIFY, id.sessionId);
proposedContentModification.setTo(id.with);
xmppConnectionService.sendIqPacket(
id.account,
proposedContentModification,
(account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
@ -885,7 +888,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override
public void onFailure(@NonNull final Throwable throwable) {
failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable);
failureToPerformAction(Jingle.Action.CONTENT_ACCEPT, throwable);
}
},
MoreExecutors.directExecutor());
@ -897,9 +900,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void sendContentAccept(final RtpContentMap contentAcceptMap) {
final JinglePacket jinglePacket =
contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId);
send(jinglePacket);
final Iq iq =
contentAcceptMap.toJinglePacket(Jingle.Action.CONTENT_ACCEPT, id.sessionId);
send(iq);
}
public synchronized void rejectContentAdd() {
@ -913,20 +916,20 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void rejectContentAdd(final RtpContentMap contentMap) {
final JinglePacket jinglePacket =
final Iq iq =
contentMap
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId);
.toJinglePacket(Jingle.Action.CONTENT_REJECT, id.sessionId);
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": rejecting content "
+ ContentAddition.summary(contentMap));
send(jinglePacket);
send(iq);
}
private boolean checkForIceRestart(
final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
final Iq jinglePacket, final RtpContentMap rtpContentMap) {
final RtpContentMap existing = getRemoteContentMap();
final Set<IceUdpTransportInfo.Credentials> existingCredentials;
final IceUdpTransportInfo.Credentials newCredentials;
@ -1005,7 +1008,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private boolean applyIceRestart(
final JinglePacket jinglePacket,
final Iq jinglePacket,
final RtpContentMap restartContentMap,
final boolean isOffer)
throws ExecutionException, InterruptedException {
@ -1106,7 +1109,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private ListenableFuture<RtpContentMap> receiveRtpContentMap(
final JinglePacket jinglePacket, final boolean expectVerification) {
final Jingle jinglePacket, final boolean expectVerification) {
try {
return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification);
} catch (final Exception e) {
@ -1149,12 +1152,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) {
if (isInitiator()) {
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE);
return;
}
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jingle, false);
Futures.addCallback(
future,
new FutureCallback<>() {
@ -1173,7 +1176,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void receiveSessionInitiate(
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
final Iq jinglePacket, final RtpContentMap contentMap) {
try {
contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(true);
@ -1233,13 +1236,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void receiveSessionAccept(final JinglePacket jinglePacket) {
private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) {
if (isResponder()) {
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT);
return;
}
final ListenableFuture<RtpContentMap> future =
receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
receiveRtpContentMap(jingle, this.omemoVerification.hasFingerprint());
Futures.addCallback(
future,
new FutureCallback<>() {
@ -1264,7 +1267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void receiveSessionAccept(
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
final Iq jinglePacket, final RtpContentMap contentMap) {
try {
contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint();
@ -1409,7 +1412,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void failureToPerformAction(
final JinglePacket.Action action, final Throwable throwable) {
final Jingle.Action action, final Throwable throwable) {
if (isTerminated()) {
return;
}
@ -1480,8 +1483,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
transitionOrThrow(State.SESSION_ACCEPTED);
final JinglePacket sessionAccept =
rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
final Iq sessionAccept =
rtpContentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId);
send(sessionAccept);
}
@ -1951,8 +1954,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
this.transitionOrThrow(targetState);
final JinglePacket sessionInitiate =
rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
final Iq sessionInitiate =
rtpContentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId);
send(sessionInitiate);
}
@ -2020,9 +2023,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ contentName);
return;
}
final JinglePacket jinglePacket =
transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
send(jinglePacket);
final Iq iq =
transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
send(iq);
}
public RtpEndUserState getEndUserState() {
@ -2340,7 +2343,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
this.webRTCWrapper.setup(this.xmppConnectionService);
this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled());
// this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled());
this.webRTCWrapper.setMicrophoneEnabledOrThrow(true);
}
private void acceptCallFromProposed() {
@ -2375,8 +2379,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void sendJingleMessage(final String action, final Jid to) {
final MessagePacket messagePacket = new MessagePacket();
messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those
final var messagePacket = new im.conversations.android.xmpp.model.stanza.Message();
messagePacket.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); // we want to carbon copy those
messagePacket.setTo(to);
final Element intent =
messagePacket
@ -2397,7 +2401,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void sendJingleMessageFinish(final Reason reason) {
final var account = id.getAccount();
final MessagePacket messagePacket =
final var messagePacket =
xmppConnectionService
.getMessageGenerator()
.sessionFinish(id.with, id.sessionId, reason);
@ -2556,34 +2560,34 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void initiateIceRestart(final RtpContentMap rtpContentMap) {
final RtpContentMap transportInfo = rtpContentMap.transportInfo();
final JinglePacket jinglePacket =
transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket);
jinglePacket.setTo(id.with);
final Iq iq =
transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId);
Log.d(Config.LOGTAG, "initiating ice restart: " + iq);
iq.setTo(id.with);
xmppConnectionService.sendIqPacket(
id.account,
jinglePacket,
(account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
iq,
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
Log.d(Config.LOGTAG, "received success to our ice restart");
setLocalContentMap(rtpContentMap);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
return;
}
if (response.getType() == IqPacket.TYPE.ERROR) {
if (response.getType() == Iq.Type.ERROR) {
if (isTieBreak(response)) {
Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
return;
}
handleIqErrorResponse(response);
}
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
if (response.getType() == Iq.Type.TIMEOUT) {
handleIqTimeoutResponse(response);
}
});
}
private boolean isTieBreak(final IqPacket response) {
private boolean isTieBreak(final Iq response) {
final Element error = response.findChild("error");
return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS);
}
@ -2604,7 +2608,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override
public void onFailure(@NonNull Throwable throwable) {
failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable);
failureToPerformAction(Jingle.Action.CONTENT_ADD, throwable);
}
},
MoreExecutors.directExecutor());
@ -2612,21 +2616,21 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void sendContentAdd(final RtpContentMap contentAdd) {
final JinglePacket jinglePacket =
contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId);
jinglePacket.setTo(id.with);
final Iq iq =
contentAdd.toJinglePacket(Jingle.Action.CONTENT_ADD, id.sessionId);
iq.setTo(id.with);
xmppConnectionService.sendIqPacket(
id.account,
jinglePacket,
(connection, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
iq,
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": received ACK to our content-add");
return;
}
if (response.getType() == IqPacket.TYPE.ERROR) {
if (response.getType() == Iq.Type.ERROR) {
if (isTieBreak(response)) {
this.outgoingContentAdd = null;
Log.d(Config.LOGTAG, "received tie-break as result of our content-add");
@ -2634,7 +2638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
handleIqErrorResponse(response);
}
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
if (response.getType() == Iq.Type.TIMEOUT) {
handleIqTimeoutResponse(response);
}
});
@ -2782,7 +2786,12 @@ public class JingleRtpConnection extends AbstractJingleConnection
@Override
public void onCallIntegrationMicrophoneEnabled(final boolean enabled) {
this.webRTCWrapper.setMicrophoneEnabled(enabled);
// this is called every time we switch audio devices. Thus it would re-enable a microphone
// that was previous disabled by the user. A proper implementation would probably be to
// track user choice and enable the microphone with a userEnabled() &&
// callIntegration.isMicrophoneEnabled() condition
Log.d(Config.LOGTAG, "ignoring onCallIntegrationMicrophoneEnabled(" + enabled + ")");
// this.webRTCWrapper.setMicrophoneEnabled(enabled);
}
@Override
@ -2827,13 +2836,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) {
if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) {
final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
final Iq request = new Iq(Iq.Type.GET);
request.setTo(id.account.getDomain());
request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
xmppConnectionService.sendIqPacket(
id.account,
request,
(account, response) -> {
(response) -> {
final var iceServers = IceServers.parse(response);
if (iceServers.isEmpty()) {
Log.w(

View file

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

View file

@ -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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
}
}

View file

@ -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
}
}

View file

@ -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;
}
}
}

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

@ -0,0 +1,10 @@
package im.conversations.android.xmpp.model;
public abstract class DeliveryReceipt extends Extension {
protected DeliveryReceipt(Class<? extends Extension> clazz) {
super(clazz);
}
public abstract String getId();
}

View file

@ -0,0 +1,8 @@
package im.conversations.android.xmpp.model;
public abstract class DeliveryReceiptRequest extends Extension {
protected DeliveryReceiptRequest(Class<? extends Extension> clazz) {
super(clazz);
}
}

View file

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

View file

@ -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());
}
}
}

View file

@ -0,0 +1,8 @@
package im.conversations.android.xmpp.model;
public abstract class StreamElement extends Extension {
protected StreamElement(Class<? extends StreamElement> clazz) {
super(clazz);
}
}

View file

@ -0,0 +1,8 @@
package im.conversations.android.xmpp.model;
public abstract class StreamFeature extends Extension{
public StreamFeature(Class<? extends StreamFeature> clazz) {
super(clazz);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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);
}
}

View file

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

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show more