Detailed changes
@@ -1,9 +1,14 @@
# Changelog
+### Version 2.18.1
+
+* Fix reactions on files received via P2P
+* Improve URI matching
+
### Version 2.18.0
* Add ability to pick backup location
-* More more URIs (tel:, mailto:) clickable
+* Make more URIs (tel:, mailto:) clickable
### Version 2.17.12
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.5.2'
+ classpath 'com.android.tools.build:gradle:8.9.1'
classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2"
}
}
@@ -85,7 +85,7 @@ dependencies {
implementation "androidx.preference:preference:1.2.1"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.13.0-alpha10'
- implementation 'androidx.work:work-runtime:2.9.1'
+ implementation 'androidx.work:work-runtime:2.10.0'
implementation "androidx.emoji2:emoji2:1.5.0"
freeImplementation "androidx.emoji2:emoji2-bundled:1.5.0"
@@ -113,7 +113,7 @@ dependencies {
implementation "com.squareup.retrofit2:converter-gson:2.11.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
- implementation 'com.google.guava:guava:33.4.0-android'
+ implementation 'com.google.guava:guava:33.4.6-android'
implementation 'io.michaelrocks:libphonenumber-android:8.13.52'
implementation 'im.conversations.webrtc:webrtc-android:129.0.0'
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
@@ -147,11 +147,11 @@ ext {
android {
namespace 'eu.siacs.conversations'
- compileSdk 34
+ compileSdk 35
defaultConfig {
minSdkVersion 23
- targetSdkVersion 34
+ targetSdkVersion 35
versionCode 42025 + tags.size()
versionName grgit.describe(always: true)
applicationId "eu.siacs.conversations"
@@ -0,0 +1,2 @@
+* Fix reactions on files received via P2P
+* Improve URI matching
@@ -0,0 +1,2 @@
+* Corrette le reazioni sui file ricevuti via P2P
+* Migliorata la corrispondenza degli URI
@@ -0,0 +1,2 @@
+* Shfrytëzim i SASL SCRAM Downgrade Protection (XEP-0474)
+* Dërgim reagimesh ndaj MP MUC te JID i saktë
@@ -0,0 +1,2 @@
+* Виправлено реакції на файли, отримані через P2P
+* Покращено співставлення URI
@@ -0,0 +1,2 @@
+* 修复对通过 P2P 接收的文件的回应
+* 改进 URI 匹配
@@ -1,8 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -2,9 +2,9 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
-
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -2,9 +2,9 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
-
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -34,6 +34,7 @@ import android.text.Editable;
import android.text.Spanned;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
+import android.text.Spannable;
import com.google.common.base.Splitter;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
@@ -76,7 +77,7 @@ public class Linkify {
};
}
- public static void addLinks(final Editable body) {
+ public static void addLinks(final Spannable body) {
android.text.util.Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null);
}
@@ -1,6 +1,7 @@
package de.gultsch.common;
import android.net.Uri;
+import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
@@ -11,7 +12,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import org.checkerframework.checker.nullness.qual.NonNull;
public class MiniUri {
@@ -6,7 +6,7 @@ public class Patterns {
public static final Pattern URI_GENERIC =
Pattern.compile(
- "(?<=^|\\p{Z}|\\s|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini|bitcoin|bitcoincash|ethereum|monero|wownero):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
+ "(?<=^|\\p{Z}|\\s|\\p{P}|<)(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini|bitcoin|bitcoincash|ethereum|monero|wownero):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*");
public static final Pattern URI_TEL =
Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$");
@@ -141,6 +141,8 @@ public final class Config {
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final boolean MUC_LEAVE_BEFORE_JOIN = false;
+ // if this is set to true messages that contain multiple bodies (per language) will be ignored
+ public static final boolean TREAT_MULTI_CONTENT_AS_INVALID = false;
public static final boolean USE_LMC_VERSION_1_1 = true;
@@ -2,6 +2,8 @@ package eu.siacs.conversations.crypto;
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.NonNull;
+import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
@@ -12,6 +14,7 @@ import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -23,7 +26,6 @@ import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.x500.RDN;
-import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
@@ -36,18 +38,18 @@ public class XmppDomainVerifier {
private static final String XMPP_ADDR = "1.3.6.1.5.5.7.8.5";
private static List<String> getCommonNames(final X509Certificate certificate) {
- List<String> domains = new ArrayList<>();
+ final var domains = new ImmutableList.Builder<String>();
try {
- X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
- RDN[] rdns = x500name.getRDNs(BCStyle.CN);
- for (int i = 0; i < rdns.length; ++i) {
+ final var x500name = new JcaX509CertificateHolder(certificate).getSubject();
+ final RDN[] nameRDNs = x500name.getRDNs(BCStyle.CN);
+ for (int i = 0; i < nameRDNs.length; ++i) {
domains.add(
IETFUtils.valueToString(
x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue()));
}
- return domains;
- } catch (CertificateEncodingException e) {
- return domains;
+ return domains.build();
+ } catch (final CertificateEncodingException e) {
+ return Collections.emptyList();
}
}
@@ -75,17 +77,24 @@ public class XmppDomainVerifier {
}
}
- public static boolean matchDomain(final String needle, final List<String> haystack) {
- for (final String entry : haystack) {
- if (entry.startsWith("*.")) {
+ public static boolean matchDomain(final String domain, final List<String> certificateDomains) {
+ for (final String certificateDomain : certificateDomains) {
+ if (certificateDomain.startsWith("*.")) {
// https://www.rfc-editor.org/rfc/rfc6125#section-6.4.3
// wild cards can only be in the left most label and don’t match '.'
- final int i = needle.indexOf('.');
- if (i != -1 && needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
+ final var wildcardEntry = certificateDomain.substring(1);
+ if (CharMatcher.is('.').countIn(wildcardEntry) < 2) {
+ Log.w(LOGTAG, "not enough labels in wildcard certificate");
+ break;
+ }
+ final int position = domain.indexOf('.');
+ if (position != -1 && domain.substring(position).equalsIgnoreCase(wildcardEntry)) {
+ Log.d(LOGTAG, "domain " + domain + " matched " + certificateDomain);
return true;
}
} else {
- if (entry.equalsIgnoreCase(needle)) {
+ if (certificateDomain.equalsIgnoreCase(domain)) {
+ Log.d(LOGTAG, "domain " + domain + " matched " + certificateDomain);
return true;
}
}
@@ -184,6 +193,7 @@ public class XmppDomainVerifier {
return all.build();
}
+ @NonNull
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -6,10 +6,8 @@ import android.os.Bundle;
import android.security.KeyChain;
import android.util.Log;
import android.util.Pair;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -17,38 +15,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.InvalidKeyIdException;
-import org.whispersystems.libsignal.SessionBuilder;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.UntrustedIdentityException;
-import org.whispersystems.libsignal.ecc.ECPublicKey;
-import org.whispersystems.libsignal.state.PreKeyBundle;
-import org.whispersystems.libsignal.state.PreKeyRecord;
-import org.whispersystems.libsignal.state.SignedPreKeyRecord;
-import org.whispersystems.libsignal.util.KeyHelper;
-
-import java.security.PrivateKey;
-import java.security.Security;
-import java.security.Signature;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -71,6 +37,35 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportIn
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
import im.conversations.android.xmpp.model.stanza.Iq;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.InvalidKeyIdException;
+import org.whispersystems.libsignal.SessionBuilder;
+import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.UntrustedIdentityException;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
+import org.whispersystems.libsignal.state.PreKeyBundle;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+import org.whispersystems.libsignal.state.SignedPreKeyRecord;
+import org.whispersystems.libsignal.util.KeyHelper;
public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
@@ -102,8 +97,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private int numPublishTriesOnEmptyPep = 0;
private boolean pepBroken = false;
private int lastDeviceListNotificationHash = 0;
- private final Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
- private final Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
+ private final Set<XmppAxolotlSession> postponedSessions =
+ new HashSet<>(); // sessions stored here will receive after mam catchup treatment
+ private final Set<SignalProtocolAddress> postponedHealing =
+ new HashSet<>(); // addresses stored here will need a healing notification after mam
+ // catchup
private final AtomicBoolean changeAccessMode = new AtomicBoolean(false);
public AxolotlService(Account account, XmppConnectionService connectionService) {
@@ -156,7 +154,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
for (Jid jid : jids) {
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
- SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId);
+ SignalProtocolAddress address =
+ new SignalProtocolAddress(jid.toString(), foreignId);
if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) {
return true;
}
@@ -167,11 +166,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public void preVerifyFingerprint(Contact contact, String fingerprint) {
- axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint);
+ axolotlStore.preVerifyFingerprint(
+ contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint);
}
public void preVerifyFingerprint(Account account, String fingerprint) {
- axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint);
+ axolotlStore.preVerifyFingerprint(
+ account, account.getJid().asBareJid().toString(), fingerprint);
}
public boolean hasVerifiedKeys(String name) {
@@ -184,11 +185,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public String getOwnFingerprint() {
- return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
+ return CryptoHelper.bytesToHex(
+ axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
}
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
- return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status);
+ return axolotlStore.getContactKeysWithTrust(
+ account.getJid().asBareJid().toString(), status);
}
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
@@ -226,21 +229,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public Collection<XmppAxolotlSession> findOwnSessions() {
SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid());
- ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values());
+ ArrayList<XmppAxolotlSession> s =
+ new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values());
Collections.sort(s);
return s;
}
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
- ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
+ ArrayList<XmppAxolotlSession> s =
+ new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
Collections.sort(s);
return s;
}
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
if (conversation.getContact().isSelf()) {
- //will be added in findOwnSessions()
+ // will be added in findOwnSessions()
return Collections.emptySet();
}
HashSet<XmppAxolotlSession> sessions = new HashSet<>();
@@ -280,7 +285,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public void destroy() {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": destroying old axolotl service. no longer in use");
mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
}
@@ -306,7 +314,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
final boolean me = jid.asBareJid().equals(account.getJid().asBareJid());
if (me) {
if (hash != 0 && hash == this.lastDeviceListNotificationHash) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": ignoring duplicate own device id list");
return;
}
this.lastDeviceListNotificationHash = hash;
@@ -315,10 +325,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (me) {
deviceIds.remove(getOwnDeviceId());
}
- final Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString()));
+ final Set<Integer> expiredDevices =
+ new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString()));
expiredDevices.removeAll(deviceIds);
for (Integer deviceId : expiredDevices) {
- SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+ SignalProtocolAddress address =
+ new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (session.getTrust().isActive()) {
@@ -328,11 +340,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
final Set<Integer> newDevices = ImmutableSet.copyOf(deviceIds);
for (final Integer deviceId : newDevices) {
- SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+ SignalProtocolAddress address =
+ new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (!session.getTrust().isActive()) {
- Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint());
+ Log.d(
+ Config.LOGTAG,
+ "reactivating device with fingerprint " + session.getFingerprint());
session.setTrust(session.getTrust().toActive());
}
}
@@ -343,7 +358,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
needsPublishing |= this.changeAccessMode.get();
for (final Integer deviceId : deviceIds) {
- SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+ SignalProtocolAddress ownDeviceAddress =
+ new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) {
FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
if (status == null || status == FetchStatus.TIMEOUT) {
@@ -363,7 +379,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
final boolean changed = oldSet == null || oldSet.hashCode() != hash;
this.deviceIds.put(jid, deviceIds);
if (changed) {
- mXmppConnectionService.updateConversationUi(); //update the lock icon
+ mXmppConnectionService.updateConversationUi(); // update the lock icon
mXmppConnectionService.keyStatusUpdated(null);
if (me) {
mXmppConnectionService.updateAccountUi();
@@ -375,7 +391,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void wipeOtherPepDevices() {
if (pepBroken) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
return;
}
Set<Integer> deviceIds = new HashSet<>();
@@ -391,22 +410,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private void publishOwnDeviceIdIfNeeded() {
if (pepBroken) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
return;
}
- Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid());
- mXmppConnectionService.sendIqPacket(account, packet, response -> {
- if (response.getType() == Iq.Type.TIMEOUT) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
- } else {
- //TODO consider calling registerDevices only after item-not-found to account for broken PEPs
- final Element item = IqParser.getItem(response);
- final Set<Integer> deviceIds = IqParser.deviceIds(item);
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds);
- registerDevices(account.getJid().asBareJid(), deviceIds);
- }
-
- });
+ 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
+ // TODO use new API
+ 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);
+ }
+ });
}
private Set<Integer> getExpiredDevices() {
@@ -415,16 +451,34 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (session.getTrust().isActive()) {
long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
if (diff > Config.OMEMO_AUTO_EXPIRY) {
- long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint());
+ long lastMessageDiff =
+ System.currentTimeMillis()
+ - mXmppConnectionService.databaseBackend
+ .getLastTimeFingerprintUsed(
+ account, session.getFingerprint());
long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0));
if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
devices.add(session.getRemoteAddress().getDeviceId());
session.setTrust(session.getTrust().toInactive());
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": added own device "
+ + session.getFingerprint()
+ + " to list of expired devices. Last message received "
+ + hours
+ + " hours ago");
} else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": own device "
+ + session.getFingerprint()
+ + " was active "
+ + hours
+ + " hours ago");
}
- } //TODO print last activation diff
+ } // TODO print last activation diff
}
}
return devices;
@@ -435,12 +489,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids");
if (deviceIdsCopy.isEmpty()) {
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
- Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
+ Log.w(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Own device publish attempt threshold exceeded, aborting...");
pepBroken = true;
return;
} else {
numPublishTriesOnEmptyPep++;
- Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
+ Log.w(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Own device list empty, attempting to publish (try "
+ + numPublishTriesOnEmptyPep
+ + ")");
}
} else {
numPublishTriesOnEmptyPep = 0;
@@ -453,74 +515,136 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
publishDeviceIdsAndRefineAccessModel(ids, true);
}
- private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
- final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
- final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
- mXmppConnectionService.sendIqPacket(account, publish, response -> {
- final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
- final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response);
- if (firstAttempt && preConditionNotMet) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration");
- mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- publishDeviceIdsAndRefineAccessModel(ids, false);
- }
-
- @Override
- public void onPushFailed() {
- publishDeviceIdsAndRefineAccessModel(ids, false);
+ private void publishDeviceIdsAndRefineAccessModel(
+ final Set<Integer> ids, final boolean firstAttempt) {
+ final Bundle publishOptions =
+ account.getXmppConnection().getFeatures().pepPublishOptions()
+ ? PublishOptions.openAccess()
+ : null;
+ final var publish =
+ mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
+ mXmppConnectionService.sendIqPacket(
+ account,
+ publish,
+ response -> {
+ final Element error =
+ response.getType() == Iq.Type.ERROR
+ ? response.findChild("error")
+ : null;
+ final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response);
+ if (firstAttempt && preConditionNotMet) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": precondition wasn't met for device list. pushing node"
+ + " configuration");
+ mXmppConnectionService.pushNodeConfiguration(
+ account,
+ AxolotlService.PEP_DEVICE_LIST,
+ publishOptions,
+ new XmppConnectionService.OnConfigurationPushed() {
+ @Override
+ public void onPushSucceeded() {
+ publishDeviceIdsAndRefineAccessModel(ids, false);
+ }
+
+ @Override
+ public void onPushFailed() {
+ publishDeviceIdsAndRefineAccessModel(ids, false);
+ }
+ });
+ } else {
+ if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": done changing access mode");
+ account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ }
+ if (response.getType() == Iq.Type.ERROR) {
+ if (preConditionNotMet) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": device list pre condition still not met on"
+ + " second attempt");
+ } else if (error != null) {
+ pepBroken = true;
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Error received while publishing own device id"
+ + response.findChild("error"));
+ }
+ }
}
});
- } else {
- if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode");
- account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
- mXmppConnectionService.databaseBackend.updateAccount(account);
- }
- if (response.getType() == Iq.Type.ERROR) {
- if (preConditionNotMet) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt");
- } else if (error != null) {
- pepBroken = true;
- Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error"));
- }
-
- }
- }
- });
}
- public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
- final Set<PreKeyRecord> preKeyRecords,
- final boolean announceAfter,
- final boolean wipe) {
+ public void publishDeviceVerificationAndBundle(
+ final SignedPreKeyRecord signedPreKeyRecord,
+ final Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
try {
IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
- PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
- X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
+ PrivateKey x509PrivateKey =
+ KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
+ X509Certificate[] chain =
+ KeyChain.getCertificateChain(
+ mXmppConnectionService, account.getPrivateKeyAlias());
Signature verifier = Signature.getInstance("sha256WithRSA");
verifier.initSign(x509PrivateKey, SECURE_RANDOM);
verifier.update(axolotlPublicKey.serialize());
byte[] signature = verifier.sign();
- 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, response -> {
- String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
- mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable");
- publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
- }
-
- @Override
- public void onPushFailed() {
- Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node");
- publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
- }
- });
- });
+ 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,
+ response -> {
+ String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
+ mXmppConnectionService.pushNodeConfiguration(
+ account,
+ node,
+ PublishOptions.openAccess(),
+ new XmppConnectionService.OnConfigurationPushed() {
+ @Override
+ public void onPushSucceeded() {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "configured verification node to be world"
+ + " readable");
+ publishDeviceBundle(
+ signedPreKeyRecord,
+ preKeyRecords,
+ announceAfter,
+ wipe);
+ }
+
+ @Override
+ public void onPushFailed() {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "unable to set access model on"
+ + " verification node");
+ publishDeviceBundle(
+ signedPreKeyRecord,
+ preKeyRecords,
+ announceAfter,
+ wipe);
+ }
+ });
+ });
} catch (Exception e) {
e.printStackTrace();
}
@@ -528,175 +652,310 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
if (pepBroken) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
return;
}
if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
- this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
+ this.changeAccessMode.set(
+ account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
} else {
if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change");
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": server doesn’t support publish-options. setting for later"
+ + " access mode change");
mXmppConnectionService.databaseBackend.updateAccount(account);
}
}
if (this.changeAccessMode.get()) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model");
- }
- final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
- mXmppConnectionService.sendIqPacket(account, packet, response -> {
-
- if (response.getType() == Iq.Type.TIMEOUT) {
- return; //ignore timeout. do nothing
- }
-
- if (response.getType() == Iq.Type.ERROR) {
- Element error = response.findChild("error");
- if (error == null || !error.hasChild("item-not-found")) {
- pepBroken = true;
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response);
- return;
- }
- }
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": server gained publish-options capabilities. changing access"
+ + " model");
+ }
+ final Iq packet =
+ mXmppConnectionService
+ .getIqGenerator()
+ .retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
+ mXmppConnectionService.sendIqPacket(
+ account,
+ packet,
+ response -> {
+ if (response.getType() == Iq.Type.TIMEOUT) {
+ return; // ignore timeout. do nothing
+ }
- PreKeyBundle bundle = IqParser.bundle(response);
- final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
- boolean flush = false;
- if (bundle == null) {
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response);
- bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
- flush = true;
- }
- if (keys == null) {
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response);
- }
- try {
- boolean changed = false;
- // Validate IdentityKey
- IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
- if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
- changed = true;
- }
+ if (response.getType() == Iq.Type.ERROR) {
+ Element error = response.findChild("error");
+ if (error == null || !error.hasChild("item-not-found")) {
+ pepBroken = true;
+ Log.w(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "request for device bundles came back with something"
+ + " other than item-not-found"
+ + response);
+ return;
+ }
+ }
- // Validate signedPreKeyRecord + ID
- SignedPreKeyRecord signedPreKeyRecord;
- int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
- try {
- signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
- if (flush
- || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
- || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
- signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
- axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
- changed = true;
+ PreKeyBundle bundle = IqParser.bundle(response);
+ final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
+ boolean flush = false;
+ if (bundle == null) {
+ Log.w(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Received invalid bundle:"
+ + response);
+ bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
+ flush = true;
}
- } catch (InvalidKeyIdException e) {
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
- signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
- axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
- changed = true;
- }
+ if (keys == null) {
+ Log.w(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Received invalid prekeys:"
+ + response);
+ }
+ try {
+ boolean changed = false;
+ // Validate IdentityKey
+ IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
+ if (flush
+ || !identityKeyPair
+ .getPublicKey()
+ .equals(bundle.getIdentityKey())) {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Adding own IdentityKey "
+ + identityKeyPair.getPublicKey()
+ + " to PEP.");
+ changed = true;
+ }
- // Validate PreKeys
- Set<PreKeyRecord> preKeyRecords = new HashSet<>();
- if (keys != null) {
- for (Integer id : keys.keySet()) {
+ // Validate signedPreKeyRecord + ID
+ SignedPreKeyRecord signedPreKeyRecord;
+ int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
try {
- PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
- if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
- preKeyRecords.add(preKeyRecord);
+ signedPreKeyRecord =
+ axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+ if (flush
+ || !bundle.getSignedPreKey()
+ .equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+ || !Arrays.equals(
+ bundle.getSignedPreKeySignature(),
+ signedPreKeyRecord.getSignature())) {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Adding new signedPreKey with ID "
+ + (numSignedPreKeys + 1)
+ + " to PEP.");
+ signedPreKeyRecord =
+ KeyHelper.generateSignedPreKey(
+ identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(
+ signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
}
- } catch (InvalidKeyIdException ignored) {
+ } catch (InvalidKeyIdException e) {
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Adding new signedPreKey with ID "
+ + (numSignedPreKeys + 1)
+ + " to PEP.");
+ signedPreKeyRecord =
+ KeyHelper.generateSignedPreKey(
+ identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(
+ signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
}
- }
- }
- int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
- if (newKeys > 0) {
- List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
- axolotlStore.getCurrentPreKeyId() + 1, newKeys);
- preKeyRecords.addAll(newRecords);
- for (PreKeyRecord record : newRecords) {
- axolotlStore.storePreKey(record.getId(), record);
- }
- changed = true;
- Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
- }
+ // Validate PreKeys
+ Set<PreKeyRecord> preKeyRecords = new HashSet<>();
+ if (keys != null) {
+ for (Integer id : keys.keySet()) {
+ try {
+ PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
+ if (preKeyRecord
+ .getKeyPair()
+ .getPublicKey()
+ .equals(keys.get(id))) {
+ preKeyRecords.add(preKeyRecord);
+ }
+ } catch (InvalidKeyIdException ignored) {
+ }
+ }
+ }
+ int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
+ if (newKeys > 0) {
+ List<PreKeyRecord> newRecords =
+ KeyHelper.generatePreKeys(
+ axolotlStore.getCurrentPreKeyId() + 1, newKeys);
+ preKeyRecords.addAll(newRecords);
+ for (PreKeyRecord record : newRecords) {
+ axolotlStore.storePreKey(record.getId(), record);
+ }
+ changed = true;
+ Log.i(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Adding "
+ + newKeys
+ + " new preKeys to PEP.");
+ }
- if (changed || changeAccessMode.get()) {
- if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
- mXmppConnectionService.publishDisplayName(account);
- publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
- } else {
- publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
- }
- } else {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
- if (wipe) {
- wipeOtherPepDevices();
- } else if (announce) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
- publishOwnDeviceIdIfNeeded();
+ if (changed || changeAccessMode.get()) {
+ if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
+ mXmppConnectionService.publishDisplayName(account);
+ publishDeviceVerificationAndBundle(
+ signedPreKeyRecord, preKeyRecords, announce, wipe);
+ } else {
+ publishDeviceBundle(
+ signedPreKeyRecord, preKeyRecords, announce, wipe);
+ }
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Bundle "
+ + getOwnDeviceId()
+ + " in PEP was current");
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announce) {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Announcing device "
+ + getOwnDeviceId());
+ publishOwnDeviceIdIfNeeded();
+ }
+ }
+ } catch (InvalidKeyException e) {
+ Log.e(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Failed to publish bundle "
+ + getOwnDeviceId()
+ + ", reason: "
+ + e.getMessage());
}
- }
- } catch (InvalidKeyException e) {
- Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
- }
- });
+ });
}
- private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
- Set<PreKeyRecord> preKeyRecords,
- final boolean announceAfter,
- final boolean wipe) {
+ private void publishDeviceBundle(
+ SignedPreKeyRecord signedPreKeyRecord,
+ Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe) {
publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true);
}
- private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord,
- final Set<PreKeyRecord> preKeyRecords,
- final boolean announceAfter,
- final boolean wipe,
- final boolean firstAttempt) {
- final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
- 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, response -> {
- final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response);
- if (firstAttempt && preconditionNotMet) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration");
- final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
- mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
- @Override
- public void onPushSucceeded() {
- publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
- }
-
- @Override
- public void onPushFailed() {
- publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
+ private void publishDeviceBundle(
+ final SignedPreKeyRecord signedPreKeyRecord,
+ final Set<PreKeyRecord> preKeyRecords,
+ final boolean announceAfter,
+ final boolean wipe,
+ final boolean firstAttempt) {
+ final Bundle publishOptions =
+ account.getXmppConnection().getFeatures().pepPublishOptions()
+ ? PublishOptions.openAccess()
+ : null;
+ 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,
+ response -> {
+ final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response);
+ if (firstAttempt && preconditionNotMet) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": precondition wasn't met for bundle. pushing node"
+ + " configuration");
+ final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
+ mXmppConnectionService.pushNodeConfiguration(
+ account,
+ node,
+ publishOptions,
+ new XmppConnectionService.OnConfigurationPushed() {
+ @Override
+ public void onPushSucceeded() {
+ publishDeviceBundle(
+ signedPreKeyRecord,
+ preKeyRecords,
+ announceAfter,
+ wipe,
+ false);
+ }
+
+ @Override
+ public void onPushFailed() {
+ publishDeviceBundle(
+ signedPreKeyRecord,
+ preKeyRecords,
+ announceAfter,
+ wipe,
+ false);
+ }
+ });
+ } else if (response.getType() == Iq.Type.RESULT) {
+ Log.d(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Successfully published bundle. ");
+ if (wipe) {
+ wipeOtherPepDevices();
+ } else if (announceAfter) {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Announcing device "
+ + getOwnDeviceId());
+ publishOwnDeviceIdIfNeeded();
+ }
+ } else if (response.getType() == Iq.Type.ERROR) {
+ if (preconditionNotMet) {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "bundle precondition still not met after second"
+ + " attempt");
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ getLogprefix(account)
+ + "Error received while publishing bundle: "
+ + response.toString());
+ }
+ pepBroken = true;
}
});
- } else if (response.getType() == Iq.Type.RESULT) {
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
- if (wipe) {
- wipeOtherPepDevices();
- } else if (announceAfter) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
- publishOwnDeviceIdIfNeeded();
- }
- } else if (response.getType() == Iq.Type.ERROR) {
- if (preconditionNotMet) {
- Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt");
- } else {
- Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString());
- }
- pepBroken = true;
- }
- });
}
public void deleteOmemoIdentity() {
@@ -36,6 +36,7 @@ import eu.siacs.conversations.crypto.sasl.HashedToken;
import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
+import eu.siacs.conversations.http.ServiceOutageStatus;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Resolver;
@@ -82,6 +83,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private static final String KEY_PGP_SIGNATURE = "pgp_signature";
private static final String KEY_PGP_ID = "pgp_id";
private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
+ public static final String KEY_SOS_URL = "sos_url";
public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
protected final JSONObject keys;
@@ -118,6 +120,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private Integer color = null;
private final HashMultimap<String, Contact> gateways = HashMultimap.create();
private Element mamPrefs = null;
+ private ServiceOutageStatus serviceOutageStatus;
public Account(final Jid jid, final String password) {
this(
@@ -853,6 +856,24 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
throw new IllegalStateException("This method should not be called");
}
+ public void setServiceOutageStatus(final ServiceOutageStatus sos) {
+ this.serviceOutageStatus = sos;
+ }
+
+ public ServiceOutageStatus getServiceOutageStatus() {
+ return this.serviceOutageStatus;
+ }
+
+ public boolean isServiceOutage() {
+ final var sos = this.serviceOutageStatus;
+ if (sos != null
+ && isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)
+ && ServiceOutageStatus.isPossibleOutage(this.status)) {
+ return sos.isNow();
+ }
+ return false;
+ }
+
public enum State {
DISABLED(false, false),
LOGGED_OUT(false, false),
@@ -914,78 +935,43 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public int getReadableId() {
- switch (this) {
- case DISABLED:
- return R.string.account_status_disabled;
- case LOGGED_OUT:
- return R.string.account_state_logged_out;
- case ONLINE:
- return R.string.account_status_online;
- case CONNECTING:
- return R.string.account_status_connecting;
- case OFFLINE:
- return R.string.account_status_offline;
- case UNAUTHORIZED:
- return R.string.account_status_unauthorized;
- case SERVER_NOT_FOUND:
- return R.string.account_status_not_found;
- case NO_INTERNET:
- return R.string.account_status_no_internet;
- case CONNECTION_TIMEOUT:
- return R.string.account_status_connection_timeout;
- case REGISTRATION_FAILED:
- return R.string.account_status_regis_fail;
- case REGISTRATION_WEB:
- return R.string.account_status_regis_web;
- case REGISTRATION_CONFLICT:
- return R.string.account_status_regis_conflict;
- case REGISTRATION_SUCCESSFUL:
- return R.string.account_status_regis_success;
- case REGISTRATION_NOT_SUPPORTED:
- return R.string.account_status_regis_not_sup;
- case REGISTRATION_INVALID_TOKEN:
- return R.string.account_status_regis_invalid_token;
- case TLS_ERROR:
- return R.string.account_status_tls_error;
- case TLS_ERROR_DOMAIN:
- return R.string.account_status_tls_error_domain;
- case INCOMPATIBLE_SERVER:
- return R.string.account_status_incompatible_server;
- case INCOMPATIBLE_CLIENT:
- return R.string.account_status_incompatible_client;
- case CHANNEL_BINDING:
- return R.string.account_status_channel_binding;
- case TOR_NOT_AVAILABLE:
- return R.string.account_status_tor_unavailable;
- case BIND_FAILURE:
- return R.string.account_status_bind_failure;
- case SESSION_FAILURE:
- return R.string.session_failure;
- case DOWNGRADE_ATTACK:
- return R.string.sasl_downgrade;
- case HOST_UNKNOWN:
- return R.string.account_status_host_unknown;
- case POLICY_VIOLATION:
- return R.string.account_status_policy_violation;
- case REGISTRATION_PLEASE_WAIT:
- return R.string.registration_please_wait;
- case REGISTRATION_PASSWORD_TOO_WEAK:
- return R.string.registration_password_too_weak;
- case STREAM_ERROR:
- return R.string.account_status_stream_error;
- case STREAM_OPENING_ERROR:
- return R.string.account_status_stream_opening_error;
- case PAYMENT_REQUIRED:
- return R.string.payment_required;
- case SEE_OTHER_HOST:
- return R.string.reconnect_on_other_host;
- case MISSING_INTERNET_PERMISSION:
- return R.string.missing_internet_permission;
- case TEMPORARY_AUTH_FAILURE:
- return R.string.account_status_temporary_auth_failure;
- default:
- return R.string.account_status_unknown;
- }
+ return switch (this) {
+ case DISABLED -> R.string.account_status_disabled;
+ case LOGGED_OUT -> R.string.account_state_logged_out;
+ case ONLINE -> R.string.account_status_online;
+ case CONNECTING -> R.string.account_status_connecting;
+ case OFFLINE -> R.string.account_status_offline;
+ case UNAUTHORIZED -> R.string.account_status_unauthorized;
+ case SERVER_NOT_FOUND -> R.string.account_status_not_found;
+ case NO_INTERNET -> R.string.account_status_no_internet;
+ case CONNECTION_TIMEOUT -> R.string.account_status_connection_timeout;
+ case REGISTRATION_FAILED -> R.string.account_status_regis_fail;
+ case REGISTRATION_WEB -> R.string.account_status_regis_web;
+ case REGISTRATION_CONFLICT -> R.string.account_status_regis_conflict;
+ case REGISTRATION_SUCCESSFUL -> R.string.account_status_regis_success;
+ case REGISTRATION_NOT_SUPPORTED -> R.string.account_status_regis_not_sup;
+ case REGISTRATION_INVALID_TOKEN -> R.string.account_status_regis_invalid_token;
+ case TLS_ERROR -> R.string.account_status_tls_error;
+ case TLS_ERROR_DOMAIN -> R.string.account_status_tls_error_domain;
+ case INCOMPATIBLE_SERVER -> R.string.account_status_incompatible_server;
+ case INCOMPATIBLE_CLIENT -> R.string.account_status_incompatible_client;
+ case CHANNEL_BINDING -> R.string.account_status_channel_binding;
+ case TOR_NOT_AVAILABLE -> R.string.account_status_tor_unavailable;
+ case BIND_FAILURE -> R.string.account_status_bind_failure;
+ case SESSION_FAILURE -> R.string.session_failure;
+ case DOWNGRADE_ATTACK -> R.string.sasl_downgrade;
+ case HOST_UNKNOWN -> R.string.account_status_host_unknown;
+ case POLICY_VIOLATION -> R.string.account_status_policy_violation;
+ case REGISTRATION_PLEASE_WAIT -> R.string.registration_please_wait;
+ case REGISTRATION_PASSWORD_TOO_WEAK -> R.string.registration_password_too_weak;
+ case STREAM_ERROR -> R.string.account_status_stream_error;
+ case STREAM_OPENING_ERROR -> R.string.account_status_stream_opening_error;
+ case PAYMENT_REQUIRED -> R.string.payment_required;
+ case SEE_OTHER_HOST -> R.string.reconnect_on_other_host;
+ case MISSING_INTERNET_PERMISSION -> R.string.missing_internet_permission;
+ case TEMPORARY_AUTH_FAILURE -> R.string.account_status_temporary_auth_failure;
+ default -> R.string.account_status_unknown;
+ };
}
}
}
@@ -10,6 +10,9 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
+import im.conversations.android.xmpp.model.pubsub.PubSub;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
@@ -37,7 +40,8 @@ public class Bookmark extends Element implements ListItem {
this.account = account;
}
- public static Map<Jid, Bookmark> parseFromStorage(Element storage, Account account) {
+ public static Map<Jid, Bookmark> parseFromStorage(
+ final Storage storage, final Account account) {
if (storage == null) {
return Collections.emptyMap();
}
@@ -58,24 +62,24 @@ public class Bookmark extends Element implements ListItem {
return bookmarks;
}
- public static Map<Jid, Bookmark> parseFromPubSub(final Element pubSub, final Account account) {
+ public static Map<Jid, Bookmark> parseFromPubSub(final PubSub pubSub, final Account account) {
if (pubSub == null) {
return Collections.emptyMap();
}
- final Element items = pubSub.findChild("items");
- if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) {
- final Map<Jid, Bookmark> bookmarks = new HashMap<>();
- for (Element item : items.getChildren()) {
- if (item.getName().equals("item")) {
- final Bookmark bookmark = Bookmark.parseFromItem(item, account);
- if (bookmark != null) {
- bookmarks.put(bookmark.jid, bookmark);
- }
- }
+ final var items = pubSub.getItems();
+ if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) {
+ return Collections.emptyMap();
+ }
+ final Map<Jid, Bookmark> bookmarks = new HashMap<>();
+ for (final var item : items.getItemMap(Conference.class).entrySet()) {
+ final Bookmark bookmark =
+ Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
+ if (bookmark == null) {
+ continue;
}
- return bookmarks;
+ bookmarks.put(bookmark.jid, bookmark);
}
- return Collections.emptyMap();
+ return bookmarks;
}
public static Bookmark parse(Element element, Account account) {
@@ -89,32 +93,32 @@ public class Bookmark extends Element implements ListItem {
return bookmark;
}
- public static Bookmark parseFromItem(Element item, Account account) {
- final Element conference = item.findChild("conference", Namespace.BOOKMARKS2);
- if (conference == null) {
- return null;
- }
- final Bookmark bookmark = new Bookmark(account);
- bookmark.jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
- // TODO verify that we only use bare jids and ignore full jids
- if (bookmark.jid == null) {
- return null;
- }
- bookmark.setBookmarkName(conference.getAttribute("name"));
- bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
- bookmark.setNick(conference.findChildContent("nick"));
- bookmark.setPassword(conference.findChildContent("password"));
- final Element extensions = conference.findChild("extensions", Namespace.BOOKMARKS2);
- if (extensions != null) {
- for (final Element ext : extensions.getChildren()) {
- if (ext.getName().equals("group") && ext.getNamespace().equals("jabber:iq:roster")) {
- bookmark.addGroup(ext.getContent());
- }
- }
- bookmark.extensions = extensions;
- }
- return bookmark;
- }
+ public static Bookmark parseFromItem(
+ final String id, final Conference conference, final Account account) {
+ if (id == null || conference == null) {
+ return null;
+ }
+ final Bookmark bookmark = new Bookmark(account);
+ bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
+ // TODO verify that we only use bare jids and ignore full jids
+ if (bookmark.jid == null) {
+ return null;
+ }
+ bookmark.setBookmarkName(conference.getAttribute("name"));
+ bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
+ bookmark.setNick(conference.findChildContent("nick"));
+ bookmark.setPassword(conference.findChildContent("password"));
+ final Element extensions = conference.findChild("extensions", Namespace.BOOKMARKS2);
+ if (extensions != null) {
+ for (final Element ext : extensions.getChildren()) {
+ if (ext.getName().equals("group") && ext.getNamespace().equals("jabber:iq:roster")) {
+ bookmark.addGroup(ext.getContent());
+ }
+ }
+ bookmark.extensions = extensions;
+ }
+ return bookmark;
+ }
public Element getExtensions() {
return extensions;
@@ -3,15 +3,12 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.util.Base64;
-
-import androidx.annotation.NonNull;
-
import com.google.common.base.Strings;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
+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 im.conversations.android.xmpp.model.stanza.Iq;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -19,341 +16,338 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-
-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 im.conversations.android.xmpp.model.stanza.Iq;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
public class ServiceDiscoveryResult {
- public static final String TABLENAME = "discovery_results";
- public static final String HASH = "hash";
- public static final String VER = "ver";
- public static final String RESULT = "result";
- protected final String hash;
- protected final byte[] ver;
- protected final List<String> features;
- protected final List<Data> forms;
- private final List<Identity> identities;
- public ServiceDiscoveryResult(final Iq packet) {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = "sha-1"; // We only support sha-1 for now
-
- final List<Element> elements = packet.query().getChildren();
-
- for (final Element element : elements) {
- if (element.getName().equals("identity")) {
- Identity id = new Identity(element);
- if (id.getType() != null && id.getCategory() != null) {
- identities.add(id);
- }
- } else if (element.getName().equals("feature")) {
- if (element.getAttribute("var") != null) {
- features.add(element.getAttribute("var"));
- }
- } else if (element.getName().equals("x") && element.getAttribute("xmlns").equals(Namespace.DATA)) {
- forms.add(Data.parse(element));
- }
- }
- this.ver = this.mkCapHash();
- }
- private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = hash;
- this.ver = ver;
-
- JSONArray identities = o.optJSONArray("identities");
- if (identities != null) {
- for (int i = 0; i < identities.length(); i++) {
- this.identities.add(new Identity(identities.getJSONObject(i)));
- }
- }
- JSONArray features = o.optJSONArray("features");
- if (features != null) {
- for (int i = 0; i < features.length(); i++) {
- this.features.add(features.getString(i));
- }
- }
- JSONArray forms = o.optJSONArray("forms");
- if (forms != null) {
- for (int i = 0; i < forms.length(); i++) {
- this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
- }
- }
- }
-
- private ServiceDiscoveryResult() {
- this.hash = "sha-1";
- this.features = Collections.emptyList();
- this.identities = Collections.emptyList();
- this.ver = null;
- this.forms = Collections.emptyList();
- }
-
- public static ServiceDiscoveryResult empty() {
- return new ServiceDiscoveryResult();
- }
-
- public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
- this(
- cursor.getString(cursor.getColumnIndexOrThrow(HASH)),
- Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT),
- new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT)))
- );
- }
-
- private static String clean(String s) {
- return s.replace("<","<");
- }
-
- private static String blankNull(String s) {
- return s == null ? "" : clean(s);
- }
-
- private static Data createFormFromJSONObject(JSONObject o) {
- Data data = new Data();
- JSONArray names = o.names();
- for (int i = 0; i < names.length(); ++i) {
- try {
- String name = names.getString(i);
- JSONArray jsonValues = o.getJSONArray(name);
- ArrayList<String> values = new ArrayList<>(jsonValues.length());
- for (int j = 0; j < jsonValues.length(); ++j) {
- values.add(jsonValues.getString(j));
- }
- data.put(name, values);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return data;
- }
-
- private static JSONObject createJSONFromForm(Data data) {
- JSONObject object = new JSONObject();
- for (Field field : data.getFields()) {
- try {
- JSONArray jsonValues = new JSONArray();
- for (String value : field.getValues()) {
- jsonValues.put(value);
- }
- object.put(field.getFieldName(), jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- try {
- JSONArray jsonValues = new JSONArray();
- jsonValues.put(data.getFormType());
- object.put(Data.FORM_TYPE, jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return object;
- }
-
- public String getVer() {
- return Base64.encodeToString(this.ver, Base64.NO_WRAP);
- }
-
- public List<Identity> getIdentities() {
- return this.identities;
- }
-
- public List<String> getFeatures() {
- return this.features;
- }
-
- public Identity getIdentity(String category, String type) {
- for (Identity id : this.getIdentities()) {
- if ((category == null || id.getCategory().equals(category)) &&
- (type == null || id.getType().equals(type))) {
- return id;
- }
- }
-
- return null;
- }
-
- public boolean hasIdentity(String category, String type) {
- return getIdentity(category, type) != null;
- }
-
- public String getExtendedDiscoInformation(String formType, String name) {
- for (Data form : this.forms) {
- if (formType.equals(form.getFormType())) {
- for (Field field : form.getFields()) {
- if (name.equals(field.getFieldName())) {
- return field.getValue();
- }
- }
- }
- }
- return null;
- }
-
- private byte[] mkCapHash() {
- StringBuilder s = new StringBuilder();
-
- List<Identity> identities = this.getIdentities();
- Collections.sort(identities);
-
- for (Identity id : identities) {
- s.append(blankNull(id.getCategory()))
- .append("/")
- .append(blankNull(id.getType()))
- .append("/")
- .append(blankNull(id.getLang()))
- .append("/")
- .append(blankNull(id.getName()))
- .append("<");
- }
-
- final List<String> features = this.getFeatures();
- Collections.sort(features);
- for (final String feature : features) {
- s.append(clean(feature)).append("<");
- }
-
- Collections.sort(forms, Comparator.comparing(Data::getFormType));
- for (final Data form : forms) {
- s.append(clean(form.getFormType())).append("<");
- final List<Field> fields = form.getFields();
- Collections.sort(
+ public static final String TABLENAME = "discovery_results";
+ public static final String HASH = "hash";
+ public static final String VER = "ver";
+ public static final String RESULT = "result";
+ protected final String hash;
+ protected final byte[] ver;
+ protected final List<String> features;
+ protected final List<Data> forms;
+ private final List<Identity> identities;
+
+ public ServiceDiscoveryResult(final Iq packet) {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = "sha-1"; // We only support sha-1 for now
+
+ final List<Element> elements = packet.query().getChildren();
+
+ for (final Element element : elements) {
+ if (element.getName().equals("identity")) {
+ Identity id = new Identity(element);
+ if (id.getType() != null && id.getCategory() != null) {
+ identities.add(id);
+ }
+ } else if (element.getName().equals("feature")) {
+ if (element.getAttribute("var") != null) {
+ features.add(element.getAttribute("var"));
+ }
+ } else if (element.getName().equals("x")
+ && element.getAttribute("xmlns").equals(Namespace.DATA)) {
+ forms.add(Data.parse(element));
+ }
+ }
+ this.ver = this.mkCapHash();
+ }
+
+ private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = hash;
+ this.ver = ver;
+
+ JSONArray identities = o.optJSONArray("identities");
+ if (identities != null) {
+ for (int i = 0; i < identities.length(); i++) {
+ this.identities.add(new Identity(identities.getJSONObject(i)));
+ }
+ }
+ JSONArray features = o.optJSONArray("features");
+ if (features != null) {
+ for (int i = 0; i < features.length(); i++) {
+ this.features.add(features.getString(i));
+ }
+ }
+ JSONArray forms = o.optJSONArray("forms");
+ if (forms != null) {
+ for (int i = 0; i < forms.length(); i++) {
+ this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
+ }
+ }
+ }
+
+ private ServiceDiscoveryResult() {
+ this.hash = "sha-1";
+ this.features = Collections.emptyList();
+ this.identities = Collections.emptyList();
+ this.ver = null;
+ this.forms = Collections.emptyList();
+ }
+
+ public static ServiceDiscoveryResult empty() {
+ return new ServiceDiscoveryResult();
+ }
+
+ public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
+ this(
+ cursor.getString(cursor.getColumnIndexOrThrow(HASH)),
+ Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT),
+ new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT))));
+ }
+
+ private static String clean(String s) {
+ return s.replace("<", "<");
+ }
+
+ private static String blankNull(String s) {
+ return s == null ? "" : clean(s);
+ }
+
+ private static Data createFormFromJSONObject(JSONObject o) {
+ Data data = new Data();
+ JSONArray names = o.names();
+ for (int i = 0; i < names.length(); ++i) {
+ try {
+ String name = names.getString(i);
+ JSONArray jsonValues = o.getJSONArray(name);
+ ArrayList<String> values = new ArrayList<>(jsonValues.length());
+ for (int j = 0; j < jsonValues.length(); ++j) {
+ values.add(jsonValues.getString(j));
+ }
+ data.put(name, values);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return data;
+ }
+
+ private static JSONObject createJSONFromForm(Data data) {
+ JSONObject object = new JSONObject();
+ for (Field field : data.getFields()) {
+ try {
+ JSONArray jsonValues = new JSONArray();
+ for (String value : field.getValues()) {
+ jsonValues.put(value);
+ }
+ object.put(field.getFieldName(), jsonValues);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ try {
+ JSONArray jsonValues = new JSONArray();
+ jsonValues.put(data.getFormType());
+ object.put(Data.FORM_TYPE, jsonValues);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return object;
+ }
+
+ public Identity getIdentity(String category, String type) {
+ for (Identity id : this.getIdentities()) {
+ if ((category == null || id.getCategory().equals(category)) &&
+ (type == null || id.getType().equals(type))) {
+ return id;
+ }
+ }
+
+ return null;
+ }
+
+ public boolean hasIdentity(String category, String type) {
+ return getIdentity(category, type) != null;
+ }
+
+ public String getVer() {
+ return Base64.encodeToString(this.ver, Base64.NO_WRAP);
+ }
+
+ public List<Identity> getIdentities() {
+ return this.identities;
+ }
+
+ public List<String> getFeatures() {
+ return this.features;
+ }
+
+ public String getExtendedDiscoInformation(final String formType, final String name) {
+ for (final Data form : this.forms) {
+ if (formType.equals(form.getFormType())) {
+ for (final Field field : form.getFields()) {
+ if (name.equals(field.getFieldName())) {
+ return field.getValue();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private byte[] mkCapHash() {
+ StringBuilder s = new StringBuilder();
+
+ List<Identity> identities = this.getIdentities();
+ Collections.sort(identities);
+
+ for (Identity id : identities) {
+ s.append(blankNull(id.getCategory()))
+ .append("/")
+ .append(blankNull(id.getType()))
+ .append("/")
+ .append(blankNull(id.getLang()))
+ .append("/")
+ .append(blankNull(id.getName()))
+ .append("<");
+ }
+
+ final List<String> features = this.getFeatures();
+ Collections.sort(features);
+ for (final String feature : features) {
+ s.append(clean(feature)).append("<");
+ }
+
+ Collections.sort(forms, Comparator.comparing(Data::getFormType));
+ for (final Data form : forms) {
+ s.append(clean(form.getFormType())).append("<");
+ final List<Field> fields = form.getFields();
+ Collections.sort(
fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName())));
- for (final Field field : fields) {
- s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
- final List<String> values = field.getValues();
- Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull));
- for (final String value : values) {
- s.append(blankNull(value)).append("<");
- }
- }
- }
-
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
+ for (final Field field : fields) {
+ s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
+ final List<String> values = field.getValues();
+ Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull));
+ for (final String value : values) {
+ s.append(blankNull(value)).append("<");
+ }
+ }
+ }
+
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
return md.digest(s.toString().getBytes(StandardCharsets.UTF_8));
}
- private JSONObject toJSON() {
- try {
- JSONObject o = new JSONObject();
-
- JSONArray ids = new JSONArray();
- for (Identity id : this.getIdentities()) {
- ids.put(id.toJSON());
- }
- o.put("identities", ids);
-
- o.put("features", new JSONArray(this.getFeatures()));
-
- JSONArray forms = new JSONArray();
- for (Data data : this.forms) {
- forms.put(createJSONFromForm(data));
- }
- o.put("forms", forms);
-
- return o;
- } catch (JSONException e) {
- return null;
- }
- }
-
- public ContentValues getContentValues() {
- final ContentValues values = new ContentValues();
- values.put(HASH, this.hash);
- values.put(VER, getVer());
- JSONObject jsonObject = toJSON();
- values.put(RESULT, jsonObject == null ? "" : jsonObject.toString());
- return values;
- }
-
- public static class Identity implements Comparable<Identity> {
- protected final String type;
- protected final String lang;
- protected final String name;
- final String category;
-
- Identity(final String category, final String type, final String lang, final String name) {
- this.category = category;
- this.type = type;
- this.lang = lang;
- this.name = name;
- }
-
- Identity(final Element el) {
- this(
- el.getAttribute("category"),
- el.getAttribute("type"),
- el.getAttribute("xml:lang"),
- el.getAttribute("name")
- );
- }
-
- Identity(final JSONObject o) {
-
- this(
- o.optString("category", null),
- o.optString("type", null),
- o.optString("lang", null),
- o.optString("name", null)
- );
- }
-
- public String getCategory() {
- return this.category;
- }
-
- public String getType() {
- return this.type;
- }
-
- public String getLang() {
- return this.lang;
- }
-
- public String getName() {
- return this.name;
- }
-
- 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()));
- }
- if (r == 0) {
- r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
- }
- if (r == 0) {
- r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
- }
-
- return r;
- }
- }
+ private JSONObject toJSON() {
+ try {
+ JSONObject o = new JSONObject();
+
+ JSONArray ids = new JSONArray();
+ for (Identity id : this.getIdentities()) {
+ ids.put(id.toJSON());
+ }
+ o.put("identities", ids);
+
+ o.put("features", new JSONArray(this.getFeatures()));
+
+ JSONArray forms = new JSONArray();
+ for (Data data : this.forms) {
+ forms.put(createJSONFromForm(data));
+ }
+ o.put("forms", forms);
+
+ return o;
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public ContentValues getContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(HASH, this.hash);
+ values.put(VER, getVer());
+ JSONObject jsonObject = toJSON();
+ values.put(RESULT, jsonObject == null ? "" : jsonObject.toString());
+ return values;
+ }
+
+ public static class Identity implements Comparable<Identity> {
+ protected final String type;
+ protected final String lang;
+ protected final String name;
+ final String category;
+
+ Identity(final String category, final String type, final String lang, final String name) {
+ this.category = category;
+ this.type = type;
+ this.lang = lang;
+ this.name = name;
+ }
+
+ Identity(final Element el) {
+ this(
+ el.getAttribute("category"),
+ el.getAttribute("type"),
+ el.getAttribute("xml:lang"),
+ el.getAttribute("name"));
+ }
+
+ Identity(final JSONObject o) {
+
+ this(
+ o.optString("category", null),
+ o.optString("type", null),
+ o.optString("lang", null),
+ o.optString("name", null));
+ }
+
+ public String getCategory() {
+ return this.category;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getLang() {
+ return this.lang;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ 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()));
+ }
+ if (r == 0) {
+ r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
+ }
+ if (r == 0) {
+ r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
+ }
+
+ return r;
+ }
+ }
}
@@ -18,11 +18,12 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import im.conversations.android.xmpp.model.correction.Replace;
+import im.conversations.android.xmpp.model.hints.Store;
import im.conversations.android.xmpp.model.reactions.Reaction;
import im.conversations.android.xmpp.model.reactions.Reactions;
+import im.conversations.android.xmpp.model.receipts.Received;
import im.conversations.android.xmpp.model.unique.OriginId;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
@@ -305,19 +306,14 @@ public class MessageGenerator extends AbstractGenerator {
}
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 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());
- for (final String namespace : namespaces) {
- receivedPacket.addChild("received", namespace).setAttribute("id", id);
- }
- receivedPacket.addChild("store", "urn:xmpp:hints");
+ receivedPacket.addExtension(new Received(id));
+ receivedPacket.addExtension(new Store());
return receivedPacket;
}
@@ -0,0 +1,162 @@
+package eu.siacs.conversations.http;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.entities.Account;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+public class ServiceOutageStatus {
+
+ private static final Collection<Account.State> SERVICE_OUTAGE_STATE =
+ Arrays.asList(
+ Account.State.CONNECTION_TIMEOUT,
+ Account.State.SERVER_NOT_FOUND,
+ Account.State.STREAM_OPENING_ERROR);
+
+ private final boolean planned;
+ private final Instant beginning;
+
+ @SerializedName("expected_end")
+ private final Instant expectedEnd;
+
+ private final Map<String, String> message;
+
+ public ServiceOutageStatus(
+ final boolean planned,
+ final Instant beginning,
+ final Instant expectedEnd,
+ final Map<String, String> message) {
+ this.planned = planned;
+ this.beginning = beginning;
+ this.expectedEnd = expectedEnd;
+ this.message = message;
+ }
+
+ public boolean isNow() {
+ final var now = Instant.now();
+ final var hasDefault = this.message != null && this.message.containsKey("default");
+ return hasDefault
+ && this.beginning != null
+ && this.expectedEnd != null
+ && this.beginning.isBefore(now)
+ && this.expectedEnd.isAfter(now);
+ }
+
+ public static ListenableFuture<ServiceOutageStatus> fetch(
+ final Context context, final HttpUrl url) {
+ final var appSettings = new AppSettings(context);
+ final var builder = HttpConnectionManager.okHttpClient(context).newBuilder();
+ if (appSettings.isUseTor()) {
+ builder.proxy(HttpConnectionManager.getProxy());
+ }
+
+ var client = builder.build();
+
+ final SettableFuture<ServiceOutageStatus> future = SettableFuture.create();
+
+ var request = new Request.Builder().url(url).build();
+
+ client.newCall(request)
+ .enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ future.setException(e);
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ try (final ResponseBody body = response.body()) {
+ if (!response.isSuccessful() || body == null) {
+ future.setException(
+ new IOException(
+ "unexpected server response ("
+ + response.code()
+ + ")"));
+ return;
+ }
+ var gson =
+ new GsonBuilder()
+ .registerTypeAdapter(
+ Instant.class,
+ new InstantDeserializer())
+ .create();
+ future.set(
+ gson.fromJson(
+ body.string(), ServiceOutageStatus.class));
+ } catch (final IOException | JsonSyntaxException e) {
+ future.setException(e);
+ }
+ }
+ });
+
+ return future;
+ }
+
+ public static boolean isPossibleOutage(final Account.State state) {
+ return SERVICE_OUTAGE_STATE.contains(state);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("planned", planned)
+ .add("beginning", beginning)
+ .add("expectedEnd", expectedEnd)
+ .add("message", message)
+ .toString();
+ }
+
+ public boolean isPlanned() {
+ return this.planned;
+ }
+
+ public long getExpectedEnd() {
+ if (this.expectedEnd == null) {
+ return 0L;
+ }
+ return this.expectedEnd.toEpochMilli();
+ }
+
+ public String getMessage() {
+ final var translated = this.message.get(Locale.getDefault().getLanguage());
+ if (Strings.isNullOrEmpty(translated)) {
+ return this.message.get("default");
+ }
+ return translated;
+ }
+
+ private static class InstantDeserializer implements JsonDeserializer<Instant> {
+ @Override
+ public Instant deserialize(
+ JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ return Instant.parse(json.getAsString());
+ }
+ }
+}
@@ -7,6 +7,7 @@ import android.util.Pair;
import com.cheogram.android.BobTransfer;
import com.cheogram.android.WebxdcUpdate;
+import androidx.annotation.NonNull;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
@@ -66,14 +67,37 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.avatar.Metadata;
+import im.conversations.android.xmpp.model.axolotl.DeviceList;
import im.conversations.android.xmpp.model.axolotl.Encrypted;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
import im.conversations.android.xmpp.model.carbons.Received;
import im.conversations.android.xmpp.model.carbons.Sent;
import im.conversations.android.xmpp.model.correction.Replace;
import im.conversations.android.xmpp.model.forward.Forwarded;
import im.conversations.android.xmpp.model.markers.Displayed;
+import im.conversations.android.xmpp.model.nick.Nick;
import im.conversations.android.xmpp.model.occupant.OccupantId;
+import im.conversations.android.xmpp.model.oob.OutOfBandData;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.pubsub.event.Delete;
+import im.conversations.android.xmpp.model.pubsub.event.Event;
+import im.conversations.android.xmpp.model.pubsub.event.Purge;
import im.conversations.android.xmpp.model.reactions.Reactions;
+import im.conversations.android.xmpp.model.receipts.Request;
+import im.conversations.android.xmpp.model.unique.StanzaId;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
public class MessageParser extends AbstractParser
implements Consumer<im.conversations.android.xmpp.model.stanza.Message> {
@@ -89,7 +113,9 @@ public class MessageParser extends AbstractParser
}
private static String extractStanzaId(
- Element packet, boolean isTypeGroupChat, Conversation conversation) {
+ final im.conversations.android.xmpp.model.stanza.Message packet,
+ final boolean isTypeGroupChat,
+ final Conversation conversation) {
final Jid by;
final boolean safeToExtract;
if (isTypeGroupChat) {
@@ -100,23 +126,14 @@ public class MessageParser extends AbstractParser
by = account.getJid().asBareJid();
safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
}
- return safeToExtract ? extractStanzaId(packet, by) : null;
+ return safeToExtract ? StanzaId.get(packet, by) : null;
}
- private static String extractStanzaId(Account account, Element packet) {
+ private static String extractStanzaId(
+ final Account account,
+ final im.conversations.android.xmpp.model.stanza.Message packet) {
final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds();
- return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null;
- }
-
- private static String extractStanzaId(Element packet, Jid by) {
- for (Element child : packet.getChildren()) {
- if (child.getName().equals("stanza-id")
- && Namespace.STANZA_IDS.equals(child.getNamespace())
- && by.equals(Jid.Invalid.getNullForInvalid(child.getAttributeAsJid("by")))) {
- return child.getAttribute("id");
- }
- }
- return null;
+ return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null;
}
private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
@@ -266,11 +283,13 @@ public class MessageParser extends AbstractParser
return null;
}
- private void parseEvent(final Element event, final Jid from, final Account account) {
- final Element items = event.findChild("items");
- final String node = items == null ? null : items.getAttribute("node");
+ private void parseEvent(final Items items, final Jid from, final Account account) {
+ final String node = items.getNode();
if ("urn:xmpp:avatar:metadata".equals(node)) {
- Avatar avatar = Avatar.parseMetadata(items);
+ // TODO support retract
+ final var entry = items.getFirstItemWithId(Metadata.class);
+ final var avatar =
+ entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
if (avatar != null) {
avatar.owner = from.asBareJid();
if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
@@ -296,24 +315,27 @@ public class MessageParser extends AbstractParser
}
}
} else if (Namespace.NICK.equals(node)) {
- final Element i = items.findChild("item");
- final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
+ final var nickItem = items.getFirstItem(Nick.class);
+ final String nick = nickItem == null ? null : nickItem.getContent();
if (nick != null) {
setNick(account, from, nick);
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
- Element item = items.findChild("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);
+ final var deviceList = items.getFirstItem(DeviceList.class);
+ if (deviceList != null) {
+ final Set<Integer> deviceIds = deviceList.getDeviceIds();
+ Log.d(
+ Config.LOGTAG,
+ AxolotlService.getLogprefix(account)
+ + "Received PEP device list "
+ + deviceIds
+ + " update from "
+ + from
+ + ", processing... ");
+ final AxolotlService axolotlService = account.getAxolotlService();
+ axolotlService.registerDevices(from, new HashSet<>(deviceIds));
+ }
+
} else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
final var connection = account.getXmppConnection();
if (connection.getFeatures().bookmarksConversion()) {
@@ -324,9 +346,7 @@ public class MessageParser extends AbstractParser
+ ": received storage:bookmark notification even though we"
+ " opted into bookmarks:1");
}
- final Element i = items.findChild("item");
- final Element storage =
- i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
+ final var storage = items.getFirstItem(Storage.class);
final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
Log.d(
@@ -340,17 +360,19 @@ public class MessageParser extends AbstractParser
+ " not detected");
}
} else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
- final Element item = items.findChild("item");
- final Element retract = items.findChild("retract");
- if (item != null) {
- final Bookmark bookmark = Bookmark.parseFromItem(item, account);
- if (bookmark != null) {
- account.putBookmark(bookmark);
- mXmppConnectionService.processModifiedBookmark(bookmark);
- mXmppConnectionService.updateConversationUi();
+ final var retractions = items.getRetractions();
+ ;
+ for (final var item : items.getItemMap(Conference.class).entrySet()) {
+ final Bookmark bookmark =
+ Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
+ if (bookmark == null) {
+ continue;
}
+ account.putBookmark(bookmark);
+ mXmppConnectionService.processModifiedBookmark(bookmark);
+ mXmppConnectionService.updateConversationUi();
}
- if (retract != null) {
+ for (final var retract : retractions) {
final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
if (id != null) {
account.removeBookmark(id);
@@ -364,35 +386,37 @@ public class MessageParser extends AbstractParser
} else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
&& Namespace.MDS_DISPLAYED.equals(node)
&& account.getJid().asBareJid().equals(from)) {
- final Element item = items.findChild("item");
- mXmppConnectionService.processMdsItem(account, item);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + " received pubsub notification for node="
- + node);
+ for (final var item :
+ items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
+ .entrySet()) {
+ mXmppConnectionService.processMdsItem(account, item);
+ }
}
}
- private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
- final Element delete = event.findChild("delete");
- final String node = delete == null ? null : delete.getAttribute("node");
+ private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
+ final String node = delete.getNode();
if (Namespace.NICK.equals(node)) {
- Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
setNick(account, from, null);
} else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
deleteAllBookmarks(account);
- } else if (Namespace.AVATAR_METADATA.equals(node)
- && account.getJid().asBareJid().equals(from)) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node");
+ } else if (Namespace.AVATAR_METADATA.equals(node)) {
+ final boolean isAccount = account.getJid().asBareJid().equals(from);
+ if (isAccount) {
+ account.setAvatar(null);
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ mXmppConnectionService.getAvatarService().clear(account);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": deleted avatar metadata node");
+ }
}
}
- private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
- final Element purge = event.findChild("purge");
- final String node = purge == null ? null : purge.getAttribute("node");
+ private void parsePurgeEvent(
+ @NonNull final Purge purge, final Jid from, final Account account) {
+ final String node = purge.getNode();
if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
deleteAllBookmarks(account);
@@ -570,7 +594,9 @@ public class MessageParser extends AbstractParser
final boolean isTypeGroupChat =
packet.getType()
== im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT;
- final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
+ final var encrypted =
+ packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class);
+ final String pgpEncrypted = encrypted == null ? null : encrypted.getContent();
Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
Set<Message.FileParams> attachments = new LinkedHashSet<>();
@@ -607,6 +633,8 @@ public class MessageParser extends AbstractParser
final var reactions = packet.getExtension(Reactions.class);
+ final var oob = packet.getExtension(OutOfBandData.class);
+ final String oobUrl = oob != null ? oob.getURL() : null;
final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class);
int status;
final Jid counterpart;
@@ -647,7 +675,8 @@ public class MessageParser extends AbstractParser
final Jid mucTrueCounterPartByPresence;
if (conversation != null) {
final var mucOptions = conversation.getMucOptions();
- occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
+ occupant =
+ mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
final var user =
occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId(), from);
mucTrueCounterPartByPresence = user == null ? null : user.getRealJid();
@@ -666,7 +695,8 @@ public class MessageParser extends AbstractParser
mXmppConnectionService.find(account, from.asBareJid());
if (conversation != null) {
final var mucOptions = conversation.getMucOptions();
- occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null;
+ occupant =
+ mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null;
} else {
occupant = null;
}
@@ -1331,9 +1361,7 @@ public class MessageParser extends AbstractParser
&& !packet.hasChild("thread")) { // We already know it has no body per above
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
- final LocalizedContent subject =
- packet.findInternationalizedChildContentInDefaultNamespace(
- "subject");
+ final LocalizedContent subject = packet.getSubject();
if (subject != null
&& conversation.getMucOptions().setSubject(subject.content)) {
mXmppConnectionService.updateConversation(conversation);
@@ -1575,15 +1603,22 @@ public class MessageParser extends AbstractParser
packet);
}
- final Element event =
- original.findChild("event", "http://jabber.org/protocol/pubsub#event");
+ final var event = original.getExtension(Event.class);
if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
- if (event.hasChild("items")) {
- parseEvent(event, original.getFrom(), account);
- } else if (event.hasChild("delete")) {
- parseDeleteEvent(event, original.getFrom(), account);
- } else if (event.hasChild("purge")) {
- parsePurgeEvent(event, original.getFrom(), account);
+ final var action = event.getAction();
+ final var node = action == null ? null : action.getNode();
+ if (node == null) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": no node found in PubSub event from "
+ + original.getFrom());
+ } else if (action instanceof Items items) {
+ parseEvent(items, original.getFrom(), account);
+ } else if (action instanceof Purge purge) {
+ parsePurgeEvent(purge, original.getFrom(), account);
+ } else if (action instanceof Delete delete) {
+ parseDeleteEvent(delete, from, account);
}
}
@@ -1884,27 +1919,14 @@ public class MessageParser extends AbstractParser
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");
+ final MessageArchiveService.Query query) {
+ final var request = packet.hasExtension(Request.class);
if (query == null) {
- final ArrayList<String> receiptsNamespaces = new ArrayList<>();
- if (markable) {
- receiptsNamespaces.add("urn:xmpp:chat-markers:0");
- }
if (request) {
- receiptsNamespaces.add("urn:xmpp:receipts");
- }
- if (receiptsNamespaces.size() > 0) {
final var receipt =
mXmppConnectionService
.getMessageGenerator()
- .received(
- account,
- packet.getFrom(),
- remoteMsgId,
- receiptsNamespaces,
- packet.getType());
+ .received(packet.getFrom(), remoteMsgId, packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
} else if (query.isCatchup()) {
@@ -83,8 +83,8 @@ public class PresenceParser extends AbstractParser
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- MucOptions.User user = parseItem(conversation, item, from, occupantIdEl, nick == null ? null : nick.getContent(), hats);
- final var occupant = packet.getExtension(OccupantId.class);
+ final MucOptions.User user = parseItem(conversation, item, from, occupantIdEl, nick == null ? null : nick.getContent(), hats);
+ final var occupant = packet.getOnlyExtension(OccupantId.class);
final String occupantId =
mucOptions.occupantId() && occupant != null
? occupant.getId()
@@ -2,20 +2,17 @@ package eu.siacs.conversations.services;
import android.content.Context;
import android.net.ConnectivityManager;
+import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
+
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
-
+import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
-
-import org.bouncycastle.crypto.engines.AESEngine;
-import org.bouncycastle.crypto.io.CipherInputStream;
-import org.bouncycastle.crypto.io.CipherOutputStream;
-import org.bouncycastle.crypto.modes.AEADBlockCipher;
-import org.bouncycastle.crypto.modes.GCMBlockCipher;
-import org.bouncycastle.crypto.params.AEADParameters;
-import org.bouncycastle.crypto.params.KeyParameter;
-
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.utils.Compatibility;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -23,20 +20,18 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicLong;
-
-import javax.annotation.Nullable;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.utils.Compatibility;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
-
-import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
public class AbstractConnectionManager {
@@ -51,18 +46,19 @@ public class AbstractConnectionManager {
public static InputStream upgrade(DownloadableFile file, InputStream is) {
if (file.getKey() != null && file.getIv() != null) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
- cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ cipher.init(
+ true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
return new CipherInputStream(is, cipher);
} else {
return is;
}
}
+ // For progress tracking see:
+ // https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
- //For progress tracking see:
- //https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
-
- public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
+ public static RequestBody requestBody(
+ final DownloadableFile file, final ProgressListener progressListener) {
return new RequestBody() {
@Override
@@ -95,7 +91,8 @@ public class AbstractConnectionManager {
void onProgress(long progress);
}
- public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
+ public static OutputStream createOutputStream(
+ DownloadableFile file, boolean append, boolean decrypt) {
FileOutputStream os;
try {
os = new FileOutputStream(file, append);
@@ -108,7 +105,8 @@ public class AbstractConnectionManager {
}
try {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
- cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ cipher.init(
+ false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
return new CipherOutputStream(os, cipher);
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
@@ -136,7 +134,9 @@ public class AbstractConnectionManager {
public void updateConversationUi(boolean force) {
synchronized (LAST_UI_UPDATE_CALL) {
- if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) {
+ if (force
+ || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get()
+ >= UI_REFRESH_THRESHOLD) {
LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime());
mXmppConnectionService.updateConversationUi();
}
@@ -144,7 +144,8 @@ public class AbstractConnectionManager {
}
public PowerManager.WakeLock createWakeLock(final String name) {
- final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
+ final PowerManager powerManager =
+ ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
}
@@ -166,7 +167,7 @@ public class AbstractConnectionManager {
}
public static Extension of(String path) {
- //TODO accept List<String> pathSegments
+ // TODO accept List<String> pathSegments
final int pos = path.lastIndexOf('/');
final String filename = path.substring(pos + 1).toLowerCase();
final String[] parts = filename.split("\\.");
@@ -115,6 +115,9 @@ import java.util.function.Consumer;
import io.ipfs.cid.Cid;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -145,6 +148,7 @@ import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.http.ServiceOutageStatus;
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.DatabaseBackend;
@@ -202,7 +206,12 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
+import im.conversations.android.xmpp.model.avatar.Metadata;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.mds.Displayed;
+import im.conversations.android.xmpp.model.pubsub.PubSub;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.storage.PrivateStorage;
import java.io.File;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -230,6 +239,7 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import me.leolin.shortcutbadger.ShortcutBadger;
+import okhttp3.HttpUrl;
import org.conscrypt.Conscrypt;
import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
import org.openintents.openpgp.IOpenPgpService2;
@@ -431,6 +441,10 @@ public class XmppConnectionService extends Service {
@Override
public void onStatusChanged(final Account account) {
+ final var status = account.getStatus();
+ if (ServiceOutageStatus.isPossibleOutage(status)) {
+ fetchServiceOutageStatus(account);
+ }
XmppConnection connection = account.getXmppConnection();
updateAccountUi();
@@ -562,6 +576,7 @@ public class XmppConnectionService extends Service {
getNotificationService().updateErrorNotification();
}
};
+
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
@@ -1397,6 +1412,34 @@ public class XmppConnectionService extends Service {
}
}
+ private void fetchServiceOutageStatus(final Account account) {
+ final var sosUrl = account.getKey(Account.KEY_SOS_URL);
+ if (Strings.isNullOrEmpty(sosUrl)) {
+ return;
+ }
+ final var url = HttpUrl.parse(sosUrl);
+ if (url == null) {
+ return;
+ }
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching service outage " + url);
+ Futures.addCallback(
+ ServiceOutageStatus.fetch(getApplicationContext(), url),
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final ServiceOutageStatus sos) {
+ Log.d(Config.LOGTAG, "fetched " + sos);
+ account.setServiceOutageStatus(sos);
+ updateAccountUi();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "error fetching sos", throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
public boolean processUnifiedPushMessage(
final Account account, final Jid transport, final Element push) {
return unifiedPushBroker.processPushMessage(account, transport, push);
@@ -2584,14 +2627,17 @@ public class XmppConnectionService extends Service {
public void fetchBookmarks(final Account account) {
final Iq iqPacket = new Iq(Iq.Type.GET);
- final Element query = iqPacket.query("jabber:iq:private");
- query.addChild("storage", Namespace.BOOKMARKS);
+ iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
final Consumer<Iq> callback =
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
- final Element query1 = response.query();
- final Element storage = query1.findChild("storage", "storage:bookmarks");
- Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
+ final var privateStorage = response.getExtension(PrivateStorage.class);
+ if (privateStorage == null) {
+ return;
+ }
+ final var bookmarkStorage = privateStorage.getExtension(Storage.class);
+ Map<Jid, Bookmark> bookmarks =
+ Bookmark.parseFromStorage(bookmarkStorage, account);
processBookmarksInitial(account, bookmarks, false);
} else {
Log.d(
@@ -2609,7 +2655,7 @@ public class XmppConnectionService extends Service {
retrieve,
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
- final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
+ final var pubsub = response.getExtension(PubSub.class);
final Map<Jid, Bookmark> bookmarks =
Bookmark.parseFromPubSub(pubsub, account);
processBookmarksInitial(account, bookmarks, true);
@@ -2627,30 +2673,34 @@ public class XmppConnectionService extends Service {
if (response.getType() != Iq.Type.RESULT) {
return;
}
- final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
- final Element items = pubSub == null ? null : pubSub.findChild("items");
- if (items == null
- || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) {
+ final var pubsub = response.getExtension(PubSub.class);
+ if (pubsub == null) {
return;
}
- for (final Element child : items.getChildren()) {
- if ("item".equals(child.getName())) {
- processMdsItem(account, child);
+ final var items = pubsub.getItems();
+ if (items == null) {
+ return;
+ }
+ if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
+ for (final var item :
+ items.getItemMap(
+ im.conversations.android.xmpp.model.mds.Displayed
+ .class)
+ .entrySet()) {
+ processMdsItem(account, item);
}
}
});
}
- public void processMdsItem(final Account account, final Element item) {
- final Jid jid =
- item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
+ public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
+ final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
if (jid == null) {
return;
}
- final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED);
- final Element stanzaId =
- displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS);
- final String id = stanzaId == null ? null : stanzaId.getAttribute("id");
+ final var displayed = item.getValue();
+ final var stanzaId = displayed.getStanzaId();
+ final String id = stanzaId == null ? null : stanzaId.getId();
final Conversation conversation = find(account, jid);
if (id != null && conversation != null) {
conversation.setDisplayState(id);
@@ -5556,16 +5606,20 @@ public class XmppConnectionService extends Service {
packet,
new Consumer<Iq>() {
- private Avatar parseAvatar(Iq packet) {
- Element pubsub =
- packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
- if (pubsub != null) {
- Element items = pubsub.findChild("items");
- if (items != null) {
- return Avatar.parseMetadata(items);
- }
+ private Avatar parseAvatar(final Iq packet) {
+ final var pubsub = packet.getExtension(PubSub.class);
+ if (pubsub == null) {
+ return null;
+ }
+ final var items = pubsub.getItems();
+ if (items == null) {
+ return null;
+ }
+ final var item = items.getFirstItemWithId(Metadata.class);
+ if (item == null) {
+ return null;
}
- return null;
+ return Avatar.parseMetadata(item.getKey(), item.getValue());
}
private boolean errorIsItemNotFound(Iq packet) {
@@ -5805,30 +5859,39 @@ public class XmppConnectionService extends Service {
account,
packet,
response -> {
- if (response.getType() == Iq.Type.RESULT) {
- Element pubsub =
- response.findChild("pubsub", "http://jabber.org/protocol/pubsub");
- if (pubsub != null) {
- Element items = pubsub.findChild("items");
- if (items != null) {
- Avatar avatar = Avatar.parseMetadata(items);
- if (avatar != null) {
- avatar.owner = account.getJid().asBareJid();
- if (fileBackend.isAvatarCached(avatar)) {
- if (account.setAvatar(avatar.getFilename())) {
- databaseBackend.updateAccount(account);
- }
- getAvatarService().clear(account);
- callback.success(avatar);
- } else {
- fetchAvatarPep(account, avatar, callback);
- }
- return;
- }
- }
+ if (response.getType() != Iq.Type.RESULT) {
+ callback.error(0, null);
+ }
+ final var pubsub = packet.getExtension(PubSub.class);
+ if (pubsub == null) {
+ callback.error(0, null);
+ return;
+ }
+ final var items = pubsub.getItems();
+ if (items == null) {
+ callback.error(0, null);
+ return;
+ }
+ final var item = items.getFirstItemWithId(Metadata.class);
+ if (item == null) {
+ callback.error(0, null);
+ return;
+ }
+ final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
+ if (avatar == null) {
+ callback.error(0, null);
+ return;
+ }
+ avatar.owner = account.getJid().asBareJid();
+ if (fileBackend.isAvatarCached(avatar)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
}
+ getAvatarService().clear(account);
+ callback.success(avatar);
+ } else {
+ fetchAvatarPep(account, avatar, callback);
}
- callback.error(0, null);
});
}
@@ -3,20 +3,27 @@ package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.XmppActivity.configureActionBar;
import android.os.Bundle;
-
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
import androidx.databinding.DataBindingUtil;
-
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityAboutBinding;
+import eu.siacs.conversations.ui.text.FixedURLSpan;
public class AboutActivity extends ActionBarActivity {
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- final ActivityAboutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_about);
+ final ActivityAboutBinding binding =
+ DataBindingUtil.setContentView(this, R.layout.activity_about);
+ final var text = new SpannableString(getString(R.string.pref_about_message));
+ Linkify.addLinks(text);
+ FixedURLSpan.fix(text);
+ binding.about.setText(text);
+ binding.about.setMovementMethod(LinkMovementMethod.getInstance());
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
setSupportActionBar(binding.toolbar);
@@ -14,7 +14,7 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
-import android.text.SpannableStringBuilder;
+import android.text.SpannableString;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
@@ -678,7 +678,7 @@ public class ConferenceDetailsActivity extends XmppActivity
this.binding.mucTitle.setVisibility(View.GONE);
}
if (printableValue(subject)) {
- SpannableStringBuilder spannable = new SpannableStringBuilder(subject);
+ final var spannable = new SpannableString(subject);
StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
Linkify.addLinks(spannable);
FixedURLSpan.fix(spannable);
@@ -689,7 +689,6 @@ public class ConferenceDetailsActivity extends XmppActivity
.TextAppearance_Material3_BodyMedium
: com.google.android.material.R.style
.TextAppearance_Material3_BodyLarge);
- this.binding.mucSubject.setAutoLinkMask(0);
this.binding.mucSubject.setVisibility(View.VISIBLE);
this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance());
} else {
@@ -2,18 +2,15 @@ package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
-
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import eu.siacs.conversations.ui.util.SettingsUtils;
-
public class ConversationActivity extends AppCompatActivity {
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- startActivity(new Intent(this, ConversationsActivity.class));
- finish();
- }
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ startActivity(new Intent(this, ConversationsActivity.class));
+ finish();
+ }
}
@@ -19,8 +19,11 @@ import android.provider.Settings;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.text.Editable;
+import android.text.SpannableString;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.format.DateUtils;
+import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -55,6 +58,7 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import de.gultsch.common.Linkify;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -74,6 +78,7 @@ import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
import eu.siacs.conversations.ui.TimePreference;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter;
+import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.PendingItem;
@@ -529,7 +534,7 @@ public class EditAccountActivity extends OmemoActivity
final List<Account> accounts =
xmppConnectionService == null ? null : xmppConnectionService.getAccounts();
- if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+ if (accounts != null && accounts.isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) {
Intent intent =
SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister);
StartConversationActivity.addInviteUri(intent, getIntent());
@@ -967,9 +972,9 @@ public class EditAccountActivity extends OmemoActivity
}
@Override
- public void onNewIntent(final Intent intent) {
+ public void onNewIntent(@NonNull final Intent intent) {
super.onNewIntent(intent);
- if (intent != null && intent.getData() != null) {
+ if (intent.getData() != null) {
final XmppUri uri = new XmppUri(intent.getData());
if (xmppConnectionServiceBound) {
processFingerprintVerification(uri, false);
@@ -1540,6 +1545,7 @@ public class EditAccountActivity extends OmemoActivity
this.binding.verificationMessage.setText("Not DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield_question);
}
+ this.binding.serviceOutage.setVisibility(View.GONE);
} else {
final TextInputLayout errorLayout;
final var status = this.mAccount.getStatus();
@@ -1569,6 +1575,44 @@ public class EditAccountActivity extends OmemoActivity
this.binding.stats.setVisibility(View.GONE);
this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
this.binding.verificationBox.setVisibility(View.GONE);
+ final var sos = mAccount.getServiceOutageStatus();
+ if (mAccount.isServiceOutage() && sos != null) {
+ this.binding.serviceOutage.setVisibility(View.VISIBLE);
+ if (sos.isPlanned()) {
+ this.binding.sosTitle.setText(R.string.account_status_service_outage_scheduled);
+ } else {
+ this.binding.sosTitle.setText(R.string.account_status_service_outage_known);
+ }
+ final var sosMessage = sos.getMessage();
+ if (Strings.isNullOrEmpty(sosMessage)) {
+ this.binding.sosMessage.setVisibility(View.GONE);
+ } else {
+ final var sosMessageSpannable = new SpannableString(sosMessage);
+ Linkify.addLinks(sosMessageSpannable);
+ FixedURLSpan.fix(sosMessageSpannable);
+ this.binding.sosMessage.setText(sosMessageSpannable);
+ this.binding.sosMessage.setVisibility(View.VISIBLE);
+ this.binding.sosMessage.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ final var expectedEnd = sos.getExpectedEnd();
+ if (expectedEnd <= 0) {
+ this.binding.sosScheduledEnd.setVisibility(View.GONE);
+ } else {
+ this.binding.sosScheduledEnd.setVisibility(View.VISIBLE);
+ this.binding.sosScheduledEnd.setText(
+ getString(
+ R.string.sos_scheduled_return,
+ DateUtils.formatDateTime(
+ this,
+ expectedEnd,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_NUMERIC_DATE
+ | DateUtils.FORMAT_SHOW_YEAR
+ | DateUtils.FORMAT_SHOW_DATE)));
+ }
+ } else {
+ this.binding.serviceOutage.setVisibility(View.GONE);
+ }
}
}
@@ -52,11 +52,25 @@ public class AccountAdapter extends ArrayAdapter<Account> {
} else {
viewHolder = (ViewHolder) view.getTag();
}
+ if (account == null) {
+ return view;
+ }
viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toString());
AvatarWorkerTask.loadAvatar(account, viewHolder.binding.accountImage, R.dimen.avatar);
- viewHolder.binding.accountStatus.setText(
- getContext().getString(account.getStatus().getReadableId()));
- switch (account.getStatus()) {
+ final var status = account.getStatus();
+ if (account.isServiceOutage()) {
+ final var sos = account.getServiceOutageStatus();
+ if (sos != null && sos.isPlanned()) {
+ viewHolder.binding.accountStatus.setText(
+ R.string.account_status_service_outage_scheduled);
+ } else {
+ viewHolder.binding.accountStatus.setText(
+ R.string.account_status_service_outage_known);
+ }
+ } else {
+ viewHolder.binding.accountStatus.setText(status.getReadableId());
+ }
+ switch (status) {
case ONLINE:
viewHolder.binding.accountStatus.setTextColor(
MaterialColors.getColor(
@@ -34,7 +34,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.text.Editable;
+import android.text.Spannable;
import android.text.Spanned;
import android.text.style.URLSpan;
import android.util.Log;
@@ -60,6 +60,19 @@ public class FixedURLSpan extends URLSpan {
protected final Account account;
+ public static void fix(final Spannable spannable) {
+ for (final URLSpan urlspan : spannable.getSpans(0, spannable.length() - 1, URLSpan.class)) {
+ final int start = spannable.getSpanStart(urlspan);
+ final int end = spannable.getSpanEnd(urlspan);
+ spannable.removeSpan(urlspan);
+ spannable.setSpan(
+ new FixedURLSpan(urlspan.getURL()),
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
public FixedURLSpan(final String url) {
this(url, null);
}
@@ -69,19 +82,6 @@ public class FixedURLSpan extends URLSpan {
this.account = account;
}
- public static void fix(final Editable editable) {
- for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
- final int start = editable.getSpanStart(urlspan);
- final int end = editable.getSpanEnd(urlspan);
- editable.removeSpan(urlspan);
- editable.setSpan(
- new FixedURLSpan(urlspan.getURL()),
- start,
- end,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- }
-
@Override
public void onClick(View widget) {
final Uri uri = Uri.parse(getURL());
@@ -83,7 +83,7 @@ public class StylingHelper {
}
}
- public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) {
+ public static void format(final Spannable editable, int start, int end, @ColorInt int textColor, final boolean composing) {
for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) {
final int keywordLength = style.getKeyword().length();
int keywordLengthStart = keywordLength;
@@ -109,13 +109,12 @@ public class StylingHelper {
}
}
- public static void format(final Editable editable, @ColorInt int textColor) {
+ public static void format(final Spannable editable, @ColorInt int textColor) {
format(editable, textColor, false);
}
- public static void format(final Editable editable, @ColorInt int textColor, final boolean composing) {
- int end = 0;
- format(editable, end, editable.length() - 1, textColor, composing);
+ public static void format(final Spannable editable, @ColorInt int textColor, final boolean composing) {
+ format(editable, 0, editable.length() - 1, textColor, composing);
}
public static void highlight(
@@ -150,7 +149,7 @@ public class StylingHelper {
}
private static void highlight(
- final TextView view, final Editable editable, final String needle) {
+ final TextView view, final Spannable editable, final String needle) {
final int length = needle.length();
String string = editable.toString();
int start = indexOfIgnoreCase(string, needle, 0);
@@ -225,7 +224,7 @@ public class StylingHelper {
};
}
- private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) {
+ private static void makeKeywordOpaque(final Spannable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) {
QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class);
@ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor;
@ColorInt int keywordColor = transformColor(textColor);
@@ -101,10 +101,6 @@ public class Element implements Node {
return element == null ? null : element.getContent();
}
- public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) {
- return LocalizedContent.get(this, name);
- }
-
public Element findChild(String name, String xmlns) {
for (Element child : getChildren()) {
if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
@@ -1,8 +1,6 @@
package eu.siacs.conversations.xml;
import com.google.common.collect.Iterables;
-
-import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -14,29 +12,13 @@ public class LocalizedContent {
public final String language;
public final int count;
- public LocalizedContent(String content, String language, int count) {
+ public LocalizedContent(final String content, final String language, final int count) {
this.content = content;
this.language = language;
this.count = count;
}
- public static LocalizedContent get(final Element element, String name) {
- final HashMap<String, String> contents = new HashMap<>();
- final String parentLanguage = element.getAttribute("xml:lang");
- for(Element child : element.getChildren()) {
- if (name.equals(child.getName())) {
- final String namespace = child.getNamespace();
- final String childLanguage = child.getAttribute("xml:lang");
- final String lang = childLanguage == null ? parentLanguage : childLanguage;
- final String content = child.getContent();
- if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) {
- if (contents.put(lang, content == null ? "" : content) != null) {
- //anything that has multiple contents for the same language is invalid
- return null;
- }
- }
- }
- }
+ public static LocalizedContent get(final Map<String, String> contents) {
if (contents.isEmpty()) {
return null;
}
@@ -45,10 +27,6 @@ public class LocalizedContent {
if (localized != null) {
return new LocalizedContent(localized, userLanguage, contents.size());
}
- final String defaultLanguageContent = contents.get(null);
- if (defaultLanguageContent != null) {
- return new LocalizedContent(defaultLanguageContent, STREAM_LANGUAGE, contents.size());
- }
final String streamLanguageContent = contents.get(STREAM_LANGUAGE);
if (streamLanguageContent != null) {
return new LocalizedContent(streamLanguageContent, STREAM_LANGUAGE, contents.size());
@@ -113,4 +113,6 @@ public final class Namespace {
public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
+ public static final String PRIVATE_XML_STORAGE = "jabber:iq:private";
+ public static final String SERVICE_OUTAGE_STATUS = "urn:xmpp:sos:0";
}
@@ -2,61 +2,56 @@ 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;
-
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
public class XmlReader implements Closeable {
- private final XmlPullParser parser;
- private InputStream is;
+ private final XmlPullParser parser;
+ private InputStream is;
- public XmlReader() {
- this.parser = Xml.newPullParser();
- try {
- this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
- } catch (XmlPullParserException e) {
- Log.d(Config.LOGTAG, "error setting namespace feature on parser");
- }
- }
+ public XmlReader() {
+ this.parser = Xml.newPullParser();
+ try {
+ this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ } catch (XmlPullParserException e) {
+ Log.d(Config.LOGTAG, "error setting namespace feature on parser");
+ }
+ }
- public void setInputStream(InputStream inputStream) throws IOException {
- if (inputStream == null) {
- throw new IOException();
- }
- this.is = inputStream;
- try {
- parser.setInput(new InputStreamReader(this.is));
- } catch (XmlPullParserException e) {
- throw new IOException("error resetting parser");
- }
- }
+ public void setInputStream(InputStream inputStream) throws IOException {
+ if (inputStream == null) {
+ throw new IOException();
+ }
+ this.is = inputStream;
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
- public void reset() throws IOException {
- if (this.is == null) {
- throw new IOException();
- }
- try {
- parser.setInput(new InputStreamReader(this.is));
- } catch (XmlPullParserException e) {
- throw new IOException("error resetting parser");
- }
- }
+ public void reset() throws IOException {
+ if (this.is == null) {
+ throw new IOException();
+ }
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ throw new IOException("error resetting parser");
+ }
+ }
- @Override
- public void close() {
- this.is = null;
- }
+ @Override
+ public void close() {
+ this.is = null;
+ }
public Tag readTag() throws IOException {
try {
@@ -88,21 +83,27 @@ public class XmlReader implements Closeable {
}
}
- } catch (Throwable throwable) {
- throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable);
- }
- return null;
- }
+ } catch (Throwable throwable) {
+ throw new IOException(
+ "xml parser mishandled "
+ + throwable.getClass().getSimpleName()
+ + "("
+ + throwable.getMessage()
+ + ")",
+ throwable);
+ }
+ return null;
+ }
- 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 <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();
@@ -3194,6 +3194,20 @@ public class XmppConnection implements Runnable {
this.blockListRequested = value;
}
+ public HttpUrl getServiceOutageStatus() {
+ final var disco = connection.disco.get(account.getDomain());
+ if (disco == null) {
+ return null;
+ }
+ final var address =
+ disco.getExtendedDiscoInformation(
+ Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses");
+ if (Strings.isNullOrEmpty(address)) {
+ return null;
+ }
+ return HttpUrl.parse(address);
+ }
+
public boolean httpUpload(long filesize) {
if (Config.DISABLE_HTTP_UPLOAD) {
return false;
@@ -1,16 +1,13 @@
package eu.siacs.conversations.xmpp.jingle;
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.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -20,10 +17,8 @@ import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
-
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
-
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -187,8 +182,7 @@ public abstract class AbstractJingleConnection {
abstract void deliverPacket(Iq jinglePacket);
- protected void receiveOutOfOrderAction(
- final Iq jinglePacket, final Jingle.Action action) {
+ protected void receiveOutOfOrderAction(final Iq jinglePacket, final Jingle.Action action) {
Log.d(
Config.LOGTAG,
String.format(
@@ -198,7 +192,8 @@ public abstract class AbstractJingleConnection {
Log.d(
Config.LOGTAG,
String.format(
- "%s: got a reason to terminate with out-of-order. but already in state %s",
+ "%s: got a reason to terminate with out-of-order. but already in state"
+ + " %s",
id.account.getJid().asBareJid(), getState()));
respondWithOutOfOrder(jinglePacket);
} else {
@@ -267,10 +262,7 @@ public abstract class AbstractJingleConnection {
}
private void respondWithJingleError(
- final Iq original,
- String jingleCondition,
- String condition,
- String conditionType) {
+ final Iq original, String jingleCondition, String condition, String conditionType) {
jingleConnectionManager.respondWithJingleError(
id.account, original, jingleCondition, condition, conditionType);
}
@@ -371,15 +363,15 @@ public abstract class AbstractJingleConnection {
return new Id(account, with, sessionId);
}
- public static Id of(Account account, Jid with) {
+ public static Id of(final Account account, final Jid with) {
return new Id(account, with, JingleConnectionManager.nextRandomId());
}
- public static Id of(Message message) {
+ public static Id of(final Message message) {
return new Id(
message.getConversation().getAccount(),
message.getCounterpart(),
- JingleConnectionManager.nextRandomId());
+ message.getUuid());
}
public Contact getContact() {
@@ -430,8 +422,8 @@ public abstract class AbstractJingleConnection {
case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
- case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
- .TERMINATED_APPLICATION_FAILURE;
+ case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS ->
+ State.TERMINATED_APPLICATION_FAILURE;
default -> State.TERMINATED_CONNECTIVITY_ERROR;
};
}
@@ -101,6 +101,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
this.xmppConnectionService.findOrCreateConversation(
id.account, id.with.asBareJid(), false, false);
this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setRemoteMsgId(id.sessionId);
this.message.setStatus(Message.STATUS_RECEIVED);
this.message.setErrorMessage(null);
this.message.setTransferable(this);
@@ -1,18 +1,17 @@
package eu.siacs.conversations.xmpp.jingle;
+import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableSet;
-
import java.util.Locale;
import java.util.Set;
-import javax.annotation.Nonnull;
-
public enum Media {
-
- VIDEO, AUDIO, UNKNOWN;
+ VIDEO,
+ AUDIO,
+ UNKNOWN;
@Override
- @Nonnull
+ @NonNull
public String toString() {
return super.toString().toLowerCase(Locale.ROOT);
}
@@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle;
+import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
@@ -26,7 +27,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import javax.annotation.Nonnull;
public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
@@ -492,7 +492,7 @@ public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTran
}
@Override
- @Nonnull
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("added", added)
@@ -1,23 +1,18 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
-
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
-
import eu.siacs.conversations.Config;
-
+import java.util.UUID;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.RtpSender;
import org.webrtc.RtpTransceiver;
-import java.util.UUID;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
class TrackWrapper<T extends MediaStreamTrack> {
public final T track;
public final RtpSender rtpSender;
@@ -63,7 +58,7 @@ class TrackWrapper<T extends MediaStreamTrack> {
}
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
- @Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
+ @NonNull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
final RtpSender rtpSender = trackWrapper.rtpSender;
final String rtpSenderId;
try {
@@ -2,12 +2,15 @@ package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import android.util.Log;
-
+import androidx.annotation.Nullable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
-
+import eu.siacs.conversations.Config;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerationAndroid;
import org.webrtc.CameraEnumerator;
@@ -17,14 +20,6 @@ import org.webrtc.PeerConnectionFactory;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoSource;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Set;
-
-import javax.annotation.Nullable;
-
-import eu.siacs.conversations.Config;
-
class VideoSourceWrapper {
private static final int CAPTURING_RESOLUTION = 1920;
@@ -151,7 +146,7 @@ class VideoSourceWrapper {
return videoSourceWrapper;
}
}
- if (deviceNames.size() == 0) {
+ if (deviceNames.isEmpty()) {
return null;
} else {
return of(enumerator, Iterables.get(deviceNames, 0), deviceNames);
@@ -25,8 +25,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.CandidatePairChangeEvent;
@@ -60,8 +58,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
public class WebRTCWrapper {
@@ -706,7 +704,7 @@ public class WebRTCWrapper {
MoreExecutors.directExecutor());
}
- @Nonnull
+ @NonNull
private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
@@ -716,7 +714,7 @@ public class WebRTCWrapper {
}
}
- @Nonnull
+ @NonNull
private PeerConnection requirePeerConnection() {
final PeerConnection peerConnection = this.peerConnection;
if (peerConnection == null) {
@@ -744,7 +742,7 @@ public class WebRTCWrapper {
return true;
}
- @Nonnull
+ @NonNull
private PeerConnectionFactory requirePeerConnectionFactory() {
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
if (peerConnectionFactory == null) {
@@ -5,6 +5,7 @@ import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription;
import android.content.Context;
import android.util.Log;
+import androidx.annotation.NonNull;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closeables;
@@ -41,7 +42,6 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nonnull;
import org.webrtc.CandidatePairChangeEvent;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
@@ -437,7 +437,7 @@ public class WebRTCDataChannelTransport implements Transport {
localDescriptionExecutorService);
}
- @Nonnull
+ @NonNull
private PeerConnectionFactory requirePeerConnectionFactory() {
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
if (peerConnectionFactory == null) {
@@ -446,7 +446,7 @@ public class WebRTCDataChannelTransport implements Transport {
return peerConnectionFactory;
}
- @Nonnull
+ @NonNull
private PeerConnection requirePeerConnection() {
final var future = this.peerConnectionFuture;
if (future != null && future.isDone()) {
@@ -9,83 +9,77 @@ import io.ipfs.cid.Cid;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.avatar.Metadata;
public class Avatar {
- public enum Origin { PEP, VCARD }
+ public enum Origin {
+ PEP,
+ VCARD
+ }
public String type;
- public String sha1sum;
- public String image;
- public int height;
- public int width;
- public long size;
- public Jid owner;
- public Origin origin = Origin.PEP; //default to maintain compat
+ public String sha1sum;
+ public String image;
+ public int height;
+ public int width;
+ public long size;
+ public Jid owner;
+ public Origin origin = Origin.PEP; // default to maintain compat
- public byte[] getImageAsBytes() {
- return Base64.decode(image, Base64.DEFAULT);
- }
+ public byte[] getImageAsBytes() {
+ return Base64.decode(image, Base64.DEFAULT);
+ }
- public String getFilename() {
- return sha1sum;
- }
+ public String getFilename() {
+ return sha1sum;
+ }
- public static Avatar parseMetadata(Element items) {
- Element item = items.findChild("item");
- if (item == null) {
- return null;
- }
- Element metadata = item.findChild("metadata");
- if (metadata == null) {
- return null;
- }
- String primaryId = item.getAttribute("id");
- if (primaryId == null) {
- return null;
- }
- for (Element child : metadata.getChildren()) {
- if (child.getName().equals("info")
- && primaryId.equals(child.getAttribute("id"))) {
- Avatar avatar = new Avatar();
- String height = child.getAttribute("height");
- String width = child.getAttribute("width");
- String size = child.getAttribute("bytes");
- try {
- if (height != null) {
- avatar.height = Integer.parseInt(height);
- }
- if (width != null) {
- avatar.width = Integer.parseInt(width);
- }
- if (size != null) {
- avatar.size = Long.parseLong(size);
- }
- } catch (NumberFormatException e) {
- return null;
- }
- avatar.type = child.getAttribute("type");
- String hash = child.getAttribute("id");
- if (!isValidSHA1(hash)) {
- return null;
- }
- avatar.sha1sum = hash;
- avatar.origin = Origin.PEP;
- return avatar;
- }
- }
- return null;
- }
+ public static Avatar parseMetadata(final String primaryId, final Metadata metadata) {
+ if (primaryId == null || metadata == null) {
+ return null;
+ }
+ for (Element child : metadata.getChildren()) {
+ if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) {
+ Avatar avatar = new Avatar();
+ String height = child.getAttribute("height");
+ String width = child.getAttribute("width");
+ String size = child.getAttribute("bytes");
+ try {
+ if (height != null) {
+ avatar.height = Integer.parseInt(height);
+ }
+ if (width != null) {
+ avatar.width = Integer.parseInt(width);
+ }
+ if (size != null) {
+ avatar.size = Long.parseLong(size);
+ }
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ avatar.type = child.getAttribute("type");
+ String hash = child.getAttribute("id");
+ if (!isValidSHA1(hash)) {
+ return null;
+ }
+ avatar.sha1sum = hash;
+ avatar.origin = Origin.PEP;
+ return avatar;
+ }
+ }
+ return null;
+ }
- @Override
- public boolean equals(Object object) {
- if (object != null && object instanceof Avatar) {
- Avatar other = (Avatar) object;
- return other.getFilename().equals(this.getFilename());
- } else {
- return false;
- }
- }
+ @Override
+ public boolean equals(Object object) {
+ if (object != null && object instanceof Avatar) {
+ Avatar other = (Avatar) object;
+ return other.getFilename().equals(this.getFilename());
+ } else {
+ return false;
+ }
+ }
public Cid cid() {
if (sha1sum == null) return null;
@@ -111,7 +105,7 @@ public class Avatar {
return avatar;
}
- private static boolean isValidSHA1(String s) {
- return s != null && s.matches("[a-fA-F0-9]{40}");
- }
+ private static boolean isValidSHA1(String s) {
+ return s != null && s.matches("[a-fA-F0-9]{40}");
+ }
}
@@ -12,12 +12,14 @@ public class PublishOptions {
public static Bundle openAccess() {
final Bundle options = new Bundle();
options.putString("pubsub#access_model", "open");
+ options.putString("pubsub#notify_delete", "true");
return options;
}
public static Bundle presenceAccess() {
final Bundle options = new Bundle();
options.putString("pubsub#access_model", "presence");
+ options.putString("pubsub#notify_delete", "true");
return options;
}
@@ -1,32 +1,10 @@
package im.conversations.android.xmpp.model.bookmark;
-import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
-@XmlElement
public class Conference extends Extension {
public Conference() {
super(Conference.class);
}
-
- public boolean isAutoJoin() {
- return this.getAttributeAsBoolean("autojoin");
- }
-
- public String getConferenceName() {
- return this.getAttribute("name");
- }
-
- public void setAutoJoin(boolean autoJoin) {
- setAttribute("autojoin", autoJoin);
- }
-
- public Nick getNick() {
- return this.getExtension(Nick.class);
- }
-
- public Extensions getExtensions() {
- return this.getExtension(Extensions.class);
- }
}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.bookmark;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Storage extends Extension {
+
+ public Storage() {
+ super(Storage.class);
+ }
+}
@@ -1,5 +1,5 @@
-@XmlPackage(namespace = Namespace.BOOKMARKS2)
+@XmlPackage(namespace = Namespace.BOOKMARKS)
package im.conversations.android.xmpp.model.bookmark;
-import im.conversations.android.annotation.XmlPackage;
import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,32 @@
+package im.conversations.android.xmpp.model.bookmark2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Conference extends Extension {
+
+ public Conference() {
+ super(Conference.class);
+ }
+
+ public boolean isAutoJoin() {
+ return this.getAttributeAsBoolean("autojoin");
+ }
+
+ public String getConferenceName() {
+ return this.getAttribute("name");
+ }
+
+ public void setAutoJoin(boolean autoJoin) {
+ setAttribute("autojoin", autoJoin);
+ }
+
+ public Nick getNick() {
+ return this.getExtension(Nick.class);
+ }
+
+ public Extensions getExtensions() {
+ return this.getExtension(Extensions.class);
+ }
+}
@@ -1,4 +1,4 @@
-package im.conversations.android.xmpp.model.bookmark;
+package im.conversations.android.xmpp.model.bookmark2;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@@ -1,4 +1,4 @@
-package im.conversations.android.xmpp.model.bookmark;
+package im.conversations.android.xmpp.model.bookmark2;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.BOOKMARKS2)
+package im.conversations.android.xmpp.model.bookmark2;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -3,10 +3,15 @@ package im.conversations.android.xmpp.model.mds;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.unique.StanzaId;
@XmlElement(namespace = Namespace.MDS_DISPLAYED)
public class Displayed extends Extension {
public Displayed() {
super(Displayed.class);
}
+
+ public StanzaId getStanzaId() {
+ return this.getOnlyExtension(StanzaId.class);
+ }
}
@@ -45,6 +45,11 @@ public interface Items {
return Iterables.getFirst(map.values(), null);
}
+ default <T extends Extension> Map.Entry<String, T> getFirstItemWithId(final Class<T> clazz) {
+ final var entries = getItemMap(clazz).entrySet();
+ return Iterables.getFirst(entries, null);
+ }
+
default <T extends Extension> T getOnlyItem(final Class<T> clazz) {
final var map = getItemMap(clazz);
return Iterables.getOnlyElement(map.values());
@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class Action extends Extension {
+
+ public Action(Class<? extends Action> clazz) {
+ super(clazz);
+ }
+
+ public String getNode() {
+ return this.getAttribute("node");
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Delete extends Action {
+
+ public Delete() {
+ super(Delete.class);
+ }
+}
@@ -12,25 +12,17 @@ public class Event extends Extension {
super(Event.class);
}
- public Items getItems() {
- return this.getExtension(ItemsWrapper.class);
- }
-
- public Purge getPurge() {
- return this.getExtension(Purge.class);
+ public Action getAction() {
+ return this.getOnlyExtension(Action.class);
}
@XmlElement(name = "items")
- public static class ItemsWrapper extends Extension implements Items {
+ public static class ItemsWrapper extends Action implements Items {
public ItemsWrapper() {
super(ItemsWrapper.class);
}
- public String getNode() {
- return this.getAttribute("node");
- }
-
public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
return this.getExtensions(Item.class);
}
@@ -1,10 +1,9 @@
package im.conversations.android.xmpp.model.pubsub.event;
import im.conversations.android.annotation.XmlElement;
-import im.conversations.android.xmpp.model.Extension;
@XmlElement
-public class Purge extends Extension {
+public class Purge extends Action {
public Purge() {
super(Purge.class);
@@ -1,10 +1,9 @@
package im.conversations.android.xmpp.model.pubsub.event;
import im.conversations.android.annotation.XmlElement;
-import im.conversations.android.xmpp.model.Extension;
@XmlElement
-public class Retract extends Extension {
+public class Retract extends Action {
public Retract() {
super(Retract.class);
@@ -10,6 +10,11 @@ public class Received extends DeliveryReceipt {
super(Received.class);
}
+ public Received(final String id) {
+ super(Received.class);
+ this.setId(id);
+ }
+
public void setId(String id) {
this.setAttribute("id", id);
}
@@ -1,11 +1,17 @@
package im.conversations.android.xmpp.model.stanza;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
-
import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.jabber.Body;
-
+import im.conversations.android.xmpp.model.jabber.Subject;
import java.util.Locale;
@XmlElement
@@ -21,9 +27,38 @@ public class Message extends Stanza {
}
public LocalizedContent getBody() {
- return findInternationalizedChildContentInDefaultNamespace("body");
+ return getLocalizedContent(Body.class);
+ }
+
+ public LocalizedContent getSubject() {
+ return getLocalizedContent(Subject.class);
+ }
+
+ private LocalizedContent getLocalizedContent(final Class<? extends Extension> clazz) {
+ final var builder = new ImmutableMultimap.Builder<String, String>();
+ final var messageLanguage = this.getAttribute("xml:lang");
+ final var parentLanguage =
+ Strings.isNullOrEmpty(messageLanguage)
+ ? LocalizedContent.STREAM_LANGUAGE
+ : messageLanguage;
+ for (final var element : this.getExtensions(clazz)) {
+ final var elementLanguage = element.getAttribute("xml:lang");
+ final var language =
+ Strings.isNullOrEmpty(elementLanguage) ? parentLanguage : elementLanguage;
+ final var content = element.getContent();
+ if (content == null) {
+ continue;
+ }
+ builder.put(language, content);
+ }
+ final var multiMap = builder.build().asMap();
+ if (Config.TREAT_MULTI_CONTENT_AS_INVALID
+ && Iterables.any(multiMap.values(), v -> v.size() > 1)) {
+ return null;
+ }
+ return LocalizedContent.get(Maps.transformValues(multiMap, v -> Joiner.on('\n').join(v)));
}
-
+
public Type getType() {
final var value = this.getAttribute("type");
if (value == null) {
@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.storage;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "query", namespace = Namespace.PRIVATE_XML_STORAGE)
+public class PrivateStorage extends Extension {
+
+ public PrivateStorage() {
+ super(PrivateStorage.class);
+ }
+}
@@ -1,8 +1,10 @@
package im.conversations.android.xmpp.model.unique;
+import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Map;
@XmlElement
public class StanzaId extends Extension {
@@ -18,4 +20,23 @@ public class StanzaId extends Extension {
public String getId() {
return this.getAttribute("id");
}
+
+ public static String get(
+ final im.conversations.android.xmpp.model.stanza.Message packet, final Jid by) {
+ final var builder = new ImmutableMap.Builder<Jid, String>();
+ for (final var stanzaId : packet.getExtensions(StanzaId.class)) {
+ final var id = stanzaId.getId();
+ final var byAttribute = Jid.Invalid.getNullForInvalid(stanzaId.getBy());
+ if (byAttribute != null && id != null) {
+ builder.put(byAttribute, id);
+ }
+ }
+ final Map<Jid, String> byToId;
+ try {
+ byToId = builder.buildOrThrow();
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ return byToId.get(by);
+ }
}
@@ -2,13 +2,11 @@ package im.conversations.android.xmpp.processor;
import android.text.TextUtils;
import android.util.Log;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
-
import im.conversations.android.xmpp.model.stanza.Iq;
public class BindProcessor implements Runnable {
@@ -24,14 +22,21 @@ public class BindProcessor implements Runnable {
@Override
public void run() {
final XmppConnection connection = account.getXmppConnection();
+ final var features = connection.getFeatures();
service.cancelAvatarFetches(account);
final boolean loggedInSuccessfully =
account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
+ final boolean sosModified;
+ final var sos = features.getServiceOutageStatus();
+ if (sos != null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + " server has SOS on " + sos);
+ sosModified = account.setKey(Account.KEY_SOS_URL, sos.toString());
+ } else {
+ sosModified = false;
+ }
final boolean gainedFeature =
- account.setOption(
- Account.OPTION_HTTP_UPLOAD_AVAILABLE,
- connection.getFeatures().httpUpload(0));
- if (loggedInSuccessfully || gainedFeature) {
+ account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, features.httpUpload(0));
+ if (loggedInSuccessfully || gainedFeature || sosModified) {
service.databaseBackend.updateAccount(account);
}
@@ -57,18 +62,17 @@ public class BindProcessor implements Runnable {
connection.fetchRoster();
- if (connection.getFeatures().bookmarks2()) {
+ if (features.bookmarks2()) {
service.fetchBookmarks2(account);
- } else if (!connection.getFeatures().bookmarksConversion()) {
+ } else if (!features.bookmarksConversion()) {
service.fetchBookmarks(account);
}
- if (connection.getFeatures().mds()) {
+ if (features.mds()) {
service.fetchMessageDisplayedSynchronization(account);
} else {
Log.d(Config.LOGTAG, account.getJid() + ": server has no support for mds");
}
- final var features = connection.getFeatures();
final boolean bind2 = features.bind2();
final boolean flexible = features.flexibleOfflineMessageRetrieval();
final boolean catchup = service.getMessageArchiveService().inCatchup(account);
@@ -8,6 +8,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -1,4 +1,5 @@
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
@@ -25,13 +26,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
+ android:id="@+id/about"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:autoLink="web"
android:fontFamily="monospace"
android:linksClickable="true"
android:layout_margin="@dimen/card_padding_regular"
- android:text="@string/pref_about_message"
+ tools:text="@string/pref_about_message"
android:textAppearance="?textAppearanceBodyMedium"
android:typeface="monospace" />
</LinearLayout>
@@ -4,7 +4,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -6,6 +6,7 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -6,6 +6,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -9,6 +9,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -5,7 +5,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -146,6 +147,56 @@
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/service_outage"
+ style="?attr/materialCardViewElevatedStyle"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="@dimen/activity_vertical_margin"
+ android:layout_marginRight="@dimen/activity_horizontal_margin"
+ android:layout_marginBottom="@dimen/activity_vertical_margin"
+ android:visibility="gone"
+ app:cardBackgroundColor="?colorErrorContainer"
+ tools:visibility="visible">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="@dimen/card_padding_regular">
+
+
+ <TextView
+ android:id="@+id/sos_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/account_status_service_outage_known"
+ android:textAppearance="?textAppearanceTitleLarge"
+ android:textColor="?colorOnErrorContainer" />
+
+ <TextView
+ android:id="@+id/sos_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAppearance="?textAppearanceBodyMedium"
+ android:textColor="?colorOnErrorContainer"
+ android:textColorLink="?colorOnErrorContainer"
+ tools:text="Our service is currently performing server updates" />
+
+ <TextView
+ android:id="@+id/sos_scheduled_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16sp"
+ android:textAppearance="?textAppearanceBodyLarge"
+ android:textColor="?colorOnErrorContainer"
+ tools:text="@string/sos_scheduled_return" />
+ </LinearLayout>
+ </com.google.android.material.card.MaterialCardView>
+
+
<com.google.android.material.card.MaterialCardView
android:id="@+id/os_optimization"
android:layout_width="fill_parent"
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
@@ -6,6 +6,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -87,7 +88,6 @@
android:id="@+id/muc_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:autoLink="web"
android:textAppearance="?textAppearanceTitleMedium" />
<androidx.constraintlayout.widget.ConstraintLayout
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,7 +4,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -5,7 +5,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/pip_placeholder"
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -20,10 +21,10 @@
</com.google.android.material.appbar.AppBarLayout>
<ListView
- android:background="@drawable/background_search"
android:id="@+id/search_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="@drawable/background_search"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:listSelector="@android:color/transparent"
@@ -4,7 +4,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -5,6 +5,7 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
tools:context=".ui.ShareLocationActivity">
<com.google.android.material.appbar.AppBarLayout
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -3,7 +3,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -4,7 +4,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -52,9 +53,9 @@
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:contentDescription="@string/add_contact_or_create_or_join_group_chat"
- app:sdMainFabClosedSrc="@drawable/ic_add_24dp"
app:sdMainFabClosedBackgroundColor="?colorPrimaryContainer"
app:sdMainFabClosedIconColor="?colorOnPrimaryContainer"
+ app:sdMainFabClosedSrc="@drawable/ic_add_24dp"
app:sdMainFabOpenedBackgroundColor="?colorPrimaryContainer"
app:sdMainFabOpenedIconColor="?colorOnPrimaryContainer"
app:sdOverlayLayout="@id/overlay"
@@ -3,7 +3,8 @@
<RelativeLayout
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
@@ -4,7 +4,7 @@
<string name="action_accounts">Gestionar cuentas</string>
<string name="action_account">Gestionar cuenta</string>
<string name="action_contact_details">Detalles del contacto</string>
- <string name="action_muc_details">Detalles de conversación</string>
+ <string name="action_muc_details">Detalles de la conversación grupal</string>
<string name="channel_details">Detalles del canal</string>
<string name="action_add_account">Añadir cuenta</string>
<string name="action_edit_contact">Editar contacto</string>
@@ -119,7 +119,7 @@
<string name="pref_notification_grace_period">Periodo de gracia</string>
<string name="pref_notification_grace_period_summary">El tiempo que se silencian las notificaciones tras detectar actividad en otro de tus dispositivos.</string>
<string name="pref_advanced_options">Avanzado</string>
- <string name="pref_never_send_crash_summary">Al enviar los informes de los fallos, ayudará a un mayor desarrollo</string>
+ <string name="pref_never_send_crash_summary">Al enviar los informes, estás ayudando al desarrollo</string>
<string name="pref_confirm_messages">Confirmar mensajes</string>
<string name="pref_confirm_messages_summary">Permitir a sus contactos saber cuando ha recibido y leído sus mensajes</string>
<string name="pref_prevent_screenshots">Impedir capturas de pantalla</string>
@@ -413,7 +413,7 @@
<string name="pdf_document">documento PDF</string>
<string name="apk">aplicación para Android</string>
<string name="vcard">Contacto</string>
- <string name="avatar_has_been_published">Se ha publicado el avatar.</string>
+ <string name="avatar_has_been_published">Foto de perfil ha sido publicada!</string>
<string name="sending_x_file">Enviando %s</string>
<string name="offering_x_file">Ofreciendo %s</string>
<string name="hide_offline">Ocultar desconectados</string>
@@ -484,7 +484,7 @@
<string name="action_renew_certificate">Renovar certificado</string>
<string name="error_fetching_omemo_key">¡Error buscando clave OMEMO!</string>
<string name="verified_omemo_key_with_certificate">¡Clave OMEMO con certificado verificada!</string>
- <string name="device_does_not_support_certificates">Su dispositivo no admite la selección de certificados de cliente.</string>
+ <string name="device_does_not_support_certificates">Tu dispositivo no admite la selección de certificados de cliente!</string>
<string name="pref_connection_options">Conexión</string>
<string name="pref_use_tor">Conectar via Tor</string>
<string name="pref_use_tor_summary">Todas las conexiones se realizan a través de la red TOR. Requiere Orbot</string>
@@ -826,7 +826,7 @@
<string name="ebook">e-book</string>
<string name="video_original">Original (sin comprimir)</string>
<string name="open_with">Abrir con…</string>
- <string name="set_profile_picture">Avatar</string>
+ <string name="set_profile_picture">Foto de Perfil</string>
<string name="choose_account">Elige una cuenta</string>
<string name="restore_backup">Restaurar copia de respaldo</string>
<string name="restore">Restaurar</string>
@@ -1108,12 +1108,12 @@
<string name="clients_may_not_support_av">El cliente XMPP de su contacto puede que no admita llamadas de audio/vídeo.</string>
<string name="could_not_modify_call">No se pudo modificar la llamada</string>
<string name="pref_chat_bubbles">Burbujas de chat</string>
- <string name="pref_chat_bubbles_summary">Color, Tamaño de fuente, Imágenes de perfil</string>
+ <string name="pref_chat_bubbles_summary">Color de fondo, Tamaño de fuente, Imágenes de perfil</string>
<string name="pref_title_bubbles">Burbujas de Chat</string>
<string name="pref_call_integration">Integración de llamadas</string>
<string name="pref_show_avatars">Mostrar avatares</string>
<string name="pref_show_avatars_summary">Mostrar imágenes de perfil para tus mensajes y conversaciones 1:1, aparte de las conversaciones en grupo.</string>
- <string name="pref_call_integration_summary">Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como ser finalizar una llamada cuando recibimos otra.</string>
+ <string name="pref_call_integration_summary">Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como finalizar una llamada cuando recibimos otra.</string>
<string name="pref_align_start">Mensajes alineados a la izquierda</string>
<string name="pref_align_start_summary">Mostrar todos los mensajes, incluso los propios, sobre el margen izquierdo para una distribución uniforme del chat.</string>
<string name="custom_notifications">Notificaciones personalizadas</string>
@@ -1127,4 +1127,12 @@
<string name="pref_backup_location">Ubicación de la copia de seguridad</string>
<string name="restore_omemo_key">Restaurar claves OMEMO</string>
<string name="account_status_channel_binding">Canal no disponible</string>
+ <string name="uri">URI</string>
+ <string name="copy_geo_uri">Copiar geolocalización</string>
+ <string name="copy_email_address">Copiar dirección email</string>
+ <string name="copied_email_address">Email copiado al portapapeles</string>
+ <string name="uri_copied_to_clipboard">URI copiada al portapapeles</string>
+ <string name="copy_URI">Copiar URI</string>
+ <string name="copied_phone_number">Número de teléfono copiado al portapapeles</string>
+ <string name="copy_telephone_number">Copiar numero de teléfono</string>
</resources>
@@ -1129,4 +1129,12 @@
<string name="restore_omemo_key">Ripristina chiavi OMEMO</string>
<string name="non_quicksy_backup">Quicksy può ripristinare backup solo per profili quicksy.im</string>
<string name="pref_backup_location">Percorso backup</string>
+ <string name="uri">URI</string>
+ <string name="copy_geo_uri">Copia posizione geografica</string>
+ <string name="copy_email_address">Copia indirizzo email</string>
+ <string name="copied_email_address">Indirizzo email copiato negli appunti</string>
+ <string name="copied_phone_number">Numero di telefono copiato negli appunti</string>
+ <string name="copy_telephone_number">Copia numero di telefono</string>
+ <string name="copy_URI">Copia URI</string>
+ <string name="uri_copied_to_clipboard">URI copiato negli appunti</string>
</resources>
@@ -1127,4 +1127,7 @@
<string name="delete_avatar_message">Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar.</string>
<string name="show_to_contacts_only">Show to contacts only</string>
<string name="pref_backup_location">Backup location</string>
+ <string name="account_status_service_outage_scheduled">Planned Downtime</string>
+ <string name="account_status_service_outage_known">Service Down (Known Issue)</string>
+ <string name="sos_scheduled_return">The service is scheduled to return at %s</string>
</resources>
@@ -14,6 +14,7 @@
<style name="Theme.Conversations3.SplashScreen" parent="@style/Theme.Conversations3">
<item name="android:windowBackground">@drawable/background_splash_screen</item>
<item name="android:windowLightStatusBar">true</item>
+ <item name="android:windowFullscreen">true</item>
</style>
<style name="Theme.Conversations3.Dialog" parent="@style/Theme.Conversations3">
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
@@ -5,6 +5,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
@@ -4,6 +4,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout