diff --git a/CHANGELOG.md b/CHANGELOG.md index 739c328d05491164e1e5ed0c3e81bc4c98922d3f..bd242198d888427b4b1dd9180bb036867aa2e396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/build.gradle b/build.gradle index 18efc96ebbc50d52f34df67593626631d7d0caf8..cff7b8224a72c7f8ac0764f6cd78e6869b203e5b 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/fastlane/metadata/android/en-US/changelogs/4214004.txt b/fastlane/metadata/android/en-US/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..03024d49fc63e7d24aabfd86c4d62b28e7dec975 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Fix reactions on files received via P2P +* Improve URI matching diff --git a/fastlane/metadata/android/it-IT/changelogs/4214004.txt b/fastlane/metadata/android/it-IT/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..f68f27f50b7e97268cab94faa78c1d3737dddf72 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Corrette le reazioni sui file ricevuti via P2P +* Migliorata la corrispondenza degli URI diff --git a/fastlane/metadata/android/sq/changelogs/4213104.txt b/fastlane/metadata/android/sq/changelogs/4213104.txt new file mode 100644 index 0000000000000000000000000000000000000000..e8da9e89e96ae919b8318b806b1cc54aac7a164c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4213104.txt @@ -0,0 +1,2 @@ +* Shfrytëzim i SASL SCRAM Downgrade Protection (XEP-0474) +* Dërgim reagimesh ndaj MP MUC te JID i saktë diff --git a/fastlane/metadata/android/uk/changelogs/4214004.txt b/fastlane/metadata/android/uk/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..b21f4b435d33f10e15114eae42f0b61bf6e223f4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Виправлено реакції на файли, отримані через P2P +* Покращено співставлення URI diff --git a/fastlane/metadata/android/zh-CN/changelogs/4214004.txt b/fastlane/metadata/android/zh-CN/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..f95ae57ca1d855ab03dcb75c8c03056cbb6eb461 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* 修复对通过 P2P 接收的文件的回应 +* 改进 URI 匹配 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f9b6580c25d12981e61c165826a3bb103b3b069b..6e4efaf7113b140864b462ffb2148391ac579caa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/src/conversations/res/layout/activity_easy_invite.xml b/src/conversations/res/layout/activity_easy_invite.xml index 6f1c8fc57d386f4a45f4ef781dc37446ce61cd1f..3d1c20c36c09770a1d5bb003ef741271239295c1 100644 --- a/src/conversations/res/layout/activity_easy_invite.xml +++ b/src/conversations/res/layout/activity_easy_invite.xml @@ -5,6 +5,7 @@ getCommonNames(final X509Certificate certificate) { - List domains = new ArrayList<>(); + final var domains = new ImmutableList.Builder(); 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 haystack) { - for (final String entry : haystack) { - if (entry.startsWith("*.")) { + public static boolean matchDomain(final String domain, final List 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) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 1f9903acf6b82b1ac579e1e6aa82ca9cbaed1c42..078661617a806379c8df46fd36bc5f8483f741f5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -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 postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment - private final Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup + private final Set postponedSessions = + new HashSet<>(); // sessions stored here will receive after mam catchup treatment + private final Set 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 getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); + return axolotlStore.getContactKeysWithTrust( + account.getJid().asBareJid().toString(), status); } public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { @@ -226,21 +229,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Collection findOwnSessions() { SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); + ArrayList s = + new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); Collections.sort(s); return s; } public Collection findSessionsForContact(Contact contact) { SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); + ArrayList s = + new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); Collections.sort(s); return s; } private Set findSessionsForConversation(Conversation conversation) { if (conversation.getContact().isSelf()) { - //will be added in findOwnSessions() + // will be added in findOwnSessions() return Collections.emptySet(); } HashSet 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 expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); + final Set 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 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 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 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 deviceIds = IqParser.deviceIds(item); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved own device list: " + + deviceIds); + registerDevices(account.getJid().asBareJid(), deviceIds); + } + }); } private Set 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 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 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 preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + public void publishDeviceVerificationAndBundle( + final SignedPreKeyRecord signedPreKeyRecord, + final Set 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 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 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 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 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 preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord + .getKeyPair() + .getPublicKey() + .equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = + KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Adding " + + newKeys + + " new preKeys to PEP."); + } - if (changed || changeAccessMode.get()) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); + 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 preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + private void publishDeviceBundle( + SignedPreKeyRecord signedPreKeyRecord, + Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true); } - private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set 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 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() { @@ -734,8 +993,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { mXmppConnectionService.updateAccountUi(); } - private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + private ListenableFuture verifySessionWithPEP( + final XmppAxolotlSession session) { + Log.d( + Config.LOGTAG, + "trying to verify fresh session (" + + session.getRemoteAddress().getName() + + ") with pep"); final SignalProtocolAddress address = session.getRemoteAddress(); final IdentityKey identityKey = session.getIdentityKey(); final Jid jid; @@ -747,64 +1011,89 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } final SettableFuture future = SettableFuture.create(); - final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - Pair verification = IqParser.verification(response); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + Pair verification = IqParser.verification(response); + if (verification != null) { try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid1 = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); - account.getRoster().getContact(jid1).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService + .getMemorizingTrustManager() + .getNonInteractive() + .checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d( + Config.LOGTAG, + "verified session with x.509 signature. fingerprint" + + " was: " + + fingerprint); + setFingerprintTrust( + fingerprint, + FingerprintStatus.createActiveVerified(true)); + axolotlStore.setFingerprintCertificate( + fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = + CryptoHelper.extractCertificateInformation( + verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid1 = Jid.of(address.getName()); + Log.d( + Config.LOGTAG, + "setting common name for " + jid1 + " to " + cn); + account.getRoster().getContact(jid1).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + // ignored + } + finishBuildingSessionsFromPEP(address); + future.set(session); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } } - finishBuildingSessionsFromPEP(address); - future.set(session); - return; } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); } + } else { + Log.d(Config.LOGTAG, "no verification found"); } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - future.set(session); - }); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + future.set(session); + }); return future; } private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + SignalProtocolAddress ownAddress = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); Map own = fetchStatusMap.getAll(ownAddress.getName()); Map remote = fetchStatusMap.getAll(address.getName()); if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { FetchStatus report = null; - if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { + if (own.containsValue(FetchStatus.SUCCESS) + || remote.containsValue(FetchStatus.SUCCESS)) { report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { + } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) + || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { report = FetchStatus.SUCCESS_VERIFIED; - } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { + } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) + || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { report = FetchStatus.SUCCESS_TRUSTED; - } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { + } else if (own.containsValue(FetchStatus.ERROR) + || remote.containsValue(FetchStatus.ERROR)) { report = FetchStatus.ERROR; } mXmppConnectionService.keyStatusUpdated(report); @@ -814,9 +1103,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { boolean publish = false; for (Map.Entry entry : own.entrySet()) { int id = entry.getKey(); - if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) { + if (entry.getValue() == FetchStatus.ERROR + && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) + && ownDeviceIds.remove(id)) { publish = true; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error fetching own device with id " + id + ". removing from announcement"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error fetching own device with id " + + id + + ". removing from announcement"); } } if (publish) { @@ -841,7 +1137,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callbacks.add(callback); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fetching device ids for " + + jid + + " already running. adding callback"); packet = null; } else { callbacks = new ArrayList<>(); @@ -849,43 +1150,49 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { callbacks.add(callback); } this.fetchDeviceIdsMap.put(jid, callbacks); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": fetching device ids for " + jid); packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); } } if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (response.getType() == Iq.Type.RESULT) { - fetchDeviceListStatus.put(jid, true); - final Element item = IqParser.getItem(response); - final Set deviceIds = IqParser.deviceIds(item); - registerDevices(jid, deviceIds); - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, deviceIds); - } - } - } else { - if (response.getType() == Iq.Type.TIMEOUT) { - fetchDeviceListStatus.remove(jid); - } else { - fetchDeviceListStatus.put(jid, false); - } - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, null); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.RESULT) { + fetchDeviceListStatus.put(jid, true); + // TODO use new API + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); + registerDevices(jid, deviceIds); + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, deviceIds); + } + } + } else { + if (response.getType() == Iq.Type.TIMEOUT) { + fetchDeviceListStatus.remove(jid); + } else { + fetchDeviceListStatus.put(jid, false); + } + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, null); + } + } } - } - } - }); + }); } } @@ -893,126 +1200,194 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final ArrayList unfinishedJids = new ArrayList<>(jids); synchronized (unfinishedJids) { for (Jid jid : unfinishedJids) { - fetchDeviceIds(jid, (j, deviceIds) -> { - synchronized (unfinishedJids) { - unfinishedJids.remove(j); - if (unfinishedJids.size() == 0 && callback != null) { - callback.fetched(); - } - } - }); + fetchDeviceIds( + jid, + (j, deviceIds) -> { + synchronized (unfinishedJids) { + unfinishedJids.remove(j); + if (unfinishedJids.size() == 0 && callback != null) { + callback.fetched(); + } + } + }); } } } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address) { return buildSessionFromPEP(address, null); } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address, OnSessionBuildFromPep callback) { final SettableFuture sessionSettableFuture = SettableFuture.create(); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Building new session for " + + address.toString()); if (address.equals(getOwnAxolotlAddress())) { - throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); + throw new AssertionError( + "We should NEVER build a session with ourselves. What happened here?!"); } final Jid jid = Jid.of(address.getName()); final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, packet -> { - if (packet.getType() == Iq.Type.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); - } else if (packet.getType() == Iq.Type.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final List preKeyBundleList = IqParser.preKeys(packet); - final PreKeyBundle bundle = IqParser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid")); - return; - } - Random random = new Random(); - final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (preKey == null) { - //should never happen - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found")); - return; - } + final Iq bundlesPacket = + mXmppConnectionService + .getIqGenerator() + .retrieveBundlesForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + bundlesPacket, + packet -> { + if (packet.getType() == Iq.Type.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + sessionSettableFuture.setException( + new CryptoFailedException("Unable to build session. Timeout")); + } else if (packet.getType() == Iq.Type.RESULT) { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received preKey IQ packet, processing..."); + final List preKeyBundleList = IqParser.preKeys(packet); + final PreKeyBundle bundle = IqParser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "preKey IQ packet invalid: " + + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Invalid")); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = + preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + // should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. No suitable PreKey found")); + return; + } - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + final PreKeyBundle preKeyBundle = + new PreKeyBundle( + 0, + address.getDeviceId(), + preKey.getPreKeyId(), + preKey.getPreKey(), + bundle.getSignedPreKeyId(), + bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), + bundle.getIdentityKey()); - try { - SessionBuilder builder = new SessionBuilder(axolotlStore, address); - builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); - sessions.put(address, session); - if (Config.X509_VERIFICATION) { - sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too - } else { - FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); - FetchStatus fetchStatus; - if (status != null && status.isVerified()) { - fetchStatus = FetchStatus.SUCCESS_VERIFIED; - } else if (status != null && status.isTrusted()) { - fetchStatus = FetchStatus.SUCCESS_TRUSTED; - } else { - fetchStatus = FetchStatus.SUCCESS; + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = + new XmppAxolotlSession( + account, + axolotlStore, + address, + bundle.getIdentityKey()); + sessions.put(address, session); + if (Config.X509_VERIFICATION) { + sessionSettableFuture.setFuture( + verifySessionWithPEP( + session)); // TODO; maybe inject callback in here + // too + } else { + FingerprintStatus status = + getFingerprintTrust( + CryptoHelper.bytesToHex( + bundle.getIdentityKey() + .getPublicKey() + .serialize())); + FetchStatus fetchStatus; + if (status != null && status.isVerified()) { + fetchStatus = FetchStatus.SUCCESS_VERIFIED; + } else if (status != null && status.isTrusted()) { + fetchStatus = FetchStatus.SUCCESS_TRUSTED; + } else { + fetchStatus = FetchStatus.SUCCESS; + } + fetchStatusMap.put(address, fetchStatus); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildSuccessful(); + } + sessionSettableFuture.set(session); + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Error building session for " + + address + + ": " + + e.getClass().getName() + + ", " + + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException(new CryptoFailedException(e)); } - fetchStatusMap.put(address, fetchStatus); + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Element error = packet.findChild("error"); + boolean itemNotFound = error != null && error.hasChild("item-not-found"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Error received while building session:" + + packet.findChild("error")); finishBuildingSessionsFromPEP(address); + if (oneOfOurs + && itemNotFound + && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } if (callback != null) { - callback.onSessionBuildSuccessful(); + callback.onSessionBuildFailed(); } - sessionSettableFuture.set(session); - } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Error")); } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException(e)); - } - } else { - fetchStatusMap.put(address, FetchStatus.ERROR); - Element error = packet.findChild("error"); - boolean itemNotFound = error != null && error.hasChild("item-not-found"); - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && itemNotFound && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error")); - } - }); + }); return sessionSettableFuture; } private void removeFromDeviceAnnouncement(Integer id) { HashSet temp = new HashSet<>(getOwnDeviceIds()); if (temp.remove(id)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " remove own device id " + id + " from announcement. devices left:" + temp); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " remove own device id " + + id + + " from announcement. devices left:" + + temp); publishOwnDeviceId(temp); } } @@ -1020,47 +1395,95 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Set findDevicesWithoutSession(final Conversation conversation) { Set addresses = new HashSet<>(); for (Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Finding devices without session for " + + jid); final Set ids = deviceIds.get(jid); if (ids != null && !ids.isEmpty()) { for (Integer foreignId : ids) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.toString(), foreignId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore + .loadSession(address) + .getSessionState() + .getRemoteIdentityKey(); if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already have session for " + + address.toString() + + ", adding to cache..."); + XmppAxolotlSession session = + new XmppAxolotlSession( + account, axolotlStore, address, identityKey); sessions.put(address, session); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Found device " + + jid + + ":" + + foreignId); if (fetchStatusMap.get(address) != FetchStatus.ERROR) { addresses.add(address); } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "skipping over " + + address + + " because it's broken"); } } } } } else { mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR); - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } } Set ownIds = this.deviceIds.get(account.getJid().asBareJid()); for (Integer ownId : (ownIds != null ? ownIds : new HashSet())) { - SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); + SignalProtocolAddress address = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already have session for " + + address.toString() + + ", adding to cache..."); + XmppAxolotlSession session = + new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().asBareJid() + ":" + ownId); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Found device " + + account.getJid().asBareJid() + + ":" + + ownId); if (fetchStatusMap.get(address) != FetchStatus.ERROR) { addresses.add(address); } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "skipping over " + + address + + " because it's broken"); } } } @@ -1077,9 +1500,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { iterator.remove(); } } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": createSessionsIfNeeded() - jids with empty device list: " + + jidsWithEmptyDeviceList); if (jidsWithEmptyDeviceList.size() > 0) { - fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); + fetchDeviceIds( + jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); return true; } else { return createSessionsIfNeededActual(conversation); @@ -1087,11 +1515,17 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private boolean createSessionsIfNeededActual(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); boolean newSessions = false; Set addresses = findDevicesWithoutSession(conversation); for (SignalProtocolAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Processing device: " + + address.toString()); FetchStatus status = fetchStatusMap.get(address); if (status == null || status == FetchStatus.TIMEOUT) { fetchStatusMap.put(address, FetchStatus.PENDING); @@ -1100,7 +1534,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else if (status == FetchStatus.PENDING) { newSessions = true; } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already fetching bundle for " + + address.toString()); } } @@ -1125,14 +1563,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public boolean hasPendingKeyFetches(List jids) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + SignalProtocolAddress ownAddress = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) { return true; } synchronized (this.fetchDeviceIdsMap) { for (Jid jid : jids) { - SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.asBareJid().toString(), 0); - if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) { + SignalProtocolAddress foreignAddress = + new SignalProtocolAddress(jid.asBareJid().toString(), 0); + if (fetchStatusMap + .getAll(foreignAddress.getName()) + .containsValue(FetchStatus.PENDING) + || this.fetchDeviceIdsMap.containsKey(jid)) { return true; } } @@ -1143,7 +1586,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Nullable private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation c) { Set remoteSessions = findSessionsForConversation(c); - final boolean acceptEmpty = (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) || c.getContact().isSelf(); + final boolean acceptEmpty = + (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) + || c.getContact().isSelf(); Collection ownSessions = findOwnSessions(); if (remoteSessions.isEmpty() && !acceptEmpty) { return false; @@ -1158,7 +1603,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return true; } - //this is being used for private muc messages only + // this is being used for private muc messages only private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { if (jid == null) { return false; @@ -1175,17 +1620,32 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return true; } - @Nullable - public XmppAxolotlMessage encrypt(final String content, Jid counterpart) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url; + } else { + content = message.getRawBody(); + } try { axolotlMessage.encrypt(content); } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); return null; } - if (!buildHeader(axolotlMessage, counterpart)) return null; - return axolotlMessage; + + final boolean success; + if (message.isPrivateMessage()) { + success = buildHeader(axolotlMessage, message.getTrueCounterpart()); + } else { + success = buildHeader(axolotlMessage, (Conversation) message.getConversation()); + } + return success ? axolotlMessage : null; } @Nullable @@ -1201,48 +1661,43 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return axolotlMessage; } - @Nullable - public XmppAxolotlMessage encrypt(Message message) { - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url; - } else { - content = message.getRawBody(); - } - - if (message.isPrivateMessage()) { - return encrypt(content, message.getTrueCounterpart()); - } else { - return encrypt(content, (Conversation) message.getConversation()); - } - } - public void preparePayloadMessage(final Message message, final boolean delay) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = encrypt(message); - if (axolotlMessage == null) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - //mXmppConnectionService.updateConversationUi(); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay, true); - } - } - }); + executor.execute( + new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + // mXmppConnectionService.updateConversationUi(); + } else { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Generated message, caching: " + + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay, true); + } + } + }); } - private OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException { - final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo(); + private OmemoVerifiedIceUdpTransportInfo encrypt( + final IceUdpTransportInfo element, final XmppAxolotlSession session) + throws CryptoFailedException { + final OmemoVerifiedIceUdpTransportInfo transportInfo = + new OmemoVerifiedIceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); for (final Element child : element.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { - final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + if ("fingerprint".equals(child.getName()) + && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Element fingerprint = + new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content = child.getContent(); axolotlMessage.encrypt(content); axolotlMessage.addDevice(session, true); @@ -1255,25 +1710,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return transportInfo; } - - public ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { + public ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { return Futures.transformAsync( getSession(jid, deviceId), session -> encrypt(rtpContentMap, session), - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { + private ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { if (Config.REQUIRE_RTP_VERIFICATION) { requireVerification(session); } - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(session.getFingerprint()); - for (final Map.Entry> content : rtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + for (final Map.Entry> + content : rtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; try { encryptedTransportInfo = encrypt(descriptionTransport.transport, session); @@ -1282,18 +1741,21 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + encryptedTransportInfo)); } return Futures.immediateFuture( new OmemoVerifiedPayload<>( omemoVerification, - new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) - )); + new OmemoVerifiedRtpContentMap( + rtpContentMap.group, descriptionTransportBuilder.build()))); } private ListenableFuture getSession(final Jid jid, final int deviceId) { - final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + final SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); final XmppAxolotlSession session = sessions.get(address); if (session == null) { return buildSessionFromPEP(address); @@ -1301,26 +1763,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } - public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + public ListenableFuture> decrypt( + OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); - final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); - for (final Map.Entry> content : omemoVerifiedRtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + final ImmutableList.Builder> pepVerificationFutures = + new ImmutableList.Builder<>(); + for (final Map.Entry> + content : omemoVerifiedRtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedPayload decryptedTransport; try { - decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); + decryptedTransport = + decrypt( + (OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, + from, + pepVerificationFutures); } catch (CryptoFailedException e) { return Futures.immediateFailedFuture(e); } omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + decryptedTransport.payload)); } processPostponed(); - final ImmutableList> sessionFutures = pepVerificationFutures.build(); + final ImmutableList> sessionFutures = + pepVerificationFutures.build(); return Futures.transform( Futures.allAsList(sessionFutures), sessions -> { @@ -1331,27 +1806,35 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } return new OmemoVerifiedPayload<>( omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) - ); - + new RtpContentMap( + omemoVerifiedRtpContentMap.group, + descriptionTransportBuilder.build())); }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { + private OmemoVerifiedPayload decrypt( + final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, + final Jid from, + ImmutableList.Builder> pepVerificationFutures) + throws CryptoFailedException { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); final OmemoVerification omemoVerification = new OmemoVerification(); for (final Element child : verifiedIceUdpTransportInfo.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { + if ("fingerprint".equals(child.getName()) + && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); + final Element encrypted = + child.findChildEnsureSingle( + XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); + final XmppAxolotlMessage xmppAxolotlMessage = + XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage); - final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); + final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = + xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); final Integer preKeyId = session.getPreKeyIdAndReset(); if (preKeyId != null) { postponedSessions.add(session); @@ -1376,45 +1859,53 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getTrust().isVerified()) { return; } - throw new NotVerifiedException(String.format( - "session with %s was not verified", - session.getFingerprint() - )); + throw new NotVerifiedException( + String.format("session with %s was not verified", session.getFingerprint())); } - public ListenableFuture prepareKeyTransportMessage(final Conversation conversation) { - return Futures.submit(()->{ - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - return axolotlMessage; - } else { - throw new IllegalStateException("No session to decrypt to"); - } - },executor); + public ListenableFuture prepareKeyTransportMessage( + final Conversation conversation) { + return Futures.submit( + () -> { + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + if (buildHeader(axolotlMessage, conversation)) { + return axolotlMessage; + } else { + throw new IllegalStateException("No session to decrypt to"); + } + }, + executor); } public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); messageCache.remove(message.getUuid()); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); } return axolotlMessage; } private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); return (identityKey != null) ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) : null; } private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { - SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toString(), message.getSenderDeviceId()); + SignalProtocolAddress senderAddress = + new SignalProtocolAddress( + message.getFrom().toString(), message.getSenderDeviceId()); return getReceivingSession(senderAddress); - } private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { @@ -1428,7 +1919,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return session; } - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException { + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage( + XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) + throws NotEncryptedForThisDeviceException, + BrokenSessionException, + OutdatedSenderException { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; XmppAxolotlSession session = getReceivingSession(message); @@ -1440,7 +1935,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { postPreKeyMessageHandling(session, postponePreKeyMessageHandling); } } catch (NotEncryptedForThisDeviceException e) { - if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { + if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) + && message.getSenderDeviceId() == ownDeviceId) { Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); } else { throw e; @@ -1451,7 +1947,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); throw e; } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), + e); } if (session.isFresh() && plaintextMessage != null) { @@ -1462,7 +1961,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": broken session with " + e.getSignalProtocolAddress().toString() + " detected", e); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": broken session with " + + e.getSignalProtocolAddress().toString() + + " detected", + e); if (postpone) { postponedHealing.add(e.getSignalProtocolAddress()); } else { @@ -1472,32 +1977,51 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void notifyRequiresHealing(final SignalProtocolAddress signalProtocolAddress) { if (healingAttempts.add(signalProtocolAddress)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); - buildSessionFromPEP(signalProtocolAddress, new OnSessionBuildFromPep() { - @Override - public void onSessionBuildSuccessful() { - Log.d(Config.LOGTAG, "successfully build new session from pep after detecting broken session"); - completeSession(getReceivingSession(signalProtocolAddress)); - } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); + buildSessionFromPEP( + signalProtocolAddress, + new OnSessionBuildFromPep() { + @Override + public void onSessionBuildSuccessful() { + Log.d( + Config.LOGTAG, + "successfully build new session from pep after detecting broken" + + " session"); + completeSession(getReceivingSession(signalProtocolAddress)); + } - @Override - public void onSessionBuildFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to build new session from pep after detecting broken session"); - } - }); + @Override + public void onSessionBuildFailed() { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to build new session from pep after" + + " detecting broken session"); + } + }); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt to heal " + signalProtocolAddress + " again"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": do not attempt to heal " + + signalProtocolAddress + + " again"); } } - private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { + private void postPreKeyMessageHandling( + final XmppAxolotlSession session, final boolean postpone) { if (postpone) { postponedSessions.add(session); } else { if (axolotlStore.flushPreKeys()) { publishBundlesIfNeeded(false, false); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); } if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) { completeSession(session); @@ -1519,7 +2043,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } iterator.remove(); } - final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); + final Iterator postponedHealingAttemptsIterator = + postponedHealing.iterator(); while (postponedHealingAttemptsIterator.hasNext()) { notifyRequiresHealing(postponedHealingAttemptsIterator.next()); postponedHealingAttemptsIterator.remove(); @@ -1544,18 +2069,24 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void completeSession(XmppAxolotlSession session) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); axolotlMessage.addDevice(session, true); try { final Jid jid = Jid.of(session.getRemoteAddress().getName()); - final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + final var packet = + mXmppConnectionService + .getMessageGenerator() + .generateKeyTransportMessage(jid, axolotlMessage); mXmppConnectionService.sendMessagePacket(account, packet); } catch (IllegalArgumentException e) { - throw new Error("Remote addresses are created from jid and should convert back to jid", e); + throw new Error( + "Remote addresses are created from jid and should convert back to jid", e); } } - public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage( + XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; final XmppAxolotlSession session = getReceivingSession(message); try { @@ -1582,7 +2113,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getIdentityKey() != null) { return verifySessionWithPEP(session); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": identity key was empty after reloading for x509 verification"); } } return Futures.immediateFuture(session); @@ -1601,7 +2135,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { void fetched(Jid jid, Set deviceIds); } - public interface OnMultipleDeviceIdFetched { void fetched(); } @@ -1661,14 +2194,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void clear() { map.clear(); } - } private static class SessionMap extends AxolotlAddressMap { private final XmppConnectionService xmppConnectionService; private final Account account; - public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + public SessionMap( + XmppConnectionService service, SQLiteAxolotlStore store, Account account) { super(); this.xmppConnectionService = service; this.account = account; @@ -1688,30 +2221,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return candidates; } - private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { + private void putDevicesForJid( + String bareJid, List deviceIds, SQLiteAxolotlStore store) { for (Integer deviceId : deviceIds) { SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); - IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); if (Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); + X509Certificate certificate = + store.getFingerprintCertificate( + CryptoHelper.bytesToHex( + identityKey.getPublicKey().serialize())); if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); + Bundle information = + CryptoHelper.extractCertificateInformation(certificate); try { final String cn = information.getString("subject_cn"); final Jid jid = Jid.of(bareJid); Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); account.getRoster().getContact(jid).setCommonName(cn); } catch (final IllegalArgumentException ignored) { - //ignored + // ignored } } } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + this.put( + axolotlAddress, + new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } private void fillMap(SQLiteAxolotlStore store) { - List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); + List deviceIds = + store.getSubDeviceSessions(account.getJid().asBareJid().toString()); putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); for (String address : store.getKnownAddresses()) { deviceIds = store.getSubDeviceSessions(address); @@ -1740,7 +2282,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } for (Map.Entry entry : devices.entrySet()) { if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); + Log.d( + Config.LOGTAG, + "resetting error for " + + jid.asBareJid() + + "(" + + entry.getKey() + + ")"); entry.setValue(FetchStatus.TIMEOUT); } } @@ -1777,6 +2325,5 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public NotVerifiedException(String message) { super(message); } - } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 61b0d627c367c85a7bebefbc9f88405aeb060311..8866550a62c5099a78e39a355ae6f5c93ff06c17 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -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 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; + }; } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index cef2b653eefc9f99dbb97bb6d2988835585c80eb..3d9d2594b98a6a5a8b8634b8d6639c15e7158568 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -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 parseFromStorage(Element storage, Account account) { + public static Map 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 parseFromPubSub(final Element pubSub, final Account account) { + public static Map 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 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 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; diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index e784b3e8cb8f0bfc9542e27860463f2fce0c21c6..1c0e6410bf79bb4f431bc4525ee98a256ab210e7 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -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 features; - protected final List forms; - private final List 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 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 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 getIdentities() { - return this.identities; - } - - public List 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 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 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 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 features; + protected final List forms; + private final List 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 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 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 getIdentities() { + return this.identities; + } + + public List 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 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 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 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 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 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 { - 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 { + 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; + } + } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 053361db62c0f5f2fabe102b3319beae746a4496..b686c54beddb100051876488dba11d38f0fe07b7 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -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 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; } diff --git a/src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java b/src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f74a9e4d7ff760d7c09edfd95ef2ed2e104e5fd8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java @@ -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 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 message; + + public ServiceOutageStatus( + final boolean planned, + final Instant beginning, + final Instant expectedEnd, + final Map 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 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 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 { + @Override + public Instant deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return Instant.parse(json.getAsString()); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c78a6660fc5b02bd7c68114282ac9ce74908fc92..3b6ba66545da06bdfb17edac16b9c8af7a402894 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -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 { @@ -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 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 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 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 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 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()) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 4a4a0dbd7fd1f5a86699444523096e98a33f6b50..af7e9976049b75f8713ebf5ad24020cfd830f04f 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -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() diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index dba2206ccc9ea305b00868c23d88e50f4a13fd96..037b31dfadf8ccfe858f6b145ca3e90553e1cf3f 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -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 pathSegments + // TODO accept List pathSegments final int pos = path.lastIndexOf('/'); final String filename = path.substring(pos + 1).toLowerCase(); final String[] parts = filename.split("\\."); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b01e2ab9295cea4c9f39c11d480ed1aa8637f0ca..3cb37eb9ebe049985a44e69b7b4c43ea4beca9f4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 callback = (response) -> { if (response.getType() == Iq.Type.RESULT) { - final Element query1 = response.query(); - final Element storage = query1.findChild("storage", "storage:bookmarks"); - Map bookmarks = Bookmark.parseFromStorage(storage, account); + final var privateStorage = response.getExtension(PrivateStorage.class); + if (privateStorage == null) { + return; + } + final var bookmarkStorage = privateStorage.getExtension(Storage.class); + Map bookmarks = + Bookmark.parseFromStorage(bookmarkStorage, account); processBookmarksInitial(account, bookmarks, false); } else { Log.d( @@ -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 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 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() { - 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); }); } diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java index bcbfdef00a9207107c29b6ebac2a53711d64ab09..58c597b244ac07a527e29a030273e5ff42bf2dfc 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index d2ef50c6d82a6ea823975492aa2afeb925431645..64247cc9627aafafcd24c9fc08a43f7653585ada 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -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 { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index da5231357e42cd486e205158aceabdc0a817cebc..8601d88f58cc51ad3f5c27ec82f96241fb5f1c9d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -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(); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 63741f007bf1cdc5209eb03cac3b9a3a7a4db4d4..f57b7952e44bb490f80bfa5d70364ee0e07ff6d3 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -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 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); + } } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 591f65cee4b9597f6a0ffdd553873416973db360..5bb4cf403a1dac97dbb03d6763feb6ed49335d51 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -52,11 +52,25 @@ public class AccountAdapter extends ArrayAdapter { } 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( diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index f8364611f972ce1ffec52327cfc5886ba6582cd3..e09c388e7b5dc6a387977c0174ff091d67a643c1 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -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()); diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index be523aa1310f587b4a0527b796bfe2b247589b16..a12e13184d9162b11aa64a8e13ffdef1ad12d9cb 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 548e2ca6ded2d5433003cd6bc2283fec3e0c0395..c5fb190723b4dce38393589ee91c41b4bd4824f0 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -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"))) { diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index a02041acb90f9f16077047995e5473a9f2a19721..1468daf8d8a01e5a62c64aa3ff4b585ca166ec55 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -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 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 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()); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 00df32651dff90b07a4850b233302c9ea63c0b1e..7de95c6bd9a827095a2081f57eb9141b4a00738e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -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"; } diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 0d5377cc9aa1d9cc9582811d0c4f6db2bef0b5f3..3a6cd7fafb92b20314f95dfd6250fa142783a4ac 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -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 readElement(final Tag current, final Class clazz) - throws IOException { - final Element element = readElement(current); - if (clazz.isInstance(element)) { - return clazz.cast(element); - } - throw new IOException( - String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); - } + public T readElement(final Tag current, final Class clazz) + throws IOException { + final Element element = readElement(current); + if (clazz.isInstance(element)) { + return clazz.cast(element); + } + throw new IOException( + String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); + } public Element readElement(final Tag currentTag) throws IOException { final var attributes = currentTag.getAttributes(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e75ba27c78bb518638dcbe14c26f1aaa881d5715..7285bd29fa55fce1f328ef298a09894847aa9864 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 1b1a2d0e3ecf5f287467f789f771b7320df82ce2..de325534e3a5c45f9bc6acc4aa0a3b8a985aed82 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -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; }; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index fbaf7ffe3e7a52d5a9148cc780cb0d6135448912..d8dd240c3c2c38d592cee78c3c2477e8aa560eb2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java index 6a41c89067e0d853f5b8db0d4a7a4140e6aab11b..4a491143e198f70cf47da3a6b501a9a973c4b955 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java @@ -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); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index f573dc30e666aea33d31590a37760efe43b11dca..6badfe31f7888ee3207d054cd8e07092dbfd0276 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -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 { @@ -492,7 +492,7 @@ public class RtpContentMap extends AbstractContentMap { public final T track; public final RtpSender rtpSender; @@ -63,7 +58,7 @@ class TrackWrapper { } public static RtpTransceiver getTransceiver( - @Nonnull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + @NonNull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { final RtpSender rtpSender = trackWrapper.rtpSender; final String rtpSenderId; try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java index 89552fc1814b1d9215ce6727268923d64182d67a..3a8a4cc6d7440acb92e244f4349aa0115e6eb83d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index cbcd596dd44e145a1d4ed8af224f956a7a0b31d1..573f969ad9c824d1e33d4aad18833fceddf5d9f6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -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 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java index 80b0322f4727cf418d7f37de58d9cb6feb1ddbf6..2540d6027daf5aa336b1e2e998b3ea8be877fd7e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -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()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 00bd671b81fbf867712eddc6023e0684a9fb05ec..5cab326f1db0f03d4742e77baafc0c966deb7e90 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -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}"); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index c9b764752d3bbea3225ffa4b67bb1a3a75b31730..2dcaf9bac7b07c6cc89d7d7a3b6db18789f59fc7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -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; } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java index 0f924e8883b40157d0aa96676ab5f6412e971138..17fed121b1145978b81fe3a8ba5ac521fb2d3de3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java @@ -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); - } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java new file mode 100644 index 0000000000000000000000000000000000000000..2ecac583dd8686251550bf6f66686ec2ee7f2f00 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Storage extends Extension { + + public Storage() { + super(Storage.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java index 1bb963be849a57cdfeae7a2eb1c8930db49c799a..2a50d0f27818cf8cffe9904275e940ad88a298e3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java new file mode 100644 index 0000000000000000000000000000000000000000..265c80e3fd7db0d2e56572eb700f94592af12a5e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java similarity index 81% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java index b9385cf5473de88d5d43d771a57fe88995bc2536..f3d3534faaa49f200f4575160af7786ae260b066 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java similarity index 79% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java index ee5efa3864baebb0c848c65769b6b0a8d4a69022..c309e5056b74daa783e7fcc2f68796b1357938ba 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d872edde9aaa08dbacb78b603f97512f80860e98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java index 9f5275371c3564ee52d1825bc67fda4327d36514..a49216c27dc2f1ea7f5cd9ee1f1538366127ac88 100644 --- a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java +++ b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java @@ -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); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java index ceb1931ca35cbb1048e5c66f14cfe5ea9eff2303..47fc68c08368afccb4fc20703bd2aeae1aa81728 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java @@ -45,6 +45,11 @@ public interface Items { return Iterables.getFirst(map.values(), null); } + default Map.Entry getFirstItemWithId(final Class clazz) { + final var entries = getItemMap(clazz).entrySet(); + return Iterables.getFirst(entries, null); + } + default T getOnlyItem(final Class clazz) { final var map = getItemMap(clazz); return Iterables.getOnlyElement(map.values()); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java new file mode 100644 index 0000000000000000000000000000000000000000..c41a9fbdb2d4038835b711978a1920e37e772189 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java @@ -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 clazz) { + super(clazz); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java new file mode 100644 index 0000000000000000000000000000000000000000..ed5f6295eca366dff2895757402cc618042547c4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java index 1e180c460053ee0bb74cbefa753c86b63f175e2f..d224060d452378cc0f81bb60b6ad5061b4dc2a5d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java @@ -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 getItems() { return this.getExtensions(Item.class); } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java index 64550e0b77f1ca33d57e2b501204995545bfcac6..66329a2aff54b05ca2de2c79abb952f911d18aa1 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java @@ -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); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java index 139a49522c48cb74aa9b4469768baf43305fb446..bbdf2fc497e441ba6ba2a191b885c88027591477 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java @@ -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); diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java index 71fe922c158adeb678b30e6181047329e5dc0784..c3c987a5c4c3f4bbcde5868397a676640d60a968 100644 --- a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java @@ -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); } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java index ddbdb84551e8d456286aedaee1ac8126c8a4614f..5bbe07851acb015c23b9bb4b6844dcb5e0f3e000 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -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 clazz) { + final var builder = new ImmutableMultimap.Builder(); + 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) { diff --git a/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..dff10104a81fe596a0ff827ec8c7dc40b5af7b8e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java index 23b0fdcac823de630719bf14f8c47a559a2fbcfd..0078bcbae2be3558658908c98a48e71d6636ac42 100644 --- a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -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(); + 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 byToId; + try { + byToId = builder.buildOrThrow(); + } catch (final IllegalArgumentException e) { + return null; + } + return byToId.get(by); + } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 1423df7f50239d8511b511e3cb25c94c5a9fdd2f..3230185b648a92acc122d474f13a4f265572b1cf 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -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); diff --git a/src/main/res/layout-w945dp/activity_conversations.xml b/src/main/res/layout-w945dp/activity_conversations.xml index 700ba7dfc2208041c8f216cded541134f43f0a28..74a4c281177846acb797ecdab7f39ae74e9b8106 100644 --- a/src/main/res/layout-w945dp/activity_conversations.xml +++ b/src/main/res/layout-w945dp/activity_conversations.xml @@ -8,6 +8,7 @@ + diff --git a/src/main/res/layout/activity_change_password.xml b/src/main/res/layout/activity_change_password.xml index b5c9b2162747be61d5a42d51977d24258c86bacf..abcac84a06d6511fba9e81228692e878749807ad 100644 --- a/src/main/res/layout/activity_change_password.xml +++ b/src/main/res/layout/activity_change_password.xml @@ -4,7 +4,8 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index a04d54ee4fb829a799da26783d16680e0f66e273..f3d3134e43fa48f4555e9ec48bf6c53ce54c1b52 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -6,6 +6,7 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> Gestionar cuentas Gestionar cuenta Detalles del contacto - Detalles de conversación + Detalles de la conversación grupal Detalles del canal Añadir cuenta Editar contacto @@ -119,7 +119,7 @@ Periodo de gracia El tiempo que se silencian las notificaciones tras detectar actividad en otro de tus dispositivos. Avanzado - Al enviar los informes de los fallos, ayudará a un mayor desarrollo + Al enviar los informes, estás ayudando al desarrollo Confirmar mensajes Permitir a sus contactos saber cuando ha recibido y leído sus mensajes Impedir capturas de pantalla @@ -413,7 +413,7 @@ documento PDF aplicación para Android Contacto - Se ha publicado el avatar. + Foto de perfil ha sido publicada! Enviando %s Ofreciendo %s Ocultar desconectados @@ -484,7 +484,7 @@ Renovar certificado ¡Error buscando clave OMEMO! ¡Clave OMEMO con certificado verificada! - Su dispositivo no admite la selección de certificados de cliente. + Tu dispositivo no admite la selección de certificados de cliente! Conexión Conectar via Tor Todas las conexiones se realizan a través de la red TOR. Requiere Orbot @@ -826,7 +826,7 @@ e-book Original (sin comprimir) Abrir con… - Avatar + Foto de Perfil Elige una cuenta Restaurar copia de respaldo Restaurar @@ -1108,12 +1108,12 @@ El cliente XMPP de su contacto puede que no admita llamadas de audio/vídeo. No se pudo modificar la llamada Burbujas de chat - Color, Tamaño de fuente, Imágenes de perfil + Color de fondo, Tamaño de fuente, Imágenes de perfil Burbujas de Chat Integración de llamadas Mostrar avatares Mostrar imágenes de perfil para tus mensajes y conversaciones 1:1, aparte de las conversaciones en grupo. - Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como ser finalizar una llamada cuando recibimos otra. + Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como finalizar una llamada cuando recibimos otra. Mensajes alineados a la izquierda Mostrar todos los mensajes, incluso los propios, sobre el margen izquierdo para una distribución uniforme del chat. Notificaciones personalizadas @@ -1127,4 +1127,12 @@ Ubicación de la copia de seguridad Restaurar claves OMEMO Canal no disponible + URI + Copiar geolocalización + Copiar dirección email + Email copiado al portapapeles + URI copiada al portapapeles + Copiar URI + Número de teléfono copiado al portapapeles + Copiar numero de teléfono diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index f68233b5c618429cf6475df11002b507b6c2b114..a5d1d25e71f3f2de6f71b8951f019534ad606841 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -1129,4 +1129,12 @@ Ripristina chiavi OMEMO Quicksy può ripristinare backup solo per profili quicksy.im Percorso backup + URI + Copia posizione geografica + Copia indirizzo email + Indirizzo email copiato negli appunti + Numero di telefono copiato negli appunti + Copia numero di telefono + Copia URI + URI copiato negli appunti diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6687dee4f60dbd5a96e8fc943ddef238676e0bdb..7638509290248a404ef85b43266964534d264619 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1127,4 +1127,7 @@ Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar. Show to contacts only Backup location + Planned Downtime + Service Down (Known Issue) + The service is scheduled to return at %s diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 9e1f625201fe361921cb781727b64ac0239cf2dc..d27d9ac4b2a11ab8dcf50f2305cb0cecf41cfc10 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -14,6 +14,7 @@