From d3b38a5273ed7bafc04bcf5b6c9d2eb4449e64e3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 19 Dec 2023 17:25:48 +0100 Subject: [PATCH 001/556] refactor Jingle File Transfer. add WebRTCDatachannel transport --- .../java/eu/siacs/conversations/Config.java | 4 +- .../crypto/axolotl/AxolotlService.java | 37 +- .../generator/AbstractGenerator.java | 7 +- .../services/UnifiedPushBroker.java | 7 +- .../siacs/conversations/utils/Checksum.java | 60 - .../eu/siacs/conversations/xml/Namespace.java | 5 + .../xmpp/jingle/AbstractContentMap.java | 82 + .../xmpp/jingle/AbstractJingleConnection.java | 359 ++- .../xmpp/jingle/ContentAddition.java | 8 +- .../xmpp/jingle/DescriptionTransport.java | 19 + .../xmpp/jingle/DirectConnectionUtils.java | 32 +- .../xmpp/jingle/FileTransferContentMap.java | 219 ++ .../conversations/xmpp/jingle/IceServers.java | 98 + .../xmpp/jingle/JingleCandidate.java | 152 - .../xmpp/jingle/JingleConnectionManager.java | 105 +- .../jingle/JingleFileTransferConnection.java | 2547 +++++++++-------- .../xmpp/jingle/JingleInBandTransport.java | 265 -- .../xmpp/jingle/JingleRtpConnection.java | 422 +-- .../xmpp/jingle/JingleSocks5Transport.java | 305 -- .../xmpp/jingle/JingleTransport.java | 15 - .../xmpp/jingle/MediaBuilder.java | 23 +- .../jingle/OmemoVerifiedRtpContentMap.java | 6 +- .../xmpp/jingle/OnPrimaryCandidateFound.java | 5 - .../xmpp/jingle/RtpContentMap.java | 307 +- .../xmpp/jingle/SessionDescription.java | 216 +- .../xmpp/jingle/WebRTCWrapper.java | 8 +- .../xmpp/jingle/stanzas/Content.java | 15 +- .../stanzas/FileTransferDescription.java | 270 +- .../jingle/stanzas/GenericDescription.java | 1 + .../xmpp/jingle/stanzas/Group.java | 2 +- .../xmpp/jingle/stanzas/IbbTransportInfo.java | 13 +- .../jingle/stanzas/IceUdpTransportInfo.java | 6 +- .../xmpp/jingle/stanzas/JinglePacket.java | 53 +- .../xmpp/jingle/stanzas/Propose.java | 2 +- .../xmpp/jingle/stanzas/S5BTransportInfo.java | 50 - .../SocksByteStreamsTransportInfo.java | 117 + .../WebRTCDataChannelTransportInfo.java | 111 + .../InbandBytestreamsTransport.java | 321 +++ .../transports/SocksByteStreamsTransport.java | 870 ++++++ .../xmpp/jingle/transports/Transport.java | 80 + .../WebRTCDataChannelTransport.java | 617 ++++ 41 files changed, 4930 insertions(+), 2911 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/utils/Checksum.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/transports/Transport.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index a1f7d1d41..343f3376a 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -41,7 +41,7 @@ public final class Config { public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US); - public static final boolean QUICK_LOG = false; + public static final boolean QUICK_LOG = true; public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im"); public static final Uri HELP = Uri.parse("https://help.conversations.im"); @@ -117,7 +117,7 @@ public final class Config { public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true; - public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb + public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback) public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts 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 9cf3d9091..3721f4cfe 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -62,11 +62,13 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.DescriptionTransport; import eu.siacs.conversations.xmpp.jingle.OmemoVerification; import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap; import eu.siacs.conversations.xmpp.jingle.RtpContentMap; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.pep.PublishOptions; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (Config.REQUIRE_RTP_VERIFICATION) { requireVerification(session); } - final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder> 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 RtpContentMap.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); @@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) + new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) ); } return Futures.immediateFuture( @@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { - final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder> 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 RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); + for (final Map.Entry> content : omemoVerifiedRtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = content.getValue(); final OmemoVerifiedPayload decryptedTransport; try { decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); @@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) + new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) ); } processPostponed(); @@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { )); } - public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { - executor.execute(new Runnable() { - @Override - public void run() { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - onMessageCreatedCallback.run(axolotlMessage); - } else { - onMessageCreatedCallback.run(null); - } + 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) { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 706b50043..42fc3c00f 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -27,11 +27,7 @@ public abstract class AbstractGenerator { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); private final String[] FEATURES = { Namespace.JINGLE, - - //Jingle File Transfer - FileTransferDescription.Version.FT_3.getNamespace(), - FileTransferDescription.Version.FT_4.getNamespace(), - FileTransferDescription.Version.FT_5.getNamespace(), + Namespace.JINGLE_APPS_FILE_TRANSFER, Namespace.JINGLE_TRANSPORTS_S5B, Namespace.JINGLE_TRANSPORTS_IBB, Namespace.JINGLE_ENCRYPTED_TRANSPORT, @@ -124,6 +120,7 @@ public abstract class AbstractGenerator { if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); features.addAll(Arrays.asList(VOIP_NAMESPACES)); + features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL); } if (mXmppConnectionService.broadcastLastActivity()) { features.add(Namespace.IDLE); diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index d152c5d07..2562e13a6 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -403,7 +403,12 @@ public class UnifiedPushBroker { updateIntent.putExtra("token", target.instance); updateIntent.putExtra("bytesMessage", payload); updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8)); - // TODO add distributor verification? + final var distributorVerificationIntent = new Intent(); + distributorVerificationIntent.setPackage(service.getPackageName()); + final var pendingIntent = + PendingIntent.getBroadcast( + service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE); + updateIntent.putExtra("distributor", pendingIntent); service.sendBroadcast(updateIntent); } diff --git a/src/main/java/eu/siacs/conversations/utils/Checksum.java b/src/main/java/eu/siacs/conversations/utils/Checksum.java deleted file mode 100644 index 407cb1944..000000000 --- a/src/main/java/eu/siacs/conversations/utils/Checksum.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import android.util.Base64; - -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class Checksum { - - public static String md5(InputStream inputStream) throws IOException { - byte[] buffer = new byte[4096]; - MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - - int count; - do { - count = inputStream.read(buffer); - if (count > 0) { - messageDigest.update(buffer, 0, count); - } - } while (count != -1); - inputStream.close(); - return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP); - } -} diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 7ccaab739..85714c765 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -47,7 +47,11 @@ public final class Namespace { public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; + public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1"; + public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1"; public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1"; + + public static final String JINGLE_APPS_FILE_TRANSFER = "urn:xmpp:jingle:apps:file-transfer:5"; public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0"; public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; @@ -71,4 +75,5 @@ public final class Namespace { public static final String REPORTING = "urn:xmpp:reporting:1"; public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264"; + public static final String HASHES = "urn:xmpp:hashes:2"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java new file mode 100644 index 000000000..847678a05 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java @@ -0,0 +1,82 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public abstract class AbstractContentMap< + D extends GenericDescription, T extends GenericTransportInfo> { + + public final Group group; + + public final Map> contents; + + protected AbstractContentMap( + final Group group, final Map> contents) { + this.group = group; + this.contents = contents; + } + + public static class UnsupportedApplicationException extends IllegalArgumentException { + UnsupportedApplicationException(String message) { + super(message); + } + } + + public static class UnsupportedTransportException extends IllegalArgumentException { + UnsupportedTransportException(String message) { + super(message); + } + } + + public Set getSenders() { + return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders)); + } + + public List getNames() { + return ImmutableList.copyOf(contents.keySet()); + } + + JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { + final JinglePacket jinglePacket = new JinglePacket(action, sessionId); + for (final Map.Entry> entry : this.contents.entrySet()) { + final DescriptionTransport descriptionTransport = entry.getValue(); + final Content content = + new Content( + Content.Creator.INITIATOR, + descriptionTransport.senders, + entry.getKey()); + if (descriptionTransport.description != null) { + content.addChild(descriptionTransport.description); + } + content.addChild(descriptionTransport.transport); + jinglePacket.addJingleContent(content); + } + if (this.group != null) { + jinglePacket.addGroup(this.group); + } + return jinglePacket; + } + + void requireContentDescriptions() { + if (this.contents.size() == 0) { + throw new IllegalStateException("No contents available"); + } + for (final Map.Entry> entry : this.contents.entrySet()) { + if (entry.getValue().description == null) { + throw new IllegalStateException( + String.format("%s is lacking content description", entry.getKey())); + } + } + } +} 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 d719c729e..efc32f5ff 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -1,47 +1,352 @@ 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; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presence; +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.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; public abstract class AbstractJingleConnection { public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-"; public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-"; + protected static final List TERMINATED = + Arrays.asList( + State.ACCEPTED, + State.REJECTED, + State.REJECTED_RACED, + State.RETRACTED, + State.RETRACTED_RACED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR); + + private static final Map> VALID_TRANSITIONS; + + static { + final ImmutableMap.Builder> transitionBuilder = + new ImmutableMap.Builder<>(); + transitionBuilder.put( + State.NULL, + ImmutableList.of( + State.PROPOSED, + State.SESSION_INITIALIZED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( + State.PROPOSED, + ImmutableList.of( + State.ACCEPTED, + State.PROCEED, + State.REJECTED, + State.RETRACTED, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection + // rebinds + )); + transitionBuilder.put( + State.PROCEED, + ImmutableList.of( + State.REJECTED_RACED, + State.RETRACTED_RACED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.TERMINATED_SUCCESS, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR, + State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error + // bounces of the proceed message + )); + transitionBuilder.put( + State.SESSION_INITIALIZED, + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( + State.SESSION_INITIALIZED_PRE_APPROVED, + ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors + // and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + transitionBuilder.put( + State.SESSION_ACCEPTED, + ImmutableList.of( + State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_SECURITY_ERROR)); + VALID_TRANSITIONS = transitionBuilder.build(); + } + final JingleConnectionManager jingleConnectionManager; protected final XmppConnectionService xmppConnectionService; protected final Id id; private final Jid initiator; - AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) { + protected State state = State.NULL; + + AbstractJingleConnection( + final JingleConnectionManager jingleConnectionManager, + final Id id, + final Jid initiator) { this.jingleConnectionManager = jingleConnectionManager; this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService(); this.id = id; this.initiator = initiator; } - boolean isInitiator() { - return initiator.equals(id.account.getJid()); - } - - abstract void deliverPacket(JinglePacket jinglePacket); - public Id getId() { return id; } + boolean isInitiator() { + return initiator.equals(id.account.getJid()); + } + + boolean isResponder() { + return !initiator.equals(id.account.getJid()); + } + + public State getState() { + return this.state; + } + + protected synchronized boolean isInState(State... state) { + return Arrays.asList(state).contains(this.state); + } + + protected boolean transition(final State target) { + return transition(target, null); + } + + protected synchronized boolean transition(final State target, final Runnable runnable) { + final Collection validTransitions = VALID_TRANSITIONS.get(this.state); + if (validTransitions != null && validTransitions.contains(target)) { + this.state = target; + if (runnable != null) { + runnable.run(); + } + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); + return true; + } else { + return false; + } + } + + protected void transitionOrThrow(final State target) { + if (!transition(target)) { + throw new IllegalStateException( + String.format("Unable to transition from %s to %s", this.state, target)); + } + } + + boolean isTerminated() { + return TERMINATED.contains(this.state); + } + + abstract void deliverPacket(JinglePacket jinglePacket); + + protected void receiveOutOfOrderAction( + final JinglePacket jinglePacket, final JinglePacket.Action action) { + Log.d( + Config.LOGTAG, + String.format( + "%s: received %s even though we are in state %s", + id.account.getJid().asBareJid(), action, getState())); + if (isTerminated()) { + Log.d( + Config.LOGTAG, + String.format( + "%s: got a reason to terminate with out-of-order. but already in state %s", + id.account.getJid().asBareJid(), getState())); + respondWithOutOfOrder(jinglePacket); + } else { + terminateWithOutOfOrder(jinglePacket); + } + } + + protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": terminating session with out-of-order"); + terminateTransport(); + transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); + respondWithOutOfOrder(jinglePacket); + this.finish(); + } + + protected void finish() { + if (isTerminated()) { + this.jingleConnectionManager.finishConnectionOrThrow(this); + } else { + throw new AssertionError( + String.format("Unable to call finish from %s", this.state)); + } + } + + protected abstract void terminateTransport(); + abstract void notifyRebound(); + protected void sendSessionTerminate( + final Reason reason, final String text, final Consumer trigger) { + final State previous = this.state; + final State target = reasonToState(reason); + transitionOrThrow(target); + if (previous != State.NULL && trigger != null) { + trigger.accept(target); + } + final JinglePacket jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + jinglePacket.setReason(reason, text); + send(jinglePacket); + finish(); + } + + protected void send(final JinglePacket jinglePacket) { + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse); + } + + protected void respondOk(final JinglePacket jinglePacket) { + xmppConnectionService.sendIqPacket( + id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); + } + + protected void respondWithTieBreak(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); + } + + protected void respondWithOutOfOrder(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); + } + + protected void respondWithItemNotFound(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, null, "item-not-found", "cancel"); + } + + private void respondWithJingleError( + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { + jingleConnectionManager.respondWithJingleError( + id.account, original, jingleCondition, condition, conditionType); + } + + private synchronized void handleIqResponse(final Account account, final IqPacket response) { + if (response.getType() == IqPacket.TYPE.ERROR) { + handleIqErrorResponse(response); + return; + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + } + + protected void handleIqErrorResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + final String errorCondition = response.getErrorCondition(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ-error from " + + response.getFrom() + + " in RTP session. " + + errorCondition); + if (isTerminated()) { + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); + return; + } + this.terminateTransport(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout") + .contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + transitionOrThrow(target); + this.finish(); + } + + protected void handleIqTimeoutResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received IQ timeout in RTP session with " + + id.with + + ". terminating with connectivity error"); + if (isTerminated()) { + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); + return; + } + this.terminateTransport(); + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + this.finish(); + } + + protected boolean remoteHasFeature(final String feature) { + final Contact contact = id.getContact(); + final Presence presence = + contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); + final ServiceDiscoveryResult serviceDiscoveryResult = + presence == null ? null : presence.getServiceDiscoveryResult(); + final List features = + serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); + return features != null && features.contains(feature); + } public static class Id implements OngoingRtpSession { public final Account account; @@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection { return new Id( message.getConversation().getAccount(), message.getCounterpart(), - JingleConnectionManager.nextRandomId() - ); + JingleConnectionManager.nextRandomId()); } public Contact getContact() { @@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Id id = (Id) o; - return Objects.equal(account.getUuid(), id.account.getUuid()) && - Objects.equal(with, id.with) && - Objects.equal(sessionId, id.sessionId); + return Objects.equal(account.getUuid(), id.account.getUuid()) + && Objects.equal(with, id.with) + && Objects.equal(sessionId, id.sessionId); } @Override @@ -122,23 +426,36 @@ public abstract class AbstractJingleConnection { } } + protected static State reasonToState(Reason reason) { + return switch (reason) { + case SUCCESS -> State.TERMINATED_SUCCESS; + 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; + default -> State.TERMINATED_CONNECTIVITY_ERROR; + }; + } public enum State { - NULL, //default value; nothing has been sent or received yet + NULL, // default value; nothing has been sent or received yet PROPOSED, ACCEPTED, PROCEED, REJECTED, - REJECTED_RACED, //used when we want to reject but haven’t received session init yet + REJECTED_RACED, // used when we want to reject but haven’t received session init yet RETRACTED, - RETRACTED_RACED, //used when receiving a retract after we already asked to proceed - SESSION_INITIALIZED, //equal to 'PENDING' + RETRACTED_RACED, // used when receiving a retract after we already asked to proceed + SESSION_INITIALIZED, // equal to 'PENDING' SESSION_INITIALIZED_PRE_APPROVED, - SESSION_ACCEPTED, //equal to 'ACTIVE' - TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close - TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call) - TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button) - TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted + SESSION_ACCEPTED, // equal to 'ACTIVE' + TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close + TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call) + TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will + // display retry button) + TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call + // before session was accepted TERMINATED_APPLICATION_FAILURE, TERMINATED_SECURITY_ERROR } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java index 97bf802fd..ab2dffc6d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java @@ -1,5 +1,7 @@ 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.collect.Collections2; @@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet; import java.util.Set; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public final class ContentAddition { @@ -32,12 +36,13 @@ public final class ContentAddition { Collections2.transform( rtpContentMap.contents.entrySet(), e -> { - final RtpContentMap.DescriptionTransport dt = e.getValue(); + final DescriptionTransport dt = e.getValue(); return new Summary(e.getKey(), dt.description.getMedia(), dt.senders); })); } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("direction", direction) @@ -77,6 +82,7 @@ public final class ContentAddition { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("name", name) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java new file mode 100644 index 000000000..70d6c512c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java @@ -0,0 +1,19 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; + +public class DescriptionTransport { + + public final Content.Senders senders; + public final D description; + public final T transport; + + public DescriptionTransport( + final Content.Senders senders, final D description, final T transport) { + this.senders = senders; + this.description = description; + this.transport = transport; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java index 83a2b95e4..a2a5c4032 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.collect.ImmutableList; + import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; @@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid; public class DirectConnectionUtils { - private static List getLocalAddresses() { - final List addresses = new ArrayList<>(); + public static List getLocalAddresses() { + final ImmutableList.Builder inetAddresses = new ImmutableList.Builder<>(); final Enumeration interfaces; try { interfaces = NetworkInterface.getNetworkInterfaces(); - } catch (SocketException e) { - return addresses; + } catch (final SocketException e) { + return inetAddresses.build(); } while (interfaces.hasMoreElements()) { NetworkInterface networkInterface = interfaces.nextElement(); @@ -34,31 +36,15 @@ public class DirectConnectionUtils { if (inetAddress instanceof Inet6Address) { //let's get rid of scope try { - addresses.add(Inet6Address.getByAddress(inetAddress.getAddress())); + inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress())); } catch (UnknownHostException e) { //ignored } } else { - addresses.add(inetAddress); + inetAddresses.add(inetAddress); } } } - return addresses; + return inetAddresses.build(); } - - public static List getLocalCandidates(Jid jid) { - SecureRandom random = new SecureRandom(); - ArrayList candidates = new ArrayList<>(); - for (InetAddress inetAddress : getLocalAddresses()) { - final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true); - candidate.setHost(inetAddress.getHostAddress()); - candidate.setPort(random.nextInt(60000) + 1024); - candidate.setType(JingleCandidate.TYPE_DIRECT); - candidate.setJid(jid); - candidate.setPriority(8257536 + candidates.size()); - candidates.add(candidate); - } - return candidates; - } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java new file mode 100644 index 000000000..c678c91cb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java @@ -0,0 +1,219 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; +import eu.siacs.conversations.xmpp.jingle.transports.Transport; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FileTransferContentMap + extends AbstractContentMap { + + private static final List> SUPPORTED_TRANSPORTS = + Arrays.asList( + SocksByteStreamsTransportInfo.class, + IbbTransportInfo.class, + WebRTCDataChannelTransportInfo.class); + + protected FileTransferContentMap( + final Group group, final Map> + contents) { + super(group, contents); + } + + public static FileTransferContentMap of(final JinglePacket jinglePacket) { + final Map> + contents = of(jinglePacket.getJingleContents()); + return new FileTransferContentMap(jinglePacket.getGroup(), contents); + } + + public static DescriptionTransport of( + final Content content) { + final GenericDescription description = content.getDescription(); + final GenericTransportInfo transportInfo = content.getTransport(); + final Content.Senders senders = content.getSenders(); + final FileTransferDescription fileTransferDescription; + if (description == null) { + fileTransferDescription = null; + } else if (description instanceof FileTransferDescription ftDescription) { + fileTransferDescription = ftDescription; + } else { + throw new UnsupportedApplicationException( + "Content does not contain file transfer description"); + } + if (!SUPPORTED_TRANSPORTS.contains(transportInfo.getClass())) { + throw new UnsupportedTransportException("Content does not have supported transport"); + } + return new DescriptionTransport<>(senders, fileTransferDescription, transportInfo); + } + + private static Map> + of(final Map contents) { + return ImmutableMap.copyOf( + Maps.transformValues(contents, content -> content == null ? null : of(content))); + } + + public static FileTransferContentMap of( + final FileTransferDescription.File file, final Transport.InitialTransportInfo initialTransportInfo) { + // TODO copy groups + final var transportInfo = initialTransportInfo.transportInfo; + return new FileTransferContentMap(initialTransportInfo.group, + Map.of( + initialTransportInfo.contentName, + new DescriptionTransport<>( + Content.Senders.INITIATOR, + FileTransferDescription.of(file), + transportInfo))); + } + + public FileTransferDescription.File requireOnlyFile() { + if (this.contents.size() != 1) { + throw new IllegalStateException("Only one file at a time is supported"); + } + final var dt = Iterables.getOnlyElement(this.contents.values()); + return dt.description.getFile(); + } + + public FileTransferDescription requireOnlyFileTransferDescription() { + if (this.contents.size() != 1) { + throw new IllegalStateException("Only one file at a time is supported"); + } + final var dt = Iterables.getOnlyElement(this.contents.values()); + return dt.description; + } + + public GenericTransportInfo requireOnlyTransportInfo() { + if (this.contents.size() != 1) { + throw new IllegalStateException( + "We expect exactly one content with one transport info"); + } + final var dt = Iterables.getOnlyElement(this.contents.values()); + return dt.transport; + } + + public FileTransferContentMap withTransport(final Transport.TransportInfo transportWrapper) { + final var transportInfo = transportWrapper.transportInfo; + return new FileTransferContentMap(transportWrapper.group, + ImmutableMap.copyOf( + Maps.transformValues( + contents, + content -> { + if (content == null) { + return null; + } + return new DescriptionTransport<>( + content.senders, content.description, transportInfo); + }))); + } + + public FileTransferContentMap candidateUsed(final String streamId, final String cid) { + return new FileTransferContentMap(null, + ImmutableMap.copyOf( + Maps.transformValues( + contents, + content -> { + if (content == null) { + return null; + } + final var transportInfo = + new SocksByteStreamsTransportInfo( + streamId, Collections.emptyList()); + final Element candidateUsed = + transportInfo.addChild( + "candidate-used", + Namespace.JINGLE_TRANSPORTS_S5B); + candidateUsed.setAttribute("cid", cid); + return new DescriptionTransport<>( + content.senders, null, transportInfo); + }))); + } + + public FileTransferContentMap candidateError(final String streamId) { + return new FileTransferContentMap(null, + ImmutableMap.copyOf( + Maps.transformValues( + contents, + content -> { + if (content == null) { + return null; + } + final var transportInfo = + new SocksByteStreamsTransportInfo( + streamId, Collections.emptyList()); + transportInfo.addChild( + "candidate-error", Namespace.JINGLE_TRANSPORTS_S5B); + return new DescriptionTransport<>( + content.senders, null, transportInfo); + }))); + } + + public FileTransferContentMap proxyActivated(final String streamId, final String cid) { + return new FileTransferContentMap(null, + ImmutableMap.copyOf( + Maps.transformValues( + contents, + content -> { + if (content == null) { + return null; + } + final var transportInfo = + new SocksByteStreamsTransportInfo( + streamId, Collections.emptyList()); + final Element candidateUsed = + transportInfo.addChild( + "activated", Namespace.JINGLE_TRANSPORTS_S5B); + candidateUsed.setAttribute("cid", cid); + return new DescriptionTransport<>( + content.senders, null, transportInfo); + }))); + } + + FileTransferContentMap transportInfo() { + return new FileTransferContentMap(this.group, + Maps.transformValues( + contents, + dt -> new DescriptionTransport<>(dt.senders, null, dt.transport))); + } + + FileTransferContentMap transportInfo( + final String contentName, final IceUdpTransportInfo.Candidate candidate) { + final DescriptionTransport descriptionTransport = + contents.get(contentName); + if (descriptionTransport == null) { + throw new IllegalArgumentException( + "Unable to find transport info for content name " + contentName); + } + final WebRTCDataChannelTransportInfo transportInfo; + if (descriptionTransport.transport instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) { + transportInfo = webRTCDataChannelTransportInfo; + } else { + throw new IllegalStateException("TransportInfo is not WebRTCDataChannel"); + } + final WebRTCDataChannelTransportInfo newTransportInfo = transportInfo.cloneWrapper(); + newTransportInfo.addCandidate(candidate); + return new FileTransferContentMap( + null, + ImmutableMap.of( + contentName, + new DescriptionTransport<>( + descriptionTransport.senders, null, newTransportInfo))); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java new file mode 100644 index 000000000..7b2f88457 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -0,0 +1,98 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.util.Log; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.IP; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import org.webrtc.PeerConnection; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class IceServers { + + public static List parse(final IqPacket response) { + ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element services = + response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = + services == null ? Collections.emptyList() : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String sport = child.getAttribute("port"); + final Integer port = sport == null ? null : Ints.tryParse(sport); + final String transport = child.getAttribute("transport"); + final String username = child.getAttribute("username"); + final String password = child.getAttribute("password"); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + + if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) + && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns", "turns").contains(type) + && "udp".equals(transport)) { + Log.w( + Config.LOGTAG, + "skipping invalid combination of udp/tls in external services"); + continue; + } + + // STUN URLs do not support a query section since M110 + final String uri; + if (Arrays.asList("stun", "stuns").contains(type)) { + uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port); + } else { + uri = + String.format( + "%s:%s:%s?transport=%s", + type, IP.wrapIPv6(host), port, transport); + } + + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder(uri); + iceServerBuilder.setTlsCertPolicy( + PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + // The WebRTC spec requires throwing an + // InvalidAccessError when username (from libwebrtc + // source coder) + // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.w( + Config.LOGTAG, + "skipping " + + type + + "/" + + transport + + " without username and password"); + continue; + } + final PeerConnection.IceServer iceServer = + iceServerBuilder.createIceServer(); + Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); + listBuilder.add(iceServer); + } + } + } + } + return listBuilder.build(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java deleted file mode 100644 index 78ffb28be..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ /dev/null @@ -1,152 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; - -public class JingleCandidate { - - public static int TYPE_UNKNOWN; - public static int TYPE_DIRECT = 0; - public static int TYPE_PROXY = 1; - - private final boolean ours; - private boolean usedByCounterpart = false; - private final String cid; - private String host; - private int port; - private int type; - private Jid jid; - private int priority; - - public JingleCandidate(String cid, boolean ours) { - this.ours = ours; - this.cid = cid; - } - - public String getCid() { - return cid; - } - - public void setHost(String host) { - this.host = host; - } - - public String getHost() { - return this.host; - } - - public void setJid(final Jid jid) { - this.jid = jid; - } - - public Jid getJid() { - return this.jid; - } - - public void setPort(int port) { - this.port = port; - } - - public int getPort() { - return this.port; - } - - public void setType(int type) { - this.type = type; - } - - public void setType(String type) { - if (type == null) { - this.type = TYPE_UNKNOWN; - return; - } - switch (type) { - case "proxy": - this.type = TYPE_PROXY; - break; - case "direct": - this.type = TYPE_DIRECT; - break; - default: - this.type = TYPE_UNKNOWN; - break; - } - } - - public void setPriority(int i) { - this.priority = i; - } - - public int getPriority() { - return this.priority; - } - - public boolean equals(JingleCandidate other) { - return this.getCid().equals(other.getCid()); - } - - public boolean equalValues(JingleCandidate other) { - return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort()); - } - - public boolean isOurs() { - return ours; - } - - public int getType() { - return this.type; - } - - public static List parse(final List elements) { - final List candidates = new ArrayList<>(); - for (final Element element : elements) { - if ("candidate".equals(element.getName())) { - candidates.add(JingleCandidate.parse(element)); - } - } - return candidates; - } - - public static JingleCandidate parse(Element element) { - final JingleCandidate candidate = new JingleCandidate(element.getAttribute("cid"), false); - candidate.setHost(element.getAttribute("host")); - candidate.setJid(InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid"))); - candidate.setType(element.getAttribute("type")); - candidate.setPriority(Integer.parseInt(element.getAttribute("priority"))); - candidate.setPort(Integer.parseInt(element.getAttribute("port"))); - return candidate; - } - - public Element toElement() { - Element element = new Element("candidate"); - element.setAttribute("cid", this.getCid()); - element.setAttribute("host", this.getHost()); - element.setAttribute("port", Integer.toString(this.getPort())); - if (jid != null) { - element.setAttribute("jid", jid); - } - element.setAttribute("priority", Integer.toString(this.getPriority())); - if (this.getType() == TYPE_DIRECT) { - element.setAttribute("type", "direct"); - } else if (this.getType() == TYPE_PROXY) { - element.setAttribute("type", "proxy"); - } - return element; - } - - public void flagAsUsedByCounterpart() { - this.usedByCounterpart = true; - } - - public boolean isUsedByCounterpart() { - return this.usedByCounterpart; - } - - public String toString() { - return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs()); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index e334860bc..23d4f175b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -29,10 +29,13 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport; +import eu.siacs.conversations.xmpp.jingle.transports.Transport; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -61,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { private final Cache terminatedSessions = CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build(); - private final HashMap primaryCandidates = new HashMap<>(); - public JingleConnectionManager(XmppConnectionService service) { super(service); this.toneManager = new ToneManager(service); @@ -90,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; - if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { + if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && isUsingClearNet(account)) { @@ -593,13 +594,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (old != null) { old.cancel(); } - final Account account = message.getConversation().getAccount(); - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message); final JingleFileTransferConnection connection = - new JingleFileTransferConnection(this, id, account.getJid()); - mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); - this.connections.put(id, connection); - connection.init(message); + new JingleFileTransferConnection(this, message); + this.connections.put(connection.getId(), connection); + connection.sendSessionInitialize(); } public Optional getOngoingRtpConnection(final Contact contact) { @@ -658,60 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { return firedUpdates; } - void getPrimaryCandidate( - final Account account, - final boolean initiator, - final OnPrimaryCandidateFound listener) { - if (Config.DISABLE_PROXY_LOOKUP) { - listener.onPrimaryCandidateFound(false, null); - return; - } - if (this.primaryCandidates.containsKey(account.getJid().asBareJid())) { - listener.onPrimaryCandidateFound( - true, this.primaryCandidates.get(account.getJid().asBareJid())); - return; - } - - final Jid proxy = - account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); - if (proxy == null) { - listener.onPrimaryCandidateFound(false, null); - return; - } - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(proxy); - iq.query(Namespace.BYTE_STREAMS); - account.getXmppConnection() - .sendIqPacket( - iq, - (a, response) -> { - final Element streamhost = - response.query() - .findChild("streamhost", Namespace.BYTE_STREAMS); - final String host = - streamhost == null ? null : streamhost.getAttribute("host"); - final String port = - streamhost == null ? null : streamhost.getAttribute("port"); - if (host != null && port != null) { - try { - JingleCandidate candidate = - new JingleCandidate(nextRandomId(), true); - candidate.setHost(host); - candidate.setPort(Integer.parseInt(port)); - candidate.setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(proxy); - candidate.setPriority(655360 + (initiator ? 30 : 0)); - primaryCandidates.put(a.getJid().asBareJid(), candidate); - listener.onPrimaryCandidateFound(true, candidate); - } catch (final NumberFormatException e) { - listener.onPrimaryCandidateFound(false, null); - } - } else { - listener.onPrimaryCandidateFound(false, null); - } - }); - } - public void retractSessionProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { RtpSessionProposal matchingProposal = null; @@ -810,36 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager { return false; } - public void deliverIbbPacket(Account account, IqPacket packet) { + public void deliverIbbPacket(final Account account, final IqPacket packet) { final String sid; final Element payload; + final InbandBytestreamsTransport.PacketType packetType; if (packet.hasChild("open", Namespace.IBB)) { + packetType = InbandBytestreamsTransport.PacketType.OPEN; payload = packet.findChild("open", Namespace.IBB); sid = payload.getAttribute("sid"); } else if (packet.hasChild("data", Namespace.IBB)) { + packetType = InbandBytestreamsTransport.PacketType.DATA; payload = packet.findChild("data", Namespace.IBB); sid = payload.getAttribute("sid"); } else if (packet.hasChild("close", Namespace.IBB)) { + packetType = InbandBytestreamsTransport.PacketType.CLOSE; payload = packet.findChild("close", Namespace.IBB); sid = payload.getAttribute("sid"); } else { + packetType = null; payload = null; sid = null; } - if (sid != null) { - for (final AbstractJingleConnection connection : this.connections.values()) { - if (connection instanceof JingleFileTransferConnection fileTransfer) { - final JingleTransport transport = fileTransfer.getTransport(); - if (transport instanceof JingleInBandTransport inBandTransport) { - if (inBandTransport.matches(account, sid)) { - inBandTransport.deliverPayload(packet, payload); + if (sid == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid"); + account.getXmppConnection() + .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + return; + } + for (final AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleFileTransferConnection fileTransfer) { + final Transport transport = fileTransfer.getTransport(); + if (transport instanceof InbandBytestreamsTransport inBandTransport) { + if (sid.equals(inBandTransport.getStreamId())) { + if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) { + account.getXmppConnection() + .sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + } else { + account.getXmppConnection() + .sendIqPacket( + packet.generateResponse(IqPacket.TYPE.ERROR), null); } return; } } } } - Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet); + Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid); account.getXmppConnection() .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } 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 c4ed04bd0..983ce433f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -1,1222 +1,303 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Base64; import android.util.Log; +import androidx.annotation.NonNull; + import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.Collections2; -import com.google.common.collect.FluentIterable; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; +import com.google.common.hash.Hashing; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; -import eu.siacs.conversations.xmpp.jingle.stanzas.S5BTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; +import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport; +import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport; +import eu.siacs.conversations.xmpp.jingle.transports.Transport; +import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport; import eu.siacs.conversations.xmpp.stanzas.IqPacket; -public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable { +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 org.webrtc.IceCandidate; - private static final int JINGLE_STATUS_TRANSMITTING = 5; - private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; - private static final int JINGLE_STATUS_INITIATED = 0; - private static final int JINGLE_STATUS_ACCEPTED = 1; - private static final int JINGLE_STATUS_FINISHED = 4; - private static final int JINGLE_STATUS_FAILED = 99; - private static final int JINGLE_STATUS_OFFERED = -1; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; - private static final int MAX_IBB_BLOCK_SIZE = 8192; +public class JingleFileTransferConnection extends AbstractJingleConnection + implements Transport.Callback, Transferable { - private int ibbBlockSize = MAX_IBB_BLOCK_SIZE; + private final Message message; - private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum - private int mStatus = Transferable.STATUS_UNKNOWN; - private Message message; - private Jid responder; - private final List candidates = new ArrayList<>(); - private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); + private FileTransferContentMap initiatorFileTransferContentMap; + private FileTransferContentMap responderFileTransferContentMap; - private String transportId; - private FileTransferDescription description; - private DownloadableFile file = null; - - private boolean proxyActivationFailed = false; - - private String contentName; - private Content.Creator contentCreator; - private Content.Senders contentSenders; - private Class initialTransport; - private boolean remoteSupportsOmemoJet; - - private int mProgress = 0; - - private boolean receivedCandidate = false; - private boolean sentCandidate = false; + private Transport transport; + private TransportSecurity transportSecurity; + private AbstractFileTransceiver fileTransceiver; + private final Queue pendingIncomingIceCandidates = new LinkedList<>(); private boolean acceptedAutomatically = false; - private boolean cancelled = false; - private XmppAxolotlMessage mXmppAxolotlMessage; - - private JingleTransport transport = null; - - private OutputStream mFileOutputStream; - private InputStream mFileInputStream; - - private final OnIqPacketReceived responseListener = (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) { - fail(IqParser.extractErrorMessage(packet)); - } else { - Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString()); - } - } - }; - private byte[] expectedHash = new byte[0]; - private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() { - - @Override - public void onFileTransmitted(DownloadableFile file) { - if (responding()) { - if (expectedHash.length > 0) { - if (Arrays.equals(expectedHash, file.getSha1Sum())) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash"); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match"); - } - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer"); - } - sendSuccess(); - xmppConnectionService.getFileBackend().updateFileParams(message); - xmppConnectionService.databaseBackend.createMessage(message); - xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); - if (acceptedAutomatically) { - message.markUnread(); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - id.account.getPgpDecryptionService().decrypt(message, true); - } else { - xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message)); - - } - Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")"); - return; - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - id.account.getPgpDecryptionService().decrypt(message, true); - } - } else { - if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info - sendHash(); - } - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - id.account.getPgpDecryptionService().decrypt(message, false); - } - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - file.delete(); - } - } - Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")"); - if (message.getEncryption() != Message.ENCRYPTION_PGP) { - xmppConnectionService.getFileBackend().updateMediaScanner(file); - } - } - - @Override - public void onFileTransferAborted() { - JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR); - JingleFileTransferConnection.this.fail(); - } - }; - private final OnTransportConnected onIbbTransportConnected = new OnTransportConnected() { - @Override - public void failed() { - Log.d(Config.LOGTAG, "ibb open failed"); - } - - @Override - public void established() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file"); - mJingleStatus = JINGLE_STATUS_TRANSMITTING; - JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged); - } - }; - private final OnProxyActivated onProxyActivated = new OnProxyActivated() { - - @Override - public void success() { - if (isInitiator()) { - Log.d(Config.LOGTAG, "we were initiating. sending file"); - transport.send(file, onFileTransmissionStatusChanged); - } else { - transport.receive(file, onFileTransmissionStatusChanged); - Log.d(Config.LOGTAG, "we were responding. receiving file"); - } - } - - @Override - public void failed() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed"); - proxyActivationFailed = true; - if (isInitiator()) { - sendFallbackToIbb(); - } - } - }; - - JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { - super(jingleConnectionManager, id, initiator); - } - - private static long parseLong(final Element element, final long l) { - final String input = element == null ? null : element.getContent(); - if (input == null) { - return l; - } - try { - return Long.parseLong(input); - } catch (Exception e) { - return l; - } - } - - //TODO get rid and use isInitiator() instead - private boolean responding() { - return responder != null && responder.equals(id.account.getJid()); - } - - - InputStream getFileInputStream() { - return this.mFileInputStream; - } - - OutputStream getFileOutputStream() throws IOException { - if (this.file == null) { - Log.d(Config.LOGTAG, "file object was not assigned"); - return null; - } - final File parent = this.file.getParentFile(); - if (parent != null && parent.mkdirs()) { - Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath()); - } - if (this.file.createNewFile()) { - Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath()); - } - this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true); - return this.mFileOutputStream; - } - - @Override - void deliverPacket(final JinglePacket packet) { - final JinglePacket.Action action = packet.getAction(); - //TODO switch case - if (action == JinglePacket.Action.SESSION_INITIATE) { - init(packet); - } else if (action == JinglePacket.Action.SESSION_TERMINATE) { - final Reason reason = packet.getReason().reason; - switch (reason) { - case CANCEL: - this.cancelled = true; - this.fail(); - break; - case SUCCESS: - this.receiveSuccess(); - break; - default: - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason); - this.fail(); - break; - - } - } else if (action == JinglePacket.Action.SESSION_ACCEPT) { - receiveAccept(packet); - } else if (action == JinglePacket.Action.SESSION_INFO) { - final Element checksum = packet.getJingleChild("checksum"); - final Element file = checksum == null ? null : checksum.findChild("file"); - final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2"); - if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) { - try { - this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT); - } catch (Exception e) { - this.expectedHash = new byte[0]; - } - } - respondToIq(packet, true); - } else if (action == JinglePacket.Action.TRANSPORT_INFO) { - receiveTransportInfo(packet); - } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) { - final Content content = packet.getJingleContent(); - final GenericTransportInfo transportInfo = content == null ? null : content.getTransport(); - if (transportInfo instanceof IbbTransportInfo) { - receiveFallbackToIbb(packet, (IbbTransportInfo) transportInfo); - } else { - Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString()); - respondToIq(packet, false); - } - } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) { - receiveTransportAccept(packet); - } else { - Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction()); - respondToIq(packet, false); - } - } - - @Override - void notifyRebound() { - if (getJingleStatus() == JINGLE_STATUS_TRANSMITTING) { - abort(Reason.CONNECTIVITY_ERROR); - } - } - - private void respondToIq(final IqPacket packet, final boolean result) { - final IqPacket response; - if (result) { - response = packet.generateResponse(IqPacket.TYPE.RESULT); - } else { - response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error").setAttribute("type", "cancel"); - error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas"); - } - xmppConnectionService.sendIqPacket(id.account, response, null); - } - - private void respondToIqWithOutOfOrder(final IqPacket packet) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error").setAttribute("type", "wait"); - error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("out-of-order", "urn:xmpp:jingle:errors:1"); - xmppConnectionService.sendIqPacket(id.account, response, null); - } - - public void init(final Message message) { - Preconditions.checkArgument(message.isFileOrImage()); - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - Conversation conversation = (Conversation) message.getConversation(); - conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> { - if (xmppAxolotlMessage != null) { - init(message, xmppAxolotlMessage); - } else { - fail(); - } - }); - } else { - init(message, null); - } - } - - private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) { - this.mXmppAxolotlMessage = xmppAxolotlMessage; - this.contentCreator = Content.Creator.INITIATOR; - this.contentSenders = Content.Senders.INITIATOR; - this.contentName = JingleConnectionManager.nextRandomId(); + public JingleFileTransferConnection( + final JingleConnectionManager jingleConnectionManager, final Message message) { + super( + jingleConnectionManager, + AbstractJingleConnection.Id.of(message), + message.getConversation().getAccount().getJid()); + Preconditions.checkArgument( + message.isFileOrImage(), + "only file or images messages can be transported via jingle"); this.message = message; - final List remoteFeatures = getRemoteFeatures(); - final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures); - this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? S5BTransportInfo.class : IbbTransportInfo.class; - this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); - this.mStatus = Transferable.STATUS_UPLOADING; - this.responder = this.id.with; - this.transportId = JingleConnectionManager.nextRandomId(); - this.setupDescription(remoteVersion); - if (this.initialTransport == IbbTransportInfo.class) { - this.sendInitRequest(); - } else { - gatherAndConnectDirectCandidates(); - this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> { - if (success) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); - connections.put(candidate.getCid(), socksConnection); - socksConnection.connect(new OnTransportConnected() { - - @Override - public void failed() { - Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort())); - sendInitRequest(); - } - - @Override - public void established() { - Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate"); - mergeCandidate(candidate); - sendInitRequest(); - } - }); - mergeCandidate(candidate); - } else { - Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found"); - sendInitRequest(); - } - }); - } - + xmppConnectionService.markMessage(message, Message.STATUS_WAITING); } - private void gatherAndConnectDirectCandidates() { - final List directCandidates; - if (Config.USE_DIRECT_JINGLE_CANDIDATES) { - if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) { - directCandidates = Collections.emptyList(); - } else { - directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid()); - } - } else { - directCandidates = Collections.emptyList(); - } - for (JingleCandidate directCandidate : directCandidates) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate); - connections.put(directCandidate.getCid(), socksConnection); - candidates.add(directCandidate); - } - } - - private FileTransferDescription.Version getAvailableFileTransferVersion(List remoteFeatures) { - if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) { - return FileTransferDescription.Version.FT_5; - } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) { - return FileTransferDescription.Version.FT_4; - } else { - return FileTransferDescription.Version.FT_3; - } - } - - private List getRemoteFeatures() { - final String resource = Strings.nullToEmpty(this.id.with.getResource()); - final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource); - final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null; - return result == null ? Collections.emptyList() : result.getFeatures(); - } - - private void init(JinglePacket packet) { //should move to deliverPacket - //TODO if not 'OFFERED' reply with out-of-order - this.mJingleStatus = JINGLE_STATUS_INITIATED; - final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false); + public JingleFileTransferConnection( + final JingleConnectionManager jingleConnectionManager, + final Id id, + final Jid initiator) { + super(jingleConnectionManager, id, initiator); + final Conversation conversation = + this.xmppConnectionService.findOrCreateConversation( + id.account, id.with.asBareJid(), false, false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message.setStatus(Message.STATUS_RECEIVED); - this.mStatus = Transferable.STATUS_OFFER; + this.message.setErrorMessage(null); this.message.setTransferable(this); - this.message.setCounterpart(this.id.with); - this.responder = this.id.account.getJid(); - final Content content = packet.getJingleContent(); - final GenericTransportInfo transportInfo = content.getTransport(); - this.contentCreator = content.getCreator(); - Content.Senders senders; - try { - senders = content.getSenders(); - } catch (final Exception e) { - senders = Content.Senders.INITIATOR; - } - this.contentSenders = senders; - this.contentName = content.getAttribute("name"); - - if (transportInfo instanceof S5BTransportInfo) { - final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; - this.transportId = s5BTransportInfo.getTransportId(); - this.initialTransport = s5BTransportInfo.getClass(); - this.mergeCandidates(s5BTransportInfo.getCandidates()); - } else if (transportInfo instanceof IbbTransportInfo) { - final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; - this.initialTransport = ibbTransportInfo.getClass(); - this.transportId = ibbTransportInfo.getTransportId(); - final int remoteBlockSize = ibbTransportInfo.getBlockSize(); - if (remoteBlockSize <= 0) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party requested invalid ibb block size"); - respondToIq(packet, false); - this.fail(); - } - this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, ibbTransportInfo.getBlockSize()); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote tried to use unknown transport " + transportInfo.getNamespace()); - respondToIq(packet, false); - this.fail(); - return; - } - - this.description = (FileTransferDescription) content.getDescription(); - - final Element fileOffer = this.description.getFileOffer(); - - if (fileOffer != null) { - boolean remoteIsUsingJet = false; - Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX); - if (encrypted == null) { - final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); - if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET"); - encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX); - remoteIsUsingJet = true; - } - } - if (encrypted != null) { - this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid()); - } - Element fileSize = fileOffer.findChild("size"); - final String path = fileOffer.findChildContent("name"); - if (path != null) { - AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path); - if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) { - message.setType(Message.TYPE_IMAGE); - xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main); - } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { - if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) { - message.setType(Message.TYPE_IMAGE); - xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary); - } else { - message.setType(Message.TYPE_FILE); - xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : "")); - } - message.setEncryption(Message.ENCRYPTION_PGP); - } else { - message.setType(Message.TYPE_FILE); - xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : "")); - } - long size = parseLong(fileSize, 0); - message.setBody(Long.toString(size)); - conversation.add(message); - jingleConnectionManager.updateConversationUi(true); - this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); - if (mXmppAxolotlMessage != null) { - XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false); - if (transportMessage != null) { - message.setEncryption(Message.ENCRYPTION_AXOLOTL); - this.file.setKey(transportMessage.getKey()); - this.file.setIv(transportMessage.getIv()); - message.setFingerprint(transportMessage.getFingerprint()); - } else { - Log.d(Config.LOGTAG, "could not process KeyTransportMessage"); - } - } - message.resetFileParams(); - //legacy OMEMO encrypted file transfers reported the file size after encryption - //JET reports the plain text size. however lower levels of our receiving code still - //expect the cipher text size. so we just + 16 bytes (auth tag size) here - this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0)); - - respondToIq(packet, true); - - if (id.account.getRoster().getContact(id.with).showInContactList() - && jingleConnectionManager.hasStoragePermission() - && size < this.jingleConnectionManager.getAutoAcceptFileSize() - && xmppConnectionService.isDataSaverDisabled()) { - Log.d(Config.LOGTAG, "auto accepting file from " + id.with); - this.acceptedAutomatically = true; - this.sendAccept(); - } else { - message.markUnread(); - Log.d(Config.LOGTAG, - "not auto accepting new file offer with size: " - + size - + " allowed size:" - + this.jingleConnectionManager - .getAutoAcceptFileSize()); - this.xmppConnectionService.getNotificationService().push(message); - } - Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); - return; - } - respondToIq(packet, false); - } - } - - private void setupDescription(final FileTransferDescription.Version version) { - this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); - final FileTransferDescription description; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - this.file.setKey(mXmppAxolotlMessage.getInnerKey()); - this.file.setIv(mXmppAxolotlMessage.getIV()); - //legacy OMEMO encrypted file transfer reported file size of the encrypted file - //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag) - this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16)); - if (remoteSupportsOmemoJet) { - description = FileTransferDescription.of(this.file, version, null); - } else { - description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage); - } - } else { - this.file.setExpectedSize(file.getSize()); - description = FileTransferDescription.of(this.file, version, null); - } - this.description = description; - } - - private void sendInitRequest() { - final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); - final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); - final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); - security.setAttribute("name", this.contentName); - security.setAttribute("cipher", JET_OMEMO_CIPHER); - security.setAttribute("type", AxolotlService.PEP_PREFIX); - security.addChild(mXmppAxolotlMessage.toElement()); - content.addChild(security); - } - content.setDescription(this.description); - message.resetFileParams(); - try { - this.mFileInputStream = new FileInputStream(file); - } catch (FileNotFoundException e) { - fail(e.getMessage()); - return; - } - if (this.initialTransport == IbbTransportInfo.class) { - content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer"); - } else { - final Collection candidates = getOurCandidates(); - content.setTransport(new S5BTransportInfo(this.transportId, candidates)); - Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); - } - packet.addJingleContent(content); - this.sendJinglePacket(packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); - if (mJingleStatus == JINGLE_STATUS_OFFERED) { - mJingleStatus = JINGLE_STATUS_INITIATED; - xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); - } else { - Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus); - } - } else { - fail(IqParser.extractErrorMessage(response)); - } - }); - - } - - private void sendHash() { - final Element checksum = new Element("checksum", description.getVersion().getNamespace()); - checksum.setAttribute("creator", "initiator"); - checksum.setAttribute("name", "a-file-offer"); - Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2"); - hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP)); - - final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO); - packet.addJingleChild(checksum); - xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)"); - } - }); - } - - private Collection getOurCandidates() { - return Collections2.filter(this.candidates, c -> c != null && c.isOurs()); - } - - private void sendAccept() { - mJingleStatus = JINGLE_STATUS_ACCEPTED; - this.mStatus = Transferable.STATUS_DOWNLOADING; - this.jingleConnectionManager.updateConversationUi(true); - if (initialTransport == S5BTransportInfo.class) { - sendAcceptSocks(); - } else { - sendAcceptIbb(); - } - } - - private void sendAcceptSocks() { - gatherAndConnectDirectCandidates(); - this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { - final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentSenders, contentName); - content.setDescription(this.description); - if (success && candidate != null && !equalCandidateExists(candidate)) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); - connections.put(candidate.getCid(), socksConnection); - socksConnection.connect(new OnTransportConnected() { - - @Override - public void failed() { - Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); - content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.addJingleContent(content); - sendJinglePacket(packet); - connectNextCandidate(); - } - - @Override - public void established() { - Log.d(Config.LOGTAG, "connected to proxy65 candidate"); - mergeCandidate(candidate); - content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.addJingleContent(content); - sendJinglePacket(packet); - connectNextCandidate(); - } - }); - } else { - Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves"); - content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.addJingleContent(content); - sendJinglePacket(packet); - connectNextCandidate(); - } - }); - } - - private void sendAcceptIbb() { - this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); - final Content content = new Content(contentCreator, contentSenders, contentName); - content.setDescription(this.description); - content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - packet.addJingleContent(content); - this.transport.receive(file, onFileTransmissionStatusChanged); - this.sendJinglePacket(packet); - } - - private JinglePacket bootstrapPacket(JinglePacket.Action action) { - final JinglePacket packet = new JinglePacket(action, this.id.sessionId); - packet.setTo(id.with); - return packet; - } - - private void sendJinglePacket(JinglePacket packet) { - xmppConnectionService.sendIqPacket(id.account, packet, responseListener); - } - - private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) { - xmppConnectionService.sendIqPacket(id.account, packet, callback); - } - - private void receiveAccept(JinglePacket packet) { - if (responding()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)"); - respondToIqWithOutOfOrder(packet); - return; - } - if (this.mJingleStatus != JINGLE_STATUS_INITIATED) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept"); - respondToIqWithOutOfOrder(packet); - return; - } - this.mJingleStatus = JINGLE_STATUS_ACCEPTED; - xmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - final Content content = packet.getJingleContent(); - final GenericTransportInfo transportInfo = content.getTransport(); - //TODO we want to fail if transportInfo doesn’t match our intialTransport and/or our id - if (transportInfo instanceof S5BTransportInfo) { - final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; - respondToIq(packet, true); - //TODO calling merge is probably a bug because that might eliminate candidates of the other party and lead to us not sending accept/deny - //TODO: we probably just want to call add - mergeCandidates(s5BTransportInfo.getCandidates()); - this.connectNextCandidate(); - } else if (transportInfo instanceof IbbTransportInfo) { - final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; - final int remoteBlockSize = ibbTransportInfo.getBlockSize(); - if (remoteBlockSize > 0) { - this.ibbBlockSize = Math.min(ibbBlockSize, remoteBlockSize); - } - respondToIq(packet, true); - this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - this.transport.connect(onIbbTransportConnected); - } else { - respondToIq(packet, false); - } - } - - private void receiveTransportInfo(JinglePacket packet) { - final Content content = packet.getJingleContent(); - final GenericTransportInfo transportInfo = content.getTransport(); - if (transportInfo instanceof S5BTransportInfo) { - final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; - if (s5BTransportInfo.hasChild("activated")) { - respondToIq(packet, true); - if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) { - onProxyActivated.success(); - } else { - String cid = s5BTransportInfo.findChild("activated").getAttribute("cid"); - Log.d(Config.LOGTAG, "received proxy activated (" + cid - + ")prior to choosing our own transport"); - JingleSocks5Transport connection = this.connections.get(cid); - if (connection != null) { - connection.setActivated(true); - } else { - Log.d(Config.LOGTAG, "activated connection not found"); - sendSessionTerminate(Reason.FAILED_TRANSPORT); - this.fail(); - } - } - } else if (s5BTransportInfo.hasChild("proxy-error")) { - respondToIq(packet, true); - onProxyActivated.failed(); - } else if (s5BTransportInfo.hasChild("candidate-error")) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error"); - respondToIq(packet, true); - this.receivedCandidate = true; - if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { - this.connect(); - } - } else if (s5BTransportInfo.hasChild("candidate-used")) { - String cid = s5BTransportInfo.findChild("candidate-used").getAttribute("cid"); - if (cid != null) { - Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); - JingleCandidate candidate = getCandidate(cid); - if (candidate == null) { - Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid); - respondToIq(packet, false); - return; - } - respondToIq(packet, true); - candidate.flagAsUsedByCounterpart(); - this.receivedCandidate = true; - if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { - this.connect(); - } else { - Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate); - } - } else { - respondToIq(packet, false); - } - } else { - respondToIq(packet, false); - } - } else { - respondToIq(packet, true); - } - } - - private void connect() { - final JingleSocks5Transport connection = chooseConnection(); - this.transport = connection; - if (connection == null) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate"); - this.disconnectSocks5Connections(); - if (isInitiator()) { - this.sendFallbackToIbb(); - } - } else { - //TODO at this point we can already close other connections to free some resources - final JingleCandidate candidate = connection.getCandidate(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString()); - this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; - if (connection.needsActivation()) { - if (connection.getCandidate().isOurs()) { - final String sid; - if (description.getVersion() == FileTransferDescription.Version.FT_3) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy"); - sid = id.sessionId; - } else { - sid = getTransportId(); - } - Log.d(Config.LOGTAG, "candidate " - + connection.getCandidate().getCid() - + " was our proxy. going to activate"); - IqPacket activation = new IqPacket(IqPacket.TYPE.SET); - activation.setTo(connection.getCandidate().getJid()); - activation.query("http://jabber.org/protocol/bytestreams") - .setAttribute("sid", sid); - activation.query().addChild("activate") - .setContent(this.id.with.toEscapedString()); - xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString()); - sendProxyError(); - onProxyActivated.failed(); - } else { - sendProxyActivated(connection.getCandidate().getCid()); - onProxyActivated.success(); - } - }); - } else { - Log.d(Config.LOGTAG, - "candidate " - + connection.getCandidate().getCid() - + " was a proxy. waiting for other party to activate"); - } - } else { - if (isInitiator()) { - Log.d(Config.LOGTAG, "we were initiating. sending file"); - connection.send(file, onFileTransmissionStatusChanged); - } else { - Log.d(Config.LOGTAG, "we were responding. receiving file"); - connection.receive(file, onFileTransmissionStatusChanged); - } - } - } - } - - private JingleSocks5Transport chooseConnection() { - final List establishedConnections = FluentIterable.from(connections.entrySet()) - .transform(Entry::getValue) - .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs()))) - .toSortedList((a, b) -> { - final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority()); - if (compare == 0) { - if (isInitiator()) { - //pick the one we sent a candidate-used for (meaning not ours) - return a.getCandidate().isOurs() ? 1 : -1; - } else { - //pick the one they sent a candidate-used for (meaning ours) - return a.getCandidate().isOurs() ? -1 : 1; - } - } - return compare; - }); - return Iterables.getFirst(establishedConnections, null); - } - - private void sendSuccess() { - sendSessionTerminate(Reason.SUCCESS); - this.disconnectSocks5Connections(); - this.mJingleStatus = JINGLE_STATUS_FINISHED; - this.message.setStatus(Message.STATUS_RECEIVED); - this.message.setTransferable(null); - this.xmppConnectionService.updateMessage(message, false); - this.jingleConnectionManager.finishConnection(this); - } - - private void sendFallbackToIbb() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); - final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); - final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - this.transportId = JingleConnectionManager.nextRandomId(); - content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - packet.addJingleContent(content); - this.sendJinglePacket(packet); - } - - - private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) { - if (isInitiator()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)"); - respondToIqWithOutOfOrder(packet); - return; - } - final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING); - if (!validState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace"); - respondToIqWithOutOfOrder(packet); - return; - } - this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one; - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb"); - final int remoteBlockSize = transportInfo.getBlockSize(); - if (remoteBlockSize > 0) { - this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); - } - this.transportId = transportInfo.getTransportId(); //TODO: handle the case where this is null by the remote party - this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - - final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); - - final Content content = new Content(contentCreator, contentSenders, contentName); - content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - answer.addJingleContent(content); - - respondToIq(packet, true); - - if (isInitiator()) { - this.sendJinglePacket(answer, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb"); - transport.connect(onIbbTransportConnected); - } - }); - } else { - this.transport.receive(file, onFileTransmissionStatusChanged); - this.sendJinglePacket(answer); - } - } - - private void receiveTransportAccept(JinglePacket packet) { - if (responding()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)"); - respondToIqWithOutOfOrder(packet); - return; - } - final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING); - if (!validState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept"); - respondToIqWithOutOfOrder(packet); - return; - } - this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one; - final Content content = packet.getJingleContent(); - final GenericTransportInfo transportInfo = content == null ? null : content.getTransport(); - if (transportInfo instanceof IbbTransportInfo) { - final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; - final int remoteBlockSize = ibbTransportInfo.getBlockSize(); - if (remoteBlockSize > 0) { - this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize); - } - final String sid = ibbTransportInfo.getTransportId(); - this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - - if (sid == null || !sid.equals(this.transportId)) { - Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId)); - } - respondToIq(packet, true); - //might be receive instead if we are not initiating - if (isInitiator()) { - this.transport.connect(onIbbTransportConnected); - } - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept"); - respondToIq(packet, false); - } - } - - private void receiveSuccess() { - if (isInitiator()) { - this.mJingleStatus = JINGLE_STATUS_FINISHED; - this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED); - this.disconnectSocks5Connections(); - if (this.transport instanceof JingleInBandTransport) { - this.transport.disconnect(); - } - this.message.setTransferable(null); - this.jingleConnectionManager.finishConnection(this); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding"); - } } @Override - public void cancel() { - this.cancelled = true; - abort(Reason.CANCEL); + void deliverPacket(final JinglePacket jinglePacket) { + switch (jinglePacket.getAction()) { + case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket); + case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket); + case SESSION_INFO -> receiveSessionInfo(jinglePacket); + case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket); + case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket); + case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket); + case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket); + default -> { + respondOk(jinglePacket); + Log.d( + Config.LOGTAG, + String.format( + "%s: received unhandled jingle action %s", + id.account.getJid().asBareJid(), jinglePacket.getAction())); + } + } } - private void abort(final Reason reason) { - this.disconnectSocks5Connections(); - if (this.transport instanceof JingleInBandTransport) { - this.transport.disconnect(); - } - sendSessionTerminate(reason); - this.jingleConnectionManager.finishConnection(this); - if (responding()) { - this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED)); - if (this.file != null) { - file.delete(); - } - this.jingleConnectionManager.updateConversationUi(true); + public void sendSessionInitialize() { + final ListenableFuture> keyTransportMessage; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + keyTransportMessage = + Futures.transform( + id.account + .getAxolotlService() + .prepareKeyTransportMessage(requireConversation()), + Optional::of, + MoreExecutors.directExecutor()); } else { - this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null); - this.message.setTransferable(null); + keyTransportMessage = Futures.immediateFuture(Optional.empty()); + } + Futures.addCallback( + keyTransportMessage, + new FutureCallback<>() { + @Override + public void onSuccess(final Optional xmppAxolotlMessage) { + sendSessionInitialize(xmppAxolotlMessage.orElse(null)); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "can not send message"); + } + }, + MoreExecutors.directExecutor()); + } + + private void sendSessionInitialize(final XmppAxolotlMessage xmppAxolotlMessage) { + this.transport = setupTransport(); + this.transport.setTransportCallback(this); + final File file = xmppConnectionService.getFileBackend().getFile(message); + final var fileDescription = + new FileTransferDescription.File( + file.length(), + file.getName(), + message.getMimeType(), + Collections.emptyList()); + final var transportInfoFuture = this.transport.asInitialTransportInfo(); + Futures.addCallback( + transportInfoFuture, + new FutureCallback<>() { + @Override + public void onSuccess( + final Transport.InitialTransportInfo initialTransportInfo) { + final FileTransferContentMap contentMap = + FileTransferContentMap.of(fileDescription, initialTransportInfo); + sendSessionInitialize(xmppAxolotlMessage, contentMap); + } + + @Override + public void onFailure(@NonNull Throwable throwable) {} + }, + MoreExecutors.directExecutor()); + } + + private Conversation requireConversation() { + final var conversational = message.getConversation(); + if (conversational instanceof Conversation c) { + return c; + } else { + throw new IllegalStateException("Message had no proper conversation attached"); } } - private void fail() { - fail(null); + private void sendSessionInitialize( + final XmppAxolotlMessage xmppAxolotlMessage, final FileTransferContentMap contentMap) { + if (transition( + State.SESSION_INITIALIZED, + () -> this.initiatorFileTransferContentMap = contentMap)) { + final var jinglePacket = + contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + if (xmppAxolotlMessage != null) { + this.transportSecurity = + new TransportSecurity( + xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV()); + jinglePacket.setSecurity( + Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage); + } + Log.d(Config.LOGTAG, "--> " + jinglePacket.toString()); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + handleIqErrorResponse(response); + return; + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); + this.transport.readyToSentAdditionalCandidates(); + } } - private void fail(String errorMessage) { - this.mJingleStatus = JINGLE_STATUS_FAILED; - this.disconnectSocks5Connections(); - if (this.transport instanceof JingleInBandTransport) { - this.transport.disconnect(); + private void receiveSessionAccept(final JinglePacket jinglePacket) { + Log.d(Config.LOGTAG, "receive session accept " + jinglePacket); + if (isResponder()) { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); + return; } - FileBackend.close(mFileInputStream); - FileBackend.close(mFileOutputStream); - if (this.message != null) { - if (responding()) { - this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED)); - if (this.file != null) { - file.delete(); - } - this.jingleConnectionManager.updateConversationUi(true); + final FileTransferContentMap contentMap; + try { + contentMap = FileTransferContentMap.of(jinglePacket); + contentMap.requireOnlyFileTransferDescription(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + receiveSessionAccept(jinglePacket, contentMap); + } + + private void receiveSessionAccept( + final JinglePacket jinglePacket, final FileTransferContentMap contentMap) { + if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) { + respondOk(jinglePacket); + final var transport = this.transport; + if (configureTransportWithPeerInfo(transport, contentMap)) { + transport.connect(); } else { - this.xmppConnectionService.markMessage(this.message, - Message.STATUS_SEND_FAILED, - cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); - this.message.setTransferable(null); + Log.e( + Config.LOGTAG, + "Transport in session accept did not match our session-initialize"); + terminateTransport(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + "Transport in session accept did not match our session-initialize"); } - } - this.jingleConnectionManager.finishConnection(this); - } - - private void sendSessionTerminate(Reason reason) { - final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE); - packet.setReason(reason, null); - this.sendJinglePacket(packet); - } - - private void connectNextCandidate() { - for (JingleCandidate candidate : this.candidates) { - if ((!connections.containsKey(candidate.getCid()) && (!candidate - .isOurs()))) { - this.connectWithCandidate(candidate); - return; - } - } - this.sendCandidateError(); - } - - private void connectWithCandidate(final JingleCandidate candidate) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport( - this, candidate); - connections.put(candidate.getCid(), socksConnection); - socksConnection.connect(new OnTransportConnected() { - - @Override - public void failed() { - Log.d(Config.LOGTAG, - "connection failed with " + candidate.getHost() + ":" - + candidate.getPort()); - connectNextCandidate(); - } - - @Override - public void established() { - Log.d(Config.LOGTAG, - "established connection with " + candidate.getHost() - + ":" + candidate.getPort()); - sendCandidateUsed(candidate.getCid()); - } - }); - } - - private void disconnectSocks5Connections() { - Iterator> it = this.connections - .entrySet().iterator(); - while (it.hasNext()) { - Entry pairs = it.next(); - pairs.getValue().disconnect(); - it.remove(); + } else { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": receive out of order session-accept"); + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); } } - private void sendProxyActivated(String cid) { - final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); - packet.addJingleContent(content); - this.sendJinglePacket(packet); - } - - private void sendProxyError() { - final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); - packet.addJingleContent(content); - this.sendJinglePacket(packet); - } - - private void sendCandidateUsed(final String cid) { - JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); - packet.addJingleContent(content); - this.sentCandidate = true; - if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { - connect(); - } - this.sendJinglePacket(packet); - } - - private void sendCandidateError() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); - JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - Content content = new Content(this.contentCreator, this.contentSenders, this.contentName); - content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); - packet.addJingleContent(content); - this.sentCandidate = true; - this.sendJinglePacket(packet); - if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) { - connect(); - } - } - - private int getJingleStatus() { - return this.mJingleStatus; - } - - private boolean equalCandidateExists(JingleCandidate candidate) { - for (JingleCandidate c : this.candidates) { - if (c.equalValues(candidate)) { - return true; - } - } - return false; - } - - private void mergeCandidate(JingleCandidate candidate) { - for (JingleCandidate c : this.candidates) { - if (c.equals(candidate)) { - return; - } - } - this.candidates.add(candidate); - } - - private void mergeCandidates(List candidates) { - Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority())); - for (JingleCandidate c : candidates) { - mergeCandidate(c); - } - } - - private JingleCandidate getCandidate(String cid) { - for (JingleCandidate c : this.candidates) { - if (c.getCid().equals(cid)) { - return c; - } - } - return null; - } - - void updateProgress(int i) { - this.mProgress = i; - jingleConnectionManager.updateConversationUi(false); - } - - String getTransportId() { - return this.transportId; - } - - FileTransferDescription.Version getFtVersion() { - return this.description.getVersion(); - } - - public JingleTransport getTransport() { - return this.transport; - } - - public boolean start() { - if (id.account.getStatus() == Account.State.ONLINE) { - if (mJingleStatus == JINGLE_STATUS_INITIATED) { - new Thread(this::sendAccept).start(); + private static boolean configureTransportWithPeerInfo( + final Transport transport, final FileTransferContentMap contentMap) { + final GenericTransportInfo transportInfo = contentMap.requireOnlyTransportInfo(); + if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport + && transportInfo instanceof WebRTCDataChannelTransportInfo) { + webRTCDataChannelTransport.setResponderDescription(SessionDescription.of(contentMap)); + return true; + } else if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport + && transportInfo + instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) { + socksBytestreamsTransport.setTheirCandidates( + socksBytestreamsTransportInfo.getCandidates()); + return true; + } else if (transport instanceof InbandBytestreamsTransport inbandBytestreamsTransport + && transportInfo instanceof IbbTransportInfo ibbTransportInfo) { + final var peerBlockSize = ibbTransportInfo.getBlockSize(); + if (peerBlockSize != null) { + inbandBytestreamsTransport.setPeerBlockSize(peerBlockSize); } return true; } else { @@ -1224,32 +305,1150 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } + private void receiveSessionInitiate(final JinglePacket jinglePacket) { + if (isInitiator()) { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + return; + } + Log.d(Config.LOGTAG, "receive session initiate " + jinglePacket); + final FileTransferContentMap contentMap; + final FileTransferDescription.File file; + try { + contentMap = FileTransferContentMap.of(jinglePacket); + contentMap.requireContentDescriptions(); + file = contentMap.requireOnlyFile(); + // TODO check is offer + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + final var security = + jinglePacket.getSecurity(Iterables.getOnlyElement(contentMap.contents.keySet())); + if (security != null) { + Log.d(Config.LOGTAG, "found security element!"); + keyTransportMessage = + id.account + .getAxolotlService() + .processReceivingKeyTransportMessage(security, false); + } else { + keyTransportMessage = null; + } + receiveSessionInitiate(jinglePacket, contentMap, file, keyTransportMessage); + } + + private void receiveSessionInitiate( + final JinglePacket jinglePacket, + final FileTransferContentMap contentMap, + final FileTransferDescription.File file, + final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage) { + + if (transition(State.SESSION_INITIALIZED, () -> setRemoteContentMap(contentMap))) { + respondOk(jinglePacket); + Log.d(Config.LOGTAG, jinglePacket.toString()); + Log.d( + Config.LOGTAG, + "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage)); + setFileOffer(file); + if (keyTransportMessage != null) { + this.transportSecurity = + new TransportSecurity( + keyTransportMessage.getKey(), keyTransportMessage.getIv()); + this.message.setFingerprint(keyTransportMessage.getFingerprint()); + this.message.setEncryption(Message.ENCRYPTION_AXOLOTL); + } else { + this.transportSecurity = null; + this.message.setFingerprint(null); + } + final var conversation = (Conversation) message.getConversation(); + conversation.add(message); + + // make auto accept decision + if (id.account.getRoster().getContact(id.with).showInContactList() + && jingleConnectionManager.hasStoragePermission() + && file.size <= this.jingleConnectionManager.getAutoAcceptFileSize() + && xmppConnectionService.isDataSaverDisabled()) { + Log.d(Config.LOGTAG, "auto accepting file from " + id.with); + this.acceptedAutomatically = true; + this.sendSessionAccept(); + } else { + Log.d( + Config.LOGTAG, + "not auto accepting new file offer with size: " + + file.size + + " allowed size:" + + this.jingleConnectionManager.getAutoAcceptFileSize()); + message.markUnread(); + this.xmppConnectionService.updateConversationUi(); + this.xmppConnectionService.getNotificationService().push(message); + } + } else { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": receive out of order session-initiate"); + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + } + } + + private void setFileOffer(final FileTransferDescription.File file) { + final AbstractConnectionManager.Extension extension = + AbstractConnectionManager.Extension.of(file.name); + if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } else { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + final String ext = extension.getExtension(); + final String filename = + Strings.isNullOrEmpty(ext) + ? message.getUuid() + : String.format("%s.%s", message.getUuid(), ext); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename); + } + + public void sendSessionAccept() { + final FileTransferContentMap contentMap = this.initiatorFileTransferContentMap; + final Transport transport; + try { + transport = setupTransport(contentMap.requireOnlyTransportInfo()); + } catch (final RuntimeException e) { + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + transitionOrThrow(State.SESSION_ACCEPTED); + this.transport = transport; + this.transport.setTransportCallback(this); + if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) { + final var sessionDescription = SessionDescription.of(contentMap); + webRTCDataChannelTransport.setInitiatorDescription(sessionDescription); + } + final var transportInfoFuture = transport.asTransportInfo(); + Futures.addCallback( + transportInfoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final Transport.TransportInfo transportInfo) { + final FileTransferContentMap responderContentMap = + contentMap.withTransport(transportInfo); + sendSessionAccept(responderContentMap); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + failureToAcceptSession(throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private void sendSessionAccept(final FileTransferContentMap contentMap) { + setLocalContentMap(contentMap); + final var jinglePacket = + contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + Log.d(Config.LOGTAG, "--> " + jinglePacket.toString()); + send(jinglePacket); + // this needs to come after session-accept or else our candidate-error might arrive first + this.transport.connect(); + this.transport.readyToSentAdditionalCandidates(); + if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) { + drainPendingIncomingIceCandidates(webRTCDataChannelTransport); + } + } + + private void drainPendingIncomingIceCandidates( + final WebRTCDataChannelTransport webRTCDataChannelTransport) { + while (this.pendingIncomingIceCandidates.peek() != null) { + final var candidate = this.pendingIncomingIceCandidates.poll(); + if (candidate == null) { + continue; + } + webRTCDataChannelTransport.addIceCandidates(ImmutableList.of(candidate)); + } + } + + private Transport setupTransport(final GenericTransportInfo transportInfo) { + final XmppConnection xmppConnection = id.account.getXmppConnection(); + final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect(); + if (transportInfo instanceof IbbTransportInfo ibbTransportInfo) { + final String streamId = ibbTransportInfo.getTransportId(); + final Long blockSize = ibbTransportInfo.getBlockSize(); + if (streamId == null || blockSize == null) { + throw new IllegalStateException("ibb transport is missing sid and/or block-size"); + } + return new InbandBytestreamsTransport( + xmppConnection, + id.with, + isInitiator(), + streamId, + Ints.saturatedCast(blockSize)); + } else if (transportInfo + instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) { + final String streamId = socksBytestreamsTransportInfo.getTransportId(); + final String destination = socksBytestreamsTransportInfo.getDestinationAddress(); + final List candidates = + socksBytestreamsTransportInfo.getCandidates(); + Log.d(Config.LOGTAG, "received socks candidates " + candidates); + return new SocksByteStreamsTransport( + xmppConnection, id, isInitiator(), useTor, streamId, candidates); + } else if (!useTor && transportInfo instanceof WebRTCDataChannelTransportInfo) { + return new WebRTCDataChannelTransport( + xmppConnectionService.getApplicationContext(), + xmppConnection, + id.account, + isInitiator()); + } else { + throw new IllegalArgumentException("Do not know how to create transport"); + } + } + + private Transport setupTransport() { + final XmppConnection xmppConnection = id.account.getXmppConnection(); + final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect(); + if (!useTor && remoteHasFeature(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL)) { + return new WebRTCDataChannelTransport( + xmppConnectionService.getApplicationContext(), + xmppConnection, + id.account, + isInitiator()); + } + if (remoteHasFeature(Namespace.JINGLE_TRANSPORTS_S5B)) { + return new SocksByteStreamsTransport(xmppConnection, id, isInitiator(), useTor); + } + return setupLastResortTransport(); + } + + private Transport setupLastResortTransport() { + final XmppConnection xmppConnection = id.account.getXmppConnection(); + return new InbandBytestreamsTransport(xmppConnection, id.with, isInitiator()); + } + + private void failureToAcceptSession(final Throwable throwable) { + if (isTerminated()) { + return; + } + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d(Config.LOGTAG, "unable to send session accept", rootCause); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); + } + + private void receiveSessionInfo(final JinglePacket jinglePacket) { + Log.d(Config.LOGTAG, "<-- " + jinglePacket); + respondOk(jinglePacket); + final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket); + if (sessionInfo instanceof FileTransferDescription.Checksum checksum) { + receiveSessionInfoChecksum(checksum); + } else if (sessionInfo instanceof FileTransferDescription.Received received) { + receiveSessionInfoReceived(received); + } + } + + private void receiveSessionInfoChecksum(final FileTransferDescription.Checksum checksum) { + Log.d(Config.LOGTAG, "received checksum " + checksum); + } + + private void receiveSessionInfoReceived(final FileTransferDescription.Received received) { + Log.d(Config.LOGTAG, "peer confirmed received " + received); + } + + private void receiveSessionTerminate(final JinglePacket jinglePacket) { + final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); + final State previous = this.state; + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session terminate reason=" + + wrapper.reason + + "(" + + Strings.nullToEmpty(wrapper.text) + + ") while in state " + + previous); + if (TERMINATED.contains(previous)) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring session terminate because already in " + + previous); + return; + } + if (isInitiator()) { + this.message.setErrorMessage( + Strings.isNullOrEmpty(wrapper.text) ? wrapper.reason.toString() : wrapper.text); + } + terminateTransport(); + final State target = reasonToState(wrapper.reason); + transitionOrThrow(target); + finish(); + } + + private void receiveTransportAccept(final JinglePacket jinglePacket) { + if (isResponder()) { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT); + return; + } + Log.d(Config.LOGTAG, "receive transport accept " + jinglePacket); + final GenericTransportInfo transportInfo; + try { + transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + if (isInState(State.SESSION_ACCEPTED)) { + final var group = jinglePacket.getGroup(); + receiveTransportAccept(jinglePacket, new Transport.TransportInfo(transportInfo, group)); + } else { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT); + } + } + + private void receiveTransportAccept( + final JinglePacket jinglePacket, final Transport.TransportInfo transportInfo) { + final FileTransferContentMap remoteContentMap = + getRemoteContentMap().withTransport(transportInfo); + setRemoteContentMap(remoteContentMap); + respondOk(jinglePacket); + final var transport = this.transport; + if (configureTransportWithPeerInfo(transport, remoteContentMap)) { + transport.connect(); + } else { + Log.e( + Config.LOGTAG, + "Transport in transport-accept did not match our transport-replace"); + terminateTransport(); + sendSessionTerminate( + Reason.FAILED_APPLICATION, + "Transport in transport-accept did not match our transport-replace"); + } + } + + private void receiveTransportInfo(final JinglePacket jinglePacket) { + final FileTransferContentMap contentMap; + final GenericTransportInfo transportInfo; + try { + contentMap = FileTransferContentMap.of(jinglePacket); + transportInfo = contentMap.requireOnlyTransportInfo(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + respondOk(jinglePacket); + final var transport = this.transport; + if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport + && transportInfo + instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) { + receiveTransportInfo(socksBytestreamsTransport, socksBytestreamsTransportInfo); + } else if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport + && transportInfo + instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) { + receiveTransportInfo( + Iterables.getOnlyElement(contentMap.contents.keySet()), + webRTCDataChannelTransport, + webRTCDataChannelTransportInfo); + } else if (transportInfo + instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) { + receiveTransportInfo( + Iterables.getOnlyElement(contentMap.contents.keySet()), + webRTCDataChannelTransportInfo); + } else { + Log.d(Config.LOGTAG, "could not deliver transport-info to transport"); + } + } + + private void receiveTransportInfo( + final String contentName, + final WebRTCDataChannelTransport webRTCDataChannelTransport, + final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) { + final var credentials = webRTCDataChannelTransportInfo.getCredentials(); + final var iceCandidates = + WebRTCDataChannelTransport.iceCandidatesOf( + contentName, credentials, webRTCDataChannelTransportInfo.getCandidates()); + final var localContentMap = getLocalContentMap(); + if (localContentMap == null) { + Log.d(Config.LOGTAG, "transport not ready. add pending ice candidate"); + this.pendingIncomingIceCandidates.addAll(iceCandidates); + } else { + webRTCDataChannelTransport.addIceCandidates(iceCandidates); + } + } + + private void receiveTransportInfo( + final String contentName, + final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) { + final var credentials = webRTCDataChannelTransportInfo.getCredentials(); + final var iceCandidates = + WebRTCDataChannelTransport.iceCandidatesOf( + contentName, credentials, webRTCDataChannelTransportInfo.getCandidates()); + this.pendingIncomingIceCandidates.addAll(iceCandidates); + } + + private void receiveTransportInfo( + final SocksByteStreamsTransport socksBytestreamsTransport, + final SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) { + final var transportInfo = socksBytestreamsTransportInfo.getTransportInfo(); + if (transportInfo instanceof SocksByteStreamsTransportInfo.CandidateError) { + socksBytestreamsTransport.setCandidateError(); + } else if (transportInfo + instanceof SocksByteStreamsTransportInfo.CandidateUsed candidateUsed) { + if (!socksBytestreamsTransport.setCandidateUsed(candidateUsed.cid)) { + sendSessionTerminate( + Reason.FAILED_TRANSPORT, + String.format( + "Peer is not connected to our candidate %s", candidateUsed.cid)); + } + } else if (transportInfo instanceof SocksByteStreamsTransportInfo.Activated activated) { + socksBytestreamsTransport.setProxyActivated(activated.cid); + } else if (transportInfo instanceof SocksByteStreamsTransportInfo.ProxyError) { + socksBytestreamsTransport.setProxyError(); + } + } + + private void receiveTransportReplace(final JinglePacket jinglePacket) { + if (isInitiator()) { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE); + return; + } + Log.d(Config.LOGTAG, "receive transport replace " + jinglePacket); + final GenericTransportInfo transportInfo; + try { + transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo(); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); + respondOk(jinglePacket); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + if (isInState(State.SESSION_ACCEPTED)) { + receiveTransportReplace(jinglePacket, transportInfo); + } else { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE); + } + } + + private void receiveTransportReplace( + final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) { + respondOk(jinglePacket); + final Transport transport; + try { + transport = setupTransport(transportInfo); + } catch (final RuntimeException e) { + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + this.transport = transport; + this.transport.setTransportCallback(this); + final var transportInfoFuture = transport.asTransportInfo(); + Futures.addCallback( + transportInfoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final Transport.TransportInfo transportWrapper) { + final FileTransferContentMap contentMap = + getLocalContentMap().withTransport(transportWrapper); + sendTransportAccept(contentMap); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + // transition into application failed (analogues to failureToAccept + } + }, + MoreExecutors.directExecutor()); + } + + private void sendTransportAccept(final FileTransferContentMap contentMap) { + setLocalContentMap(contentMap); + final var jinglePacket = + contentMap + .transportInfo() + .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId); + Log.d(Config.LOGTAG, "sending transport accept " + jinglePacket); + send(jinglePacket); + transport.connect(); + } + + protected void sendSessionTerminate(final Reason reason, final String text) { + if (isInitiator()) { + this.message.setErrorMessage(Strings.isNullOrEmpty(text) ? reason.toString() : text); + } + sendSessionTerminate(reason, text, null); + } + + private FileTransferContentMap getLocalContentMap() { + return isInitiator() + ? this.initiatorFileTransferContentMap + : this.responderFileTransferContentMap; + } + + private FileTransferContentMap getRemoteContentMap() { + return isInitiator() + ? this.responderFileTransferContentMap + : this.initiatorFileTransferContentMap; + } + + private void setLocalContentMap(final FileTransferContentMap contentMap) { + if (isInitiator()) { + this.initiatorFileTransferContentMap = contentMap; + } else { + this.responderFileTransferContentMap = contentMap; + } + } + + private void setRemoteContentMap(final FileTransferContentMap contentMap) { + if (isInitiator()) { + this.responderFileTransferContentMap = contentMap; + } else { + this.initiatorFileTransferContentMap = contentMap; + } + } + + public Transport getTransport() { + return this.transport; + } + + @Override + protected void terminateTransport() { + final var transport = this.transport; + if (transport == null) { + return; + } + transport.terminate(); + this.transport = null; + } + + @Override + void notifyRebound() {} + + @Override + public void onTransportEstablished() { + Log.d(Config.LOGTAG, "on transport established"); + final AbstractFileTransceiver fileTransceiver; + try { + fileTransceiver = setupTransceiver(isResponder()); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "failed to set up file transceiver", e); + sendSessionTerminate(Reason.ofThrowable(e), e.getMessage()); + return; + } + this.fileTransceiver = fileTransceiver; + final var fileTransceiverThread = new Thread(fileTransceiver); + fileTransceiverThread.start(); + Futures.addCallback( + fileTransceiver.complete, + new FutureCallback<>() { + @Override + public void onSuccess(final List hashes) { + onFileTransmissionComplete(hashes); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + onFileTransmissionFailed(throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private void onFileTransmissionComplete(final List hashes) { + // TODO if we ever support receiving files this should become isSending(); isReceiving() + if (isInitiator()) { + sendSessionInfoChecksum(hashes); + } else { + Log.d(Config.LOGTAG, "file transfer complete " + hashes); + sendFileSessionInfoReceived(); + terminateTransport(); + messageReceivedSuccess(); + sendSessionTerminate(Reason.SUCCESS, null); + } + } + + private void messageReceivedSuccess() { + this.message.setTransferable(null); + xmppConnectionService.getFileBackend().updateFileParams(message); + xmppConnectionService.databaseBackend.createMessage(message); + final File file = xmppConnectionService.getFileBackend().getFile(message); + if (acceptedAutomatically) { + message.markUnread(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + id.account.getPgpDecryptionService().decrypt(message, true); + } else { + xmppConnectionService + .getFileBackend() + .updateMediaScanner( + file, + () -> + JingleFileTransferConnection.this + .xmppConnectionService + .getNotificationService() + .push(message)); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + id.account.getPgpDecryptionService().decrypt(message, false); + } else { + xmppConnectionService.getFileBackend().updateMediaScanner(file); + } + } + + private void onFileTransmissionFailed(final Throwable throwable) { + if (isTerminated()) { + Log.d( + Config.LOGTAG, + "file transfer failed but session is already terminated", + throwable); + } else { + terminateTransport(); + Log.d(Config.LOGTAG, "on file transmission failed", throwable); + sendSessionTerminate(Reason.CONNECTIVITY_ERROR, null); + } + } + + private AbstractFileTransceiver setupTransceiver(final boolean receiving) throws IOException { + final var fileDescription = getLocalContentMap().requireOnlyFile(); + final File file = xmppConnectionService.getFileBackend().getFile(message); + final Runnable updateRunnable = () -> jingleConnectionManager.updateConversationUi(false); + if (receiving) { + return new FileReceiver( + file, + this.transportSecurity, + transport.getInputStream(), + transport.getTerminationLatch(), + fileDescription.size, + updateRunnable); + } else { + return new FileTransmitter( + file, + this.transportSecurity, + transport.getOutputStream(), + transport.getTerminationLatch(), + fileDescription.size, + updateRunnable); + } + } + + private void sendFileSessionInfoReceived() { + final var contentMap = getLocalContentMap(); + final String name = Iterables.getOnlyElement(contentMap.contents.keySet()); + sendSessionInfo(new FileTransferDescription.Received(name)); + } + + private void sendSessionInfoChecksum(List hashes) { + final var contentMap = getLocalContentMap(); + final String name = Iterables.getOnlyElement(contentMap.contents.keySet()); + sendSessionInfo(new FileTransferDescription.Checksum(name, hashes)); + } + + private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) { + final var jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId); + jinglePacket.addJingleChild(sessionInfo.asElement()); + jinglePacket.setTo(this.id.with); + Log.d(Config.LOGTAG, "--> " + jinglePacket); + send(jinglePacket); + } + + @Override + public void onTransportSetupFailed() { + final var transport = this.transport; + if (transport == null) { + // this really is not supposed to happen + sendSessionTerminate(Reason.FAILED_APPLICATION, null); + return; + } + Log.d(Config.LOGTAG, "onTransportSetupFailed"); + final var isTransportInBand = transport instanceof InbandBytestreamsTransport; + if (isTransportInBand) { + terminateTransport(); + sendSessionTerminate(Reason.CONNECTIVITY_ERROR, "Failed to setup IBB transport"); + return; + } + // terminate the current transport + transport.terminate(); + if (isInitiator()) { + this.transport = setupLastResortTransport(); + this.transport.setTransportCallback(this); + final var transportInfoFuture = this.transport.asTransportInfo(); + Futures.addCallback( + transportInfoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final Transport.TransportInfo transportWrapper) { + final FileTransferContentMap contentMap = getLocalContentMap(); + sendTransportReplace(contentMap.withTransport(transportWrapper)); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + // TODO send application failure; + } + }, + MoreExecutors.directExecutor()); + + } else { + Log.d(Config.LOGTAG, "transport setup failed. waiting for initiator to replace"); + } + } + + private void sendTransportReplace(final FileTransferContentMap contentMap) { + setLocalContentMap(contentMap); + final var jinglePacket = + contentMap + .transportInfo() + .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId); + Log.d(Config.LOGTAG, "sending transport replace " + jinglePacket); + send(jinglePacket); + } + + @Override + public void onAdditionalCandidate( + final String contentName, final Transport.Candidate candidate) { + if (candidate instanceof IceUdpTransportInfo.Candidate iceCandidate) { + sendTransportInfo(contentName, iceCandidate); + } + } + + public void sendTransportInfo( + final String contentName, final IceUdpTransportInfo.Candidate candidate) { + final FileTransferContentMap transportInfo; + try { + final FileTransferContentMap rtpContentMap = getLocalContentMap(); + transportInfo = rtpContentMap.transportInfo(contentName, candidate); + } catch (final Exception e) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to prepare transport-info from candidate for content=" + + contentName); + return; + } + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "--> " + jinglePacket); + send(jinglePacket); + } + + @Override + public void onCandidateUsed( + final String streamId, final SocksByteStreamsTransport.Candidate candidate) { + final FileTransferContentMap contentMap = getLocalContentMap(); + if (contentMap == null) { + Log.e(Config.LOGTAG, "local content map is null on candidate used"); + return; + } + final var jinglePacket = + contentMap + .candidateUsed(streamId, candidate.cid) + .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "sending candidate used " + jinglePacket); + send(jinglePacket); + } + + @Override + public void onCandidateError(final String streamId) { + final FileTransferContentMap contentMap = getLocalContentMap(); + if (contentMap == null) { + Log.e(Config.LOGTAG, "local content map is null on candidate used"); + return; + } + final var jinglePacket = + contentMap + .candidateError(streamId) + .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "sending candidate error " + jinglePacket); + send(jinglePacket); + } + + @Override + public void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate) { + final FileTransferContentMap contentMap = getLocalContentMap(); + if (contentMap == null) { + Log.e(Config.LOGTAG, "local content map is null on candidate used"); + return; + } + final var jinglePacket = + contentMap + .proxyActivated(streamId, candidate.cid) + .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + send(jinglePacket); + } + + @Override + protected boolean transition(final State target, final Runnable runnable) { + final boolean transitioned = super.transition(target, runnable); + if (transitioned && isInitiator()) { + Log.d(Config.LOGTAG, "running mark message hooks"); + if (target == State.SESSION_ACCEPTED) { + xmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + } else if (target == State.TERMINATED_SUCCESS) { + xmppConnectionService.markMessage(message, Message.STATUS_SEND_RECEIVED); + } else if (TERMINATED.contains(target)) { + xmppConnectionService.markMessage( + message, Message.STATUS_SEND_FAILED, message.getErrorMessage()); + } else { + xmppConnectionService.updateConversationUi(); + } + } else { + if (Arrays.asList(State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY) + .contains(target)) { + this.message.setTransferable( + new TransferablePlaceholder(Transferable.STATUS_CANCELLED)); + } else if (target != State.TERMINATED_SUCCESS && TERMINATED.contains(target)) { + this.message.setTransferable( + new TransferablePlaceholder(Transferable.STATUS_FAILED)); + } + xmppConnectionService.updateConversationUi(); + } + return transitioned; + } + + @Override + protected void finish() { + if (transport != null) { + throw new AssertionError( + "finish MUST not be called without terminating the transport first"); + } + // we don't want to remove TransferablePlaceholder + if (message.getTransferable() instanceof JingleFileTransferConnection) { + Log.d(Config.LOGTAG, "nulling transferable on message"); + this.message.setTransferable(null); + } + super.finish(); + } + + private int getTransferableStatus() { + // status in file transfer is a bit weird. for sending it is mostly handled via + // Message.STATUS_* (offered, unsend (sic) send_received) the transferable status is just + // uploading + // for receiving the message status remains at 'received' but Transferable goes through + // various status + if (isInitiator()) { + return Transferable.STATUS_UPLOADING; + } + final var state = getState(); + return switch (state) { + case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> Transferable + .STATUS_OFFER; + case TERMINATED_APPLICATION_FAILURE, + TERMINATED_CONNECTIVITY_ERROR, + TERMINATED_DECLINED_OR_BUSY, + TERMINATED_SECURITY_ERROR -> Transferable.STATUS_FAILED; + case TERMINATED_CANCEL_OR_TIMEOUT -> Transferable.STATUS_CANCELLED; + case SESSION_ACCEPTED -> Transferable.STATUS_DOWNLOADING; + default -> Transferable.STATUS_UNKNOWN; + }; + } + + // these methods are for interacting with 'Transferable' - we might want to remove the concept + // at some point + + @Override + public boolean start() { + Log.d(Config.LOGTAG, "user pressed start()"); + // TODO there is a 'connected' check apparently? + if (isInState(State.SESSION_INITIALIZED)) { + sendSessionAccept(); + } + return true; + } + @Override public int getStatus() { - return this.mStatus; + return getTransferableStatus(); } @Override public Long getFileSize() { - if (this.file != null) { - return this.file.getExpectedSize(); - } else { - return null; + final var transceiver = this.fileTransceiver; + if (transceiver != null) { + return transceiver.total; } + final var contentMap = this.initiatorFileTransferContentMap; + if (contentMap != null) { + return contentMap.requireOnlyFile().size; + } + return null; } @Override public int getProgress() { - return this.mProgress; + final var transceiver = this.fileTransceiver; + return transceiver != null ? transceiver.getProgress() : 0; } - AbstractConnectionManager getConnectionManager() { - return this.jingleConnectionManager; + @Override + public void cancel() { + if (stopFileTransfer()) { + Log.d(Config.LOGTAG, "user has stopped file transfer"); + } else { + Log.d(Config.LOGTAG, "user pressed cancel but file transfer was already terminated?"); + } } - interface OnProxyActivated { - void success(); + private boolean stopFileTransfer() { + if (isInitiator()) { + return stopFileTransfer(Reason.CANCEL); + } else { + return stopFileTransfer(Reason.DECLINE); + } + } - void failed(); + private boolean stopFileTransfer(final Reason reason) { + final State target = reasonToState(reason); + if (transition(target)) { + // we change state before terminating transport so we don't consume the following + // IOException and turn it into a connectivity error + terminateTransport(); + final JinglePacket jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + jinglePacket.setReason(reason, "User requested to stop file transfer"); + send(jinglePacket); + finish(); + return true; + } else { + return false; + } + } + + private abstract static class AbstractFileTransceiver implements Runnable { + + protected final SettableFuture> complete = + SettableFuture.create(); + + protected final File file; + protected final TransportSecurity transportSecurity; + + protected final CountDownLatch transportTerminationLatch; + protected final long total; + protected long transmitted = 0; + private int progress = Integer.MIN_VALUE; + private final Runnable updateRunnable; + + private AbstractFileTransceiver( + final File file, + final TransportSecurity transportSecurity, + final CountDownLatch transportTerminationLatch, + final long total, + final Runnable updateRunnable) { + this.file = file; + this.transportSecurity = transportSecurity; + this.transportTerminationLatch = transportTerminationLatch; + this.total = transportSecurity == null ? total : (total + 16); + this.updateRunnable = updateRunnable; + } + + static void closeTransport(final Closeable stream) { + try { + stream.close(); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "transport has already been closed. good"); + } + } + + public int getProgress() { + return Ints.saturatedCast(Math.round((1.0 * transmitted / total) * 100)); + } + + public void updateProgress() { + final int current = getProgress(); + final boolean update; + synchronized (this) { + if (this.progress != current) { + this.progress = current; + update = true; + } else { + update = false; + } + if (update) { + this.updateRunnable.run(); + } + } + } + + protected void awaitTransportTermination() { + try { + this.transportTerminationLatch.await(); + } catch (final InterruptedException ignored) { + return; + } + Log.d(Config.LOGTAG, getClass().getSimpleName() + " says Goodbye!"); + } + } + + private static class FileTransmitter extends AbstractFileTransceiver { + + private final OutputStream outputStream; + + private FileTransmitter( + final File file, + final TransportSecurity transportSecurity, + final OutputStream outputStream, + final CountDownLatch transportTerminationLatch, + final long total, + final Runnable updateRunnable) { + super(file, transportSecurity, transportTerminationLatch, total, updateRunnable); + this.outputStream = outputStream; + } + + private InputStream openFileInputStream() throws FileNotFoundException { + final var fileInputStream = new FileInputStream(this.file); + if (this.transportSecurity == null) { + return fileInputStream; + } else { + final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init( + true, + new AEADParameters( + new KeyParameter(transportSecurity.key), + 128, + transportSecurity.iv)); + Log.d(Config.LOGTAG, "setting up CipherInputStream"); + return new CipherInputStream(fileInputStream, cipher); + } + } + + @Override + public void run() { + Log.d(Config.LOGTAG, "file transmitter attempting to send " + total + " bytes"); + final var sha1Hasher = Hashing.sha1().newHasher(); + final var sha256Hasher = Hashing.sha256().newHasher(); + try (final var fileInputStream = openFileInputStream()) { + final var buffer = new byte[4096]; + while (total - transmitted > 0) { + final int count = fileInputStream.read(buffer); + if (count == -1) { + throw new EOFException( + String.format("reached EOF after %d/%d", transmitted, total)); + } + outputStream.write(buffer, 0, count); + sha1Hasher.putBytes(buffer, 0, count); + sha256Hasher.putBytes(buffer, 0, count); + transmitted += count; + updateProgress(); + } + outputStream.flush(); + Log.d( + Config.LOGTAG, + "transmitted " + transmitted + " bytes from " + file.getAbsolutePath()); + final List hashes = + ImmutableList.of( + new FileTransferDescription.Hash( + sha1Hasher.hash().asBytes(), + FileTransferDescription.Algorithm.SHA_1), + new FileTransferDescription.Hash( + sha256Hasher.hash().asBytes(), + FileTransferDescription.Algorithm.SHA_256)); + complete.set(hashes); + } catch (final Exception e) { + complete.setException(e); + } + // the transport implementations backed by PipedOutputStreams do not like it when + // the writing Thread (this thread) goes away. so we just wait until the other peer + // has received our file and we are shutting down the transport + Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread"); + awaitTransportTermination(); + closeTransport(outputStream); + } + } + + private static class FileReceiver extends AbstractFileTransceiver { + + private final InputStream inputStream; + + private FileReceiver( + final File file, + final TransportSecurity transportSecurity, + final InputStream inputStream, + final CountDownLatch transportTerminationLatch, + final long total, + final Runnable updateRunnable) { + super(file, transportSecurity, transportTerminationLatch, total, updateRunnable); + this.inputStream = inputStream; + } + + private OutputStream openFileOutputStream() throws FileNotFoundException { + final var directory = this.file.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d(Config.LOGTAG, "created directory " + directory.getAbsolutePath()); + } + final var fileOutputStream = new FileOutputStream(this.file); + if (this.transportSecurity == null) { + return fileOutputStream; + } else { + final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init( + false, + new AEADParameters( + new KeyParameter(transportSecurity.key), + 128, + transportSecurity.iv)); + Log.d(Config.LOGTAG, "setting up CipherOutputStream"); + return new CipherOutputStream(fileOutputStream, cipher); + } + } + + @Override + public void run() { + Log.d(Config.LOGTAG, "file receiver attempting to receive " + total + " bytes"); + final var sha1Hasher = Hashing.sha1().newHasher(); + final var sha256Hasher = Hashing.sha256().newHasher(); + try (final var fileOutputStream = openFileOutputStream()) { + final var buffer = new byte[4096]; + while (total - transmitted > 0) { + final int count = inputStream.read(buffer); + if (count == -1) { + throw new EOFException( + String.format("reached EOF after %d/%d", transmitted, total)); + } + fileOutputStream.write(buffer, 0, count); + sha1Hasher.putBytes(buffer, 0, count); + sha256Hasher.putBytes(buffer, 0, count); + transmitted += count; + updateProgress(); + } + Log.d( + Config.LOGTAG, + "written " + transmitted + " bytes to " + file.getAbsolutePath()); + final List hashes = + ImmutableList.of( + new FileTransferDescription.Hash( + sha1Hasher.hash().asBytes(), + FileTransferDescription.Algorithm.SHA_1), + new FileTransferDescription.Hash( + sha256Hasher.hash().asBytes(), + FileTransferDescription.Algorithm.SHA_256)); + complete.set(hashes); + } catch (final Exception e) { + complete.setException(e); + } + Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread"); + awaitTransportTermination(); + closeTransport(inputStream); + } + } + + private static final class TransportSecurity { + final byte[] key; + final byte[] iv; + + private TransportSecurity(byte[] key, byte[] iv) { + this.key = key; + this.iv = iv; + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java deleted file mode 100644 index c68941928..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java +++ /dev/null @@ -1,265 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import android.util.Base64; -import android.util.Log; - -import com.google.common.base.Preconditions; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class JingleInBandTransport extends JingleTransport { - - private final Account account; - private final Jid counterpart; - private final int blockSize; - private int seq = 0; - private final String sessionId; - - private boolean established = false; - - private boolean connected = true; - - private DownloadableFile file; - private final JingleFileTransferConnection connection; - - private InputStream fileInputStream = null; - private InputStream innerInputStream = null; - private OutputStream fileOutputStream = null; - private long remainingSize = 0; - private long fileSize = 0; - private MessageDigest digest; - - private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; - - private final OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (!connected) { - return; - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (remainingSize > 0) { - sendNextBlock(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } - }; - - JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) { - this.connection = connection; - this.account = connection.getId().account; - this.counterpart = connection.getId().with; - this.blockSize = blockSize; - this.sessionId = sid; - } - - private void sendClose() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close"); - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element close = iq.addChild("close", "http://jabber.org/protocol/ibb"); - close.setAttribute("sid", this.sessionId); - this.account.getXmppConnection().sendIqPacket(iq, null); - } - - public boolean matches(final Account account, final String sessionId) { - return this.account == account && this.sessionId.equals(sessionId); - } - - public void connect(final OnTransportConnected callback) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); - open.setAttribute("sid", this.sessionId); - open.setAttribute("stanza", "iq"); - open.setAttribute("block-size", Integer.toString(this.blockSize)); - this.connected = true; - this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - callback.failed(); - } else { - callback.established(); - } - }); - } - - @Override - public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback); - this.file = file; - try { - this.digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - this.fileOutputStream = connection.getFileOutputStream(); - if (this.fileOutputStream == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream"); - callback.onFileTransferAborted(); - return; - } - this.remainingSize = this.fileSize = file.getExpectedSize(); - } catch (final NoSuchAlgorithmException | IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage()); - callback.onFileTransferAborted(); - } - } - - @Override - public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback); - this.file = file; - try { - this.remainingSize = this.file.getExpectedSize(); - this.fileSize = this.remainingSize; - this.digest = MessageDigest.getInstance("SHA-1"); - this.digest.reset(); - fileInputStream = connection.getFileInputStream(); - if (fileInputStream == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream"); - callback.onFileTransferAborted(); - return; - } - innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - if (this.connected) { - this.sendNextBlock(); - } - } catch (Exception e) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); - } - } - - @Override - public void disconnect() { - this.connected = false; - FileBackend.close(fileOutputStream); - FileBackend.close(fileInputStream); - } - - private void sendNextBlock() { - byte[] buffer = new byte[this.blockSize]; - try { - int count = innerInputStream.read(buffer); - if (count == -1) { - sendClose(); - file.setSha1Sum(digest.digest()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1"); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - fileInputStream.close(); - return; - } else if (count != buffer.length) { - int rem = innerInputStream.read(buffer, count, buffer.length - count); - if (rem > 0) { - count += rem; - } - } - this.remainingSize -= count; - this.digest.update(buffer, 0, count); - String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP); - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element data = iq.addChild("data", "http://jabber.org/protocol/ibb"); - data.setAttribute("seq", Integer.toString(this.seq)); - data.setAttribute("block-size", Integer.toString(this.blockSize)); - data.setAttribute("sid", this.sessionId); - data.setContent(base64); - this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived); - this.account.getXmppConnection().r(); //don't fill up stanza queue too much - this.seq++; - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - if (this.remainingSize <= 0) { - file.setSha1Sum(digest.digest()); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - sendClose(); - fileInputStream.close(); - } - } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage()); - FileBackend.close(fileInputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } - - private void receiveNextBlock(String data) { - try { - byte[] buffer = Base64.decode(data, Base64.NO_WRAP); - if (this.remainingSize < buffer.length) { - buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize); - } - this.remainingSize -= buffer.length; - this.fileOutputStream.write(buffer); - this.digest.update(buffer); - if (this.remainingSize <= 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close"); - } else { - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e); - FileBackend.close(fileOutputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } - - private void done() { - try { - file.setSha1Sum(digest.digest()); - fileOutputStream.flush(); - fileOutputStream.close(); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); - FileBackend.close(fileOutputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } - - void deliverPayload(IqPacket packet, Element payload) { - if (payload.getName().equals("open")) { - if (!established) { - established = true; - connected = true; - this.receiveNextBlock(""); - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else { - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.ERROR), null); - } - } else if (connected && payload.getName().equals("data")) { - this.receiveNextBlock(payload.getContent()); - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else if (connected && payload.getName().equals("close")) { - this.connected = false; - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - if (this.remainingSize <= 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done"); - done(); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining"); - FileBackend.close(fileOutputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } else { - this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index a7d781840..80d7d2118 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -13,7 +13,6 @@ import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -30,14 +29,10 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.RtpSessionStatus; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.xml.Element; @@ -78,96 +73,13 @@ public class JingleRtpConnection extends AbstractJingleConnection Arrays.asList( State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); private static final long BUSY_TIME_OUT = 30; - private static final List TERMINATED = - Arrays.asList( - State.ACCEPTED, - State.REJECTED, - State.REJECTED_RACED, - State.RETRACTED, - State.RETRACTED_RACED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR); - - private static final Map> VALID_TRANSITIONS; - - static { - final ImmutableMap.Builder> transitionBuilder = - new ImmutableMap.Builder<>(); - transitionBuilder.put( - State.NULL, - ImmutableList.of( - State.PROPOSED, - State.SESSION_INITIALIZED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR)); - transitionBuilder.put( - State.PROPOSED, - ImmutableList.of( - State.ACCEPTED, - State.PROCEED, - State.REJECTED, - State.RETRACTED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection - // rebinds - )); - transitionBuilder.put( - State.PROCEED, - ImmutableList.of( - State.REJECTED_RACED, - State.RETRACTED_RACED, - State.SESSION_INITIALIZED_PRE_APPROVED, - State.TERMINATED_SUCCESS, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR, - State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error - // bounces of the proceed message - )); - transitionBuilder.put( - State.SESSION_INITIALIZED, - ImmutableList.of( - State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors - // and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR)); - transitionBuilder.put( - State.SESSION_INITIALIZED_PRE_APPROVED, - ImmutableList.of( - State.SESSION_ACCEPTED, - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors - // and IQ timeouts - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR)); - transitionBuilder.put( - State.SESSION_ACCEPTED, - ImmutableList.of( - State.TERMINATED_SUCCESS, - State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_CONNECTIVITY_ERROR, - State.TERMINATED_CANCEL_OR_TIMEOUT, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR)); - VALID_TRANSITIONS = transitionBuilder.build(); - } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final Queue> + private final Queue>> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; - private State state = State.NULL; + private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; @@ -192,18 +104,6 @@ public class JingleRtpConnection extends AbstractJingleConnection id.sessionId); } - private static State reasonToState(Reason reason) { - return switch (reason) { - case SUCCESS -> State.TERMINATED_SUCCESS; - 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; - default -> State.TERMINATED_CONNECTIVITY_ERROR; - }; - } - @Override synchronized void deliverPacket(final JinglePacket jinglePacket) { switch (jinglePacket.getAction()) { @@ -233,7 +133,7 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } webRTCWrapper.close(); - if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { + if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } if (isInState( @@ -322,7 +222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void receiveTransportInfo( final JinglePacket jinglePacket, final RtpContentMap contentMap) { - final Set> candidates = + final Set>> candidates = contentMap.contents.entrySet(); final RtpContentMap remote = getRemoteContentMap(); final Set remoteContentIds = @@ -522,7 +422,7 @@ public class JingleRtpConnection extends AbstractJingleConnection setRemoteContentMap(modifiedContentMap); - final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator()); + final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder()); final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( @@ -596,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } final SessionDescription offer; try { - offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator()); + offer = SessionDescription.of(modifiedRemoteContentMap, isResponder()); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -815,7 +715,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap nextRemote = currentRemote.addContent( patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup()); - return SessionDescription.of(nextRemote, !isInitiator()); + return SessionDescription.of(nextRemote, isResponder()); } throw new IllegalStateException( "Unexpected rollback condition. Senders were not uniformly none"); @@ -881,7 +781,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final SessionDescription offer; try { - offer = SessionDescription.of(modifiedContentMap, !isInitiator()); + offer = SessionDescription.of(modifiedContentMap, isResponder()); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -1066,7 +966,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final boolean isOffer) throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = - SessionDescription.of(restartContentMap, !isInitiator()); + SessionDescription.of(restartContentMap, isResponder()); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER @@ -1095,14 +995,14 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void processCandidates( - final Set> contents) { - for (final Map.Entry content : contents) { + final Set>> contents) { + for (final Map.Entry> content : contents) { processCandidate(content); } } private void processCandidate( - final Map.Entry content) { + final Map.Entry> content) { final RtpContentMap rtpContentMap = getRemoteContentMap(); final List indices = toIdentificationTags(rtpContentMap); final String sdpMid = content.getKey(); // aka content name @@ -1204,21 +1104,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { - Log.d( - Config.LOGTAG, - String.format( - "%s: received session-initiate even though we were initiating", - id.account.getJid().asBareJid())); - if (isTerminated()) { - Log.d( - Config.LOGTAG, - String.format( - "%s: got a reason to terminate with out-of-order. but already in state %s", - id.account.getJid().asBareJid(), getState())); - respondWithOutOfOrder(jinglePacket); - } else { - terminateWithOutOfOrder(jinglePacket); - } + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); return; } final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); @@ -1300,13 +1186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveSessionAccept(final JinglePacket jinglePacket) { - if (!isInitiator()) { - Log.d( - Config.LOGTAG, - String.format( - "%s: received session-accept even though we were responding", - id.account.getJid().asBareJid())); - terminateWithOutOfOrder(jinglePacket); + if (isResponder()) { + receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); return; } final ListenableFuture future = @@ -1491,7 +1372,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void addIceCandidatesFromBlackLog() { - Map.Entry foo; + Map.Entry> foo; while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidate(foo); Log.d( @@ -2061,24 +1942,16 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void sendSessionTerminate(final Reason reason) { + protected void sendSessionTerminate(final Reason reason) { sendSessionTerminate(reason, null); } - private void sendSessionTerminate(final Reason reason, final String text) { - final State previous = this.state; - final State target = reasonToState(reason); - transitionOrThrow(target); - if (previous != State.NULL) { - writeLogMessage(target); - } - final JinglePacket jinglePacket = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - jinglePacket.setReason(reason, text); - send(jinglePacket); - finish(); + + protected void sendSessionTerminate(final Reason reason, final String text) { + sendSessionTerminate(reason,text, this::writeLogMessage); } + private void sendTransportInfo( final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; @@ -2099,110 +1972,6 @@ public class JingleRtpConnection extends AbstractJingleConnection send(jinglePacket); } - private void send(final JinglePacket jinglePacket) { - jinglePacket.setTo(id.with); - xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse); - } - - private synchronized void handleIqResponse(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.ERROR) { - handleIqErrorResponse(response); - return; - } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - handleIqTimeoutResponse(response); - } - } - - private void handleIqErrorResponse(final IqPacket response) { - Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); - final String errorCondition = response.getErrorCondition(); - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": received IQ-error from " - + response.getFrom() - + " in RTP session. " - + errorCondition); - if (isTerminated()) { - Log.i( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - final State target; - if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout") - .contains(errorCondition)) { - target = State.TERMINATED_CONNECTIVITY_ERROR; - } else { - target = State.TERMINATED_APPLICATION_FAILURE; - } - transitionOrThrow(target); - this.finish(); - } - - private void handleIqTimeoutResponse(final IqPacket response) { - Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": received IQ timeout in RTP session with " - + id.with - + ". terminating with connectivity error"); - if (isTerminated()) { - Log.i( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - this.finish(); - } - - private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() + ": terminating session with out-of-order"); - this.webRTCWrapper.close(); - transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); - respondWithOutOfOrder(jinglePacket); - this.finish(); - } - - private void respondWithTieBreak(final JinglePacket jinglePacket) { - respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); - } - - private void respondWithOutOfOrder(final JinglePacket jinglePacket) { - respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); - } - - private void respondWithItemNotFound(final JinglePacket jinglePacket) { - respondWithJingleError(jinglePacket, null, "item-not-found", "cancel"); - } - - void respondWithJingleError( - final IqPacket original, - String jingleCondition, - String condition, - String conditionType) { - jingleConnectionManager.respondWithJingleError( - id.account, original, jingleCondition, condition, conditionType); - } - - private void respondOk(final JinglePacket jinglePacket) { - xmppConnectionService.sendIqPacket( - id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); - } - public RtpEndUserState getEndUserState() { switch (this.state) { case NULL, PROPOSED, SESSION_INITIALIZED -> { @@ -2398,7 +2167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection + ": received endCall() when session has already been terminated. nothing to do"); return; } - if (isInState(State.PROPOSED) && !isInitiator()) { + if (isInState(State.PROPOSED) && isResponder()) { rejectCallFromProposed(); return; } @@ -2527,22 +2296,10 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionAccept(); } - private synchronized boolean isInState(State... state) { - return Arrays.asList(state).contains(this.state); - } - private boolean transition(final State target) { - return transition(target, null); - } - - private synchronized boolean transition(final State target, final Runnable runnable) { - final Collection validTransitions = VALID_TRANSITIONS.get(this.state); - if (validTransitions != null && validTransitions.contains(target)) { - this.state = target; - if (runnable != null) { - runnable.run(); - } - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); + @Override + protected synchronized boolean transition(final State target, final Runnable runnable) { + if (super.transition(target, runnable)) { updateEndUserState(); updateOngoingCallNotification(); return true; @@ -2551,13 +2308,6 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - void transitionOrThrow(final State target) { - if (!transition(target)) { - throw new IllegalStateException( - String.format("Unable to transition from %s to %s", this.state, target)); - } - } - @Override public void onIceCandidate(final IceCandidate iceCandidate) { final RtpContentMap rtpContentMap = @@ -2893,98 +2643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection id.account, request, (account, response) -> { - ImmutableList.Builder listBuilder = - new ImmutableList.Builder<>(); - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element services = - response.findChild( - "services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = - services == null - ? Collections.emptyList() - : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String sport = child.getAttribute("port"); - final Integer port = - sport == null ? null : Ints.tryParse(sport); - final String transport = child.getAttribute("transport"); - final String username = child.getAttribute("username"); - final String password = child.getAttribute("password"); - if (Strings.isNullOrEmpty(host) || port == null) { - continue; - } - if (port < 0 || port > 65535) { - continue; - } - - if (Arrays.asList("stun", "stuns", "turn", "turns") - .contains(type) - && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns", "turns").contains(type) - && "udp".equals(transport)) { - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": skipping invalid combination of udp/tls in external services"); - continue; - } - - // STUN URLs do not support a query section since M110 - final String uri; - if (Arrays.asList("stun", "stuns").contains(type)) { - uri = - String.format( - "%s:%s:%s", - type, IP.wrapIPv6(host), port); - } else { - uri = - String.format( - "%s:%s:%s?transport=%s", - type, - IP.wrapIPv6(host), - port, - transport); - } - - final PeerConnection.IceServer.Builder iceServerBuilder = - PeerConnection.IceServer.builder(uri); - iceServerBuilder.setTlsCertPolicy( - PeerConnection.TlsCertPolicy - .TLS_CERT_POLICY_INSECURE_NO_CHECK); - if (username != null && password != null) { - iceServerBuilder.setUsername(username); - iceServerBuilder.setPassword(password); - } else if (Arrays.asList("turn", "turns").contains(type)) { - // The WebRTC spec requires throwing an - // InvalidAccessError when username (from libwebrtc - // source coder) - // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": skipping " - + type - + "/" - + transport - + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = - iceServerBuilder.createIceServer(); - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": discovered ICE Server: " - + iceServer); - listBuilder.add(iceServer); - } - } - } - } - final List iceServers = listBuilder.build(); + final var iceServers = IceServers.parse(response); if (iceServers.size() == 0) { Log.w( Config.LOGTAG, @@ -3001,13 +2660,19 @@ public class JingleRtpConnection extends AbstractJingleConnection onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); } } + + @Override + protected void terminateTransport() { + this.webRTCWrapper.close(); + } - private void finish() { + @Override + protected void finish() { if (isTerminated()) { this.cancelRingingTimeout(); this.webRTCWrapper.verifyClosed(); this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); - this.jingleConnectionManager.finishConnectionOrThrow(this); + super.finish(); } else { throw new IllegalStateException( String.format("Unable to call finish from %s", this.state)); @@ -3045,14 +2710,6 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - public State getState() { - return this.state; - } - - boolean isTerminated() { - return TERMINATED.contains(this.state); - } - public Optional getLocalVideoTrack() { return webRTCWrapper.getLocalVideoTrack(); } @@ -3091,17 +2748,6 @@ public class JingleRtpConnection extends AbstractJingleConnection return remoteHasFeature(Namespace.SDP_OFFER_ANSWER); } - private boolean remoteHasFeature(final String feature) { - final Contact contact = id.getContact(); - final Presence presence = - contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); - final ServiceDiscoveryResult serviceDiscoveryResult = - presence == null ? null : presence.getServiceDiscoveryResult(); - final List features = - serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); - return features != null && features.contains(feature); - } - private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java deleted file mode 100644 index a57f4927f..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ /dev/null @@ -1,305 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import android.os.PowerManager; -import android.util.Log; - -import com.google.common.io.ByteStreams; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketAddress; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.SocksSocketFactory; -import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; - -public class JingleSocks5Transport extends JingleTransport { - - private static final int SOCKET_TIMEOUT_DIRECT = 3000; - private static final int SOCKET_TIMEOUT_PROXY = 5000; - - private final JingleCandidate candidate; - private final JingleFileTransferConnection connection; - private final String destination; - private final Account account; - private OutputStream outputStream; - private InputStream inputStream; - private boolean isEstablished = false; - private boolean activated = false; - private ServerSocket serverSocket; - private Socket socket; - - JingleSocks5Transport(JingleFileTransferConnection jingleConnection, JingleCandidate candidate) { - final MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - this.candidate = candidate; - this.connection = jingleConnection; - this.account = jingleConnection.getId().account; - final StringBuilder destBuilder = new StringBuilder(); - if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) { - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); - destBuilder.append(this.connection.getId().sessionId); - } else { - destBuilder.append(this.connection.getTransportId()); - } - if (candidate.isOurs()) { - destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().with); - } else { - destBuilder.append(this.connection.getId().with); - destBuilder.append(this.account.getJid()); - } - messageDigest.reset(); - this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes())); - if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) { - createServerSocket(); - } - } - - private void createServerSocket() { - try { - serverSocket = new ServerSocket(); - serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort())); - new Thread(() -> { - try { - final Socket socket = serverSocket.accept(); - new Thread(() -> { - try { - acceptIncomingSocketConnection(socket); - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to read from socket", e); - - } - }).start(); - } catch (IOException e) { - if (!serverSocket.isClosed()) { - Log.d(Config.LOGTAG, "unable to accept socket", e); - } - } - }).start(); - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to bind server socket ", e); - } - } - - private void acceptIncomingSocketConnection(final Socket socket) throws IOException { - Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress()); - socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT); - final byte[] authBegin = new byte[2]; - final InputStream inputStream = socket.getInputStream(); - final OutputStream outputStream = socket.getOutputStream(); - ByteStreams.readFully(inputStream, authBegin); - if (authBegin[0] != 0x5) { - socket.close(); - } - final short methodCount = authBegin[1]; - final byte[] methods = new byte[methodCount]; - ByteStreams.readFully(inputStream, methods); - if (SocksSocketFactory.contains((byte) 0x00, methods)) { - outputStream.write(new byte[]{0x05, 0x00}); - } else { - outputStream.write(new byte[]{0x05, (byte) 0xff}); - } - final byte[] connectCommand = new byte[4]; - ByteStreams.readFully(inputStream, connectCommand); - if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) { - int destinationCount = inputStream.read(); - final byte[] destination = new byte[destinationCount]; - ByteStreams.readFully(inputStream, destination); - final byte[] port = new byte[2]; - ByteStreams.readFully(inputStream, port); - final String receivedDestination = new String(destination); - final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); - final byte[] responseHeader; - final boolean success; - if (receivedDestination.equals(this.destination) && this.socket == null) { - responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03}; - success = true; - } else { - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")"); - responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03}; - success = false; - } - response.put(responseHeader); - response.put((byte) destination.length); - response.put(destination); - response.put(port); - outputStream.write(response.array()); - outputStream.flush(); - if (success) { - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort()); - socket.setSoTimeout(0); - this.socket = socket; - this.inputStream = inputStream; - this.outputStream = outputStream; - this.isEstablished = true; - FileBackend.close(serverSocket); - } else { - FileBackend.close(socket); - } - } else { - socket.close(); - } - } - - public void connect(final OnTransportConnected callback) { - new Thread(() -> { - final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY; - try { - final boolean useTor = this.account.isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); - if (useTor) { - socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort()); - } else { - socket = new Socket(); - SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort()); - socket.connect(address, timeout); - } - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); - socket.setSoTimeout(timeout); - SocksSocketFactory.createSocksConnection(socket, destination, 0); - socket.setSoTimeout(0); - isEstablished = true; - callback.established(); - } catch (final IOException e) { - callback.failed(); - } - }).start(); - - } - - public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { - new Thread(() -> { - InputStream fileInputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getId().sessionId); - long transmitted = 0; - try { - wakeLock.acquire(); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - fileInputStream = connection.getFileInputStream(); - if (fileInputStream == null) { - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create input stream"); - callback.onFileTransferAborted(); - return; - } - final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - long size = file.getExpectedSize(); - int count; - byte[] buffer = new byte[8192]; - while ((count = innerInputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, count); - digest.update(buffer, 0, count); - transmitted += count; - connection.updateProgress((int) ((((double) transmitted) / size) * 100)); - } - outputStream.flush(); - file.setSha1Sum(digest.digest()); - if (callback != null) { - callback.onFileTransmitted(file); - } - } catch (Exception e) { - final Account account = this.account; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e); - callback.onFileTransferAborted(); - } finally { - FileBackend.close(fileInputStream); - WakeLockHelper.release(wakeLock); - } - }).start(); - - } - - public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { - new Thread(() -> { - OutputStream fileOutputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getId().sessionId); - try { - wakeLock.acquire(); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - //inputStream.skip(45); - socket.setSoTimeout(30000); - fileOutputStream = connection.getFileOutputStream(); - if (fileOutputStream == null) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create output stream"); - return; - } - double size = file.getExpectedSize(); - long remainingSize = file.getExpectedSize(); - byte[] buffer = new byte[8192]; - int count; - while (remainingSize > 0) { - count = inputStream.read(buffer); - if (count == -1) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining"); - return; - } else { - fileOutputStream.write(buffer, 0, count); - digest.update(buffer, 0, count); - remainingSize -= count; - } - connection.updateProgress((int) (((size - remainingSize) / size) * 100)); - } - fileOutputStream.flush(); - fileOutputStream.close(); - file.setSha1Sum(digest.digest()); - callback.onFileTransmitted(file); - } catch (Exception e) { - Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": " + e.getMessage()); - callback.onFileTransferAborted(); - } finally { - WakeLockHelper.release(wakeLock); - FileBackend.close(fileOutputStream); - FileBackend.close(inputStream); - } - }).start(); - } - - public boolean isProxy() { - return this.candidate.getType() == JingleCandidate.TYPE_PROXY; - } - - public boolean needsActivation() { - return (this.isProxy() && !this.activated); - } - - public void disconnect() { - FileBackend.close(inputStream); - FileBackend.close(outputStream); - FileBackend.close(socket); - FileBackend.close(serverSocket); - } - - public boolean isEstablished() { - return this.isEstablished; - } - - public JingleCandidate getCandidate() { - return this.candidate; - } - - public void setActivated(boolean activated) { - this.activated = activated; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java deleted file mode 100644 index e832d3f58..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import eu.siacs.conversations.entities.DownloadableFile; - -public abstract class JingleTransport { - public abstract void connect(final OnTransportConnected callback); - - public abstract void receive(final DownloadableFile file, - final OnFileTransmissionStatusChanged callback); - - public abstract void send(final DownloadableFile file, - final OnFileTransmissionStatusChanged callback); - - public abstract void disconnect(); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java index 67e275414..db33666cb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; -import com.google.common.collect.ArrayListMultimap; +import com.google.common.base.Joiner; +import com.google.common.collect.Multimap; import java.util.List; @@ -8,9 +9,9 @@ public class MediaBuilder { private String media; private int port; private String protocol; - private List formats; + private String format; private String connectionData; - private ArrayListMultimap attributes; + private Multimap attributes; public MediaBuilder setMedia(String media) { this.media = media; @@ -27,8 +28,13 @@ public class MediaBuilder { return this; } - public MediaBuilder setFormats(List formats) { - this.formats = formats; + public MediaBuilder setFormats(final List formats) { + this.format = Joiner.on(' ').join(formats); + return this; + } + + public MediaBuilder setFormat(final String format) { + this.format = format; return this; } @@ -37,12 +43,13 @@ public class MediaBuilder { return this; } - public MediaBuilder setAttributes(ArrayListMultimap attributes) { + public MediaBuilder setAttributes(Multimap attributes) { this.attributes = attributes; return this; } public SessionDescription.Media createMedia() { - return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes); + return new SessionDescription.Media( + media, port, protocol, format, connectionData, attributes); } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java index f5e041014..0d5d32d50 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java @@ -3,12 +3,14 @@ package eu.siacs.conversations.xmpp.jingle; import java.util.Map; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class OmemoVerifiedRtpContentMap extends RtpContentMap { - public OmemoVerifiedRtpContentMap(Group group, Map contents) { + public OmemoVerifiedRtpContentMap(Group group, Map> contents) { super(group, contents); - for(final DescriptionTransport descriptionTransport : contents.values()) { + for(final DescriptionTransport descriptionTransport : contents.values()) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { ((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint(); continue; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java deleted file mode 100644 index 76e337177..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -public interface OnPrimaryCandidateFound { - void onPrimaryCandidateFound(boolean success, JingleCandidate canditate); -} 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 cfd4bef78..94f8ca300 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -6,7 +6,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; @@ -31,19 +30,17 @@ import java.util.Set; import javax.annotation.Nonnull; -public class RtpContentMap { +public class RtpContentMap extends AbstractContentMap { - public final Group group; - public final Map contents; - - public RtpContentMap(Group group, Map contents) { - this.group = group; - this.contents = contents; + public RtpContentMap( + Group group, + Map> contents) { + super(group, contents); } public static RtpContentMap of(final JinglePacket jinglePacket) { - final Map contents = - DescriptionTransport.of(jinglePacket.getJingleContents()); + final Map> contents = + of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); } else { @@ -51,12 +48,15 @@ public class RtpContentMap { } } - private static boolean isOmemoVerified(Map contents) { - final Collection values = contents.values(); + private static boolean isOmemoVerified( + Map> contents) { + final Collection> values = + contents.values(); if (values.size() == 0) { return false; } - for (final DescriptionTransport descriptionTransport : values) { + for (final DescriptionTransport descriptionTransport : + values) { if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) { continue; } @@ -67,13 +67,13 @@ public class RtpContentMap { public static RtpContentMap of( final SessionDescription sessionDescription, final boolean isInitiator) { - final ImmutableMap.Builder contentMapBuilder = - new ImmutableMap.Builder<>(); + final ImmutableMap.Builder< + String, DescriptionTransport> + contentMapBuilder = new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); - contentMapBuilder.put( - id, DescriptionTransport.of(sessionDescription, isInitiator, media)); + contentMapBuilder.put(id, of(sessionDescription, isInitiator, media)); } final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); @@ -94,26 +94,6 @@ public class RtpContentMap { })); } - public Set getSenders() { - return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders)); - } - - public List getNames() { - return ImmutableList.copyOf(contents.keySet()); - } - - void requireContentDescriptions() { - if (this.contents.size() == 0) { - throw new IllegalStateException("No contents available"); - } - for (Map.Entry entry : this.contents.entrySet()) { - if (entry.getValue().description == null) { - throw new IllegalStateException( - String.format("%s is lacking content description", entry.getKey())); - } - } - } - void requireDTLSFingerprint() { requireDTLSFingerprint(false); } @@ -122,7 +102,8 @@ public class RtpContentMap { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } - for (Map.Entry entry : this.contents.entrySet()) { + for (Map.Entry> entry : + this.contents.entrySet()) { final IceUdpTransportInfo transport = entry.getValue().transport; final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint == null @@ -146,31 +127,10 @@ public class RtpContentMap { } } } - - JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { - final JinglePacket jinglePacket = new JinglePacket(action, sessionId); - if (this.group != null) { - jinglePacket.addGroup(this.group); - } - for (Map.Entry entry : this.contents.entrySet()) { - final DescriptionTransport descriptionTransport = entry.getValue(); - final Content content = - new Content( - Content.Creator.INITIATOR, - descriptionTransport.senders, - entry.getKey()); - if (descriptionTransport.description != null) { - content.addChild(descriptionTransport.description); - } - content.addChild(descriptionTransport.transport); - jinglePacket.addJingleContent(content); - } - return jinglePacket; - } - RtpContentMap transportInfo( final String contentName, final IceUdpTransportInfo.Candidate candidate) { - final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); + final DescriptionTransport descriptionTransport = + contents.get(contentName); final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { @@ -183,7 +143,7 @@ public class RtpContentMap { null, ImmutableMap.of( contentName, - new DescriptionTransport( + new DescriptionTransport<>( descriptionTransport.senders, null, newTransportInfo))); } @@ -193,21 +153,24 @@ public class RtpContentMap { Maps.transformValues( contents, dt -> - new DescriptionTransport( + new DescriptionTransport<>( dt.senders, null, dt.transport.cloneWrapper()))); } RtpContentMap withCandidates( ImmutableMultimap candidates) { - final ImmutableMap.Builder contentBuilder = - new ImmutableMap.Builder<>(); - for (final Map.Entry entry : this.contents.entrySet()) { + final ImmutableMap.Builder< + String, DescriptionTransport> + contentBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry> + entry : this.contents.entrySet()) { final String name = entry.getKey(); - final DescriptionTransport descriptionTransport = entry.getValue(); + final DescriptionTransport descriptionTransport = + entry.getValue(); final var transport = descriptionTransport.transport; contentBuilder.put( name, - new DescriptionTransport( + new DescriptionTransport<>( descriptionTransport.senders, descriptionTransport.description, transport.withCandidates(candidates.get(name)))); @@ -247,7 +210,7 @@ public class RtpContentMap { } public IceUdpTransportInfo.Credentials getCredentials(final String contentName) { - final DescriptionTransport descriptionTransport = this.contents.get(contentName); + final var descriptionTransport = this.contents.get(contentName); if (descriptionTransport == null) { throw new IllegalArgumentException( String.format( @@ -287,7 +250,7 @@ public class RtpContentMap { public boolean emptyCandidates() { int count = 0; - for (DescriptionTransport descriptionTransport : contents.values()) { + for (final var descriptionTransport : contents.values()) { count += descriptionTransport.transport.getCandidates().size(); } return count == 0; @@ -300,17 +263,19 @@ public class RtpContentMap { public RtpContentMap modifiedCredentials( IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { - final ImmutableMap.Builder contentMapBuilder = - new ImmutableMap.Builder<>(); - for (final Map.Entry content : contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + final ImmutableMap.Builder< + String, DescriptionTransport> + contentMapBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry> + content : contents.entrySet()) { + final var descriptionTransport = content.getValue(); final RtpDescription rtpDescription = descriptionTransport.description; final IceUdpTransportInfo transportInfo = descriptionTransport.transport; final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); contentMapBuilder.put( content.getKey(), - new DescriptionTransport( + new DescriptionTransport<>( descriptionTransport.senders, rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); @@ -321,16 +286,18 @@ public class RtpContentMap { this.group, Maps.transformValues( contents, - dt -> new DescriptionTransport(senders, dt.description, dt.transport))); + dt -> new DescriptionTransport<>(senders, dt.description, dt.transport))); } public RtpContentMap modifiedSendersChecked( final boolean isInitiator, final Map modification) { - final ImmutableMap.Builder contentMapBuilder = - new ImmutableMap.Builder<>(); - for (final Map.Entry content : contents.entrySet()) { + final ImmutableMap.Builder< + String, DescriptionTransport> + contentMapBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry> + content : contents.entrySet()) { final String id = content.getKey(); - final DescriptionTransport descriptionTransport = content.getValue(); + final var descriptionTransport = content.getValue(); final Content.Senders currentSenders = descriptionTransport.senders; final Content.Senders targetSenders = modification.get(id); if (targetSenders == null || currentSenders == targetSenders) { @@ -339,7 +306,7 @@ public class RtpContentMap { checkSenderModification(isInitiator, currentSenders, targetSenders); contentMapBuilder.put( id, - new DescriptionTransport( + new DescriptionTransport<>( targetSenders, descriptionTransport.description, descriptionTransport.transport)); @@ -386,7 +353,7 @@ public class RtpContentMap { Maps.transformValues( this.contents, dt -> - new DescriptionTransport( + new DescriptionTransport<>( dt.senders, RtpDescription.stub(dt.description.getMedia()), IceUdpTransportInfo.STUB))); @@ -415,120 +382,96 @@ public class RtpContentMap { public RtpContentMap addContent( final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) { - final Map combined = merge(contents, modification.contents); - final Map combinedFixedTransport = - Maps.transformValues( - combined, - dt -> { - final IceUdpTransportInfo iceUdpTransportInfo; - if (dt.transport.isStub()) { - final IceUdpTransportInfo.Credentials credentials = - getDistinctCredentials(); - final Collection iceOptions = getCombinedIceOptions(); - final DTLS dtls = getDistinctDtls(); - iceUdpTransportInfo = - IceUdpTransportInfo.of( - credentials, - iceOptions, - setupOverwrite, - dtls.hash, - dtls.fingerprint); - } else { - final IceUdpTransportInfo.Fingerprint fp = - dt.transport.getFingerprint(); - final IceUdpTransportInfo.Setup setup = fp.getSetup(); - iceUdpTransportInfo = - IceUdpTransportInfo.of( - dt.transport.getCredentials(), - dt.transport.getIceOptions(), - setup == IceUdpTransportInfo.Setup.ACTPASS - ? setupOverwrite - : setup, - fp.getHash(), - fp.getContent()); - } - return new DescriptionTransport( - dt.senders, dt.description, iceUdpTransportInfo); - }); + final Map> combined = + merge(contents, modification.contents); + final Map> + combinedFixedTransport = + Maps.transformValues( + combined, + dt -> { + final IceUdpTransportInfo iceUdpTransportInfo; + if (dt.transport.isStub()) { + final IceUdpTransportInfo.Credentials credentials = + getDistinctCredentials(); + final Collection iceOptions = + getCombinedIceOptions(); + final DTLS dtls = getDistinctDtls(); + iceUdpTransportInfo = + IceUdpTransportInfo.of( + credentials, + iceOptions, + setupOverwrite, + dtls.hash, + dtls.fingerprint); + } else { + final IceUdpTransportInfo.Fingerprint fp = + dt.transport.getFingerprint(); + final IceUdpTransportInfo.Setup setup = fp.getSetup(); + iceUdpTransportInfo = + IceUdpTransportInfo.of( + dt.transport.getCredentials(), + dt.transport.getIceOptions(), + setup == IceUdpTransportInfo.Setup.ACTPASS + ? setupOverwrite + : setup, + fp.getHash(), + fp.getContent()); + } + return new DescriptionTransport<>( + dt.senders, dt.description, iceUdpTransportInfo); + }); return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport)); } - private static Map merge( - final Map a, final Map b) { - final Map combined = new LinkedHashMap<>(); + private static Map> merge( + final Map> a, + final Map> b) { + final Map> combined = + new LinkedHashMap<>(); combined.putAll(a); combined.putAll(b); return ImmutableMap.copyOf(combined); } - public static class DescriptionTransport { - public final Content.Senders senders; - public final RtpDescription description; - public final IceUdpTransportInfo transport; - - public DescriptionTransport( - final Content.Senders senders, - final RtpDescription description, - final IceUdpTransportInfo transport) { - this.senders = senders; - this.description = description; - this.transport = transport; + public static DescriptionTransport of( + final Content content) { + final GenericDescription description = content.getDescription(); + final GenericTransportInfo transportInfo = content.getTransport(); + final Content.Senders senders = content.getSenders(); + final RtpDescription rtpDescription; + final IceUdpTransportInfo iceUdpTransportInfo; + if (description == null) { + rtpDescription = null; + } else if (description instanceof RtpDescription) { + rtpDescription = (RtpDescription) description; + } else { + throw new UnsupportedApplicationException("Content does not contain rtp description"); } - - public static DescriptionTransport of(final Content content) { - final GenericDescription description = content.getDescription(); - final GenericTransportInfo transportInfo = content.getTransport(); - final Content.Senders senders = content.getSenders(); - final RtpDescription rtpDescription; - final IceUdpTransportInfo iceUdpTransportInfo; - if (description == null) { - rtpDescription = null; - } else if (description instanceof RtpDescription) { - rtpDescription = (RtpDescription) description; - } else { - throw new UnsupportedApplicationException( - "Content does not contain rtp description"); - } - if (transportInfo instanceof IceUdpTransportInfo) { - iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; - } else { - throw new UnsupportedTransportException( - "Content does not contain ICE-UDP transport"); - } - return new DescriptionTransport( - senders, - rtpDescription, - OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); - } - - private static DescriptionTransport of( - final SessionDescription sessionDescription, - final boolean isInitiator, - final SessionDescription.Media media) { - final Content.Senders senders = Content.Senders.of(media, isInitiator); - final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); - final IceUdpTransportInfo transportInfo = - IceUdpTransportInfo.of(sessionDescription, media); - return new DescriptionTransport(senders, rtpDescription, transportInfo); - } - - public static Map of(final Map contents) { - return ImmutableMap.copyOf( - Maps.transformValues( - contents, content -> content == null ? null : of(content))); + if (transportInfo instanceof IceUdpTransportInfo) { + iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; + } else { + throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); } + return new DescriptionTransport<>( + senders, + rtpDescription, + OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static class UnsupportedApplicationException extends IllegalArgumentException { - UnsupportedApplicationException(String message) { - super(message); - } + private static DescriptionTransport of( + final SessionDescription sessionDescription, + final boolean isInitiator, + final SessionDescription.Media media) { + final Content.Senders senders = Content.Senders.of(media, isInitiator); + final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); + final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); + return new DescriptionTransport<>(senders, rtpDescription, transportInfo); } - public static class UnsupportedTransportException extends IllegalArgumentException { - UnsupportedTransportException(String message) { - super(message); - } + private static Map> of( + final Map contents) { + return ImmutableMap.copyOf( + Maps.transformValues(contents, content -> content == null ? null : of(content))); } public static final class Diff { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 2d2dc9570..025a7acc9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -10,12 +10,17 @@ import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; import java.util.Collection; import java.util.Collections; @@ -28,6 +33,8 @@ public class SessionDescription { public static final String LINE_DIVIDER = "\r\n"; private static final String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint + private static final String HARDCODED_APPLICATION_PROTOCOL = "UDP/DTLS/SCTP"; + private static final String FORMAT_WEBRTC_DATA_CHANNEL = "webrtc-datachannel"; private static final int HARDCODED_MEDIA_PORT = 9; private static final Collection HARDCODED_ICE_OPTIONS = Collections.singleton("trickle"); @@ -52,9 +59,8 @@ public class SessionDescription { this.media = media; } - private static void appendAttributes( - StringBuilder s, ArrayListMultimap attributes) { - for (Map.Entry attribute : attributes.entries()) { + private static void appendAttributes(StringBuilder s, Multimap attributes) { + for (final Map.Entry attribute : attributes.entries()) { final String key = attribute.getKey(); final String value = attribute.getValue(); s.append("a=").append(key); @@ -79,24 +85,20 @@ public class SessionDescription { final char key = pair[0].charAt(0); final String value = pair[1]; switch (key) { - case 'v': - sessionDescriptionBuilder.setVersion(ignorantIntParser(value)); - break; - case 'c': + case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value)); + case 'c' -> { if (currentMediaBuilder != null) { currentMediaBuilder.setConnectionData(value); } else { sessionDescriptionBuilder.setConnectionData(value); } - break; - case 's': - sessionDescriptionBuilder.setName(value); - break; - case 'a': + } + case 's' -> sessionDescriptionBuilder.setName(value); + case 'a' -> { final Pair attribute = parseAttribute(value); attributeMap.put(attribute.first, attribute.second); - break; - case 'm': + } + case 'm' -> { if (currentMediaBuilder == null) { sessionDescriptionBuilder.setAttributes(attributeMap); } else { @@ -118,7 +120,7 @@ public class SessionDescription { } else { Log.d(Config.LOGTAG, "skipping media line " + line); } - break; + } } } if (currentMediaBuilder != null) { @@ -131,6 +133,56 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } + public static SessionDescription of(final FileTransferContentMap contentMap) { + final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); + final ArrayListMultimap attributeMap = ArrayListMultimap.create(); + final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); + + final Group group = contentMap.group; + if (group != null) { + final String semantics = group.getSemantics(); + checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); + final var idTags = group.getIdentificationTags(); + for (final String content : idTags) { + checkNoWhitespace(content, "group content names must not contain any whitespace"); + } + attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags)); + } + + // TODO my-media-stream can be removed I think + attributeMap.put("msid-semantic", " WMS my-media-stream"); + + for (final Map.Entry< + String, DescriptionTransport> + entry : contentMap.contents.entrySet()) { + final var dt = entry.getValue(); + final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo; + if (dt.transport instanceof WebRTCDataChannelTransportInfo transportInfo) { + webRTCDataChannelTransportInfo = transportInfo; + } else { + throw new IllegalArgumentException("Transport is not of type WebRTCDataChannel"); + } + final String name = entry.getKey(); + checkNoWhitespace(name, "content name must not contain any whitespace"); + + final MediaBuilder mediaBuilder = new MediaBuilder(); + mediaBuilder.setMedia("application"); + mediaBuilder.setConnectionData(HARDCODED_CONNECTION); + mediaBuilder.setPort(HARDCODED_MEDIA_PORT); + mediaBuilder.setProtocol(HARDCODED_APPLICATION_PROTOCOL); + mediaBuilder.setAttributes( + transportInfoMediaAttributes(webRTCDataChannelTransportInfo)); + mediaBuilder.setFormat(FORMAT_WEBRTC_DATA_CHANNEL); + mediaListBuilder.add(mediaBuilder.createMedia()); + } + + sessionDescriptionBuilder.setVersion(0); + sessionDescriptionBuilder.setName("-"); + sessionDescriptionBuilder.setMedia(mediaListBuilder.build()); + sessionDescriptionBuilder.setAttributes(attributeMap); + return sessionDescriptionBuilder.createSessionDescription(); + } + public static SessionDescription of( final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); @@ -140,58 +192,27 @@ public class SessionDescription { if (group != null) { final String semantics = group.getSemantics(); checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); - attributeMap.put( - "group", - group.getSemantics() - + " " - + Joiner.on(' ').join(group.getIdentificationTags())); + final var idTags = group.getIdentificationTags(); + for (final String content : idTags) { + checkNoWhitespace(content, "group content names must not contain any whitespace"); + } + attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags)); } + // TODO my-media-stream can be removed I think attributeMap.put("msid-semantic", " WMS my-media-stream"); - for (final Map.Entry entry : - contentMap.contents.entrySet()) { + for (final Map.Entry> + entry : contentMap.contents.entrySet()) { final String name = entry.getKey(); - RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); - RtpDescription description = descriptionTransport.description; - IceUdpTransportInfo transport = descriptionTransport.transport; + checkNoWhitespace(name, "content name must not contain any whitespace"); + final DescriptionTransport descriptionTransport = + entry.getValue(); + final RtpDescription description = descriptionTransport.description; final ArrayListMultimap mediaAttributes = ArrayListMultimap.create(); - final String ufrag = transport.getAttribute("ufrag"); - final String pwd = transport.getAttribute("pwd"); - if (Strings.isNullOrEmpty(ufrag)) { - throw new IllegalArgumentException( - "Transport element is missing required ufrag attribute"); - } - checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); - mediaAttributes.put("ice-ufrag", ufrag); - if (Strings.isNullOrEmpty(pwd)) { - throw new IllegalArgumentException( - "Transport element is missing required pwd attribute"); - } - checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); - mediaAttributes.put("ice-pwd", pwd); - final List negotiatedIceOptions = transport.getIceOptions(); - final Collection iceOptions = - negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions; - mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions)); - final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); - if (fingerprint != null) { - final String hashFunction = fingerprint.getHash(); - final String hash = fingerprint.getContent(); - if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) { - throw new IllegalArgumentException("DTLS-SRTP missing hash"); - } - checkNoWhitespace( - hashFunction, "DTLS-SRTP hash function must not contain whitespace"); - checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace"); - mediaAttributes.put("fingerprint", hashFunction + " " + hash); - final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); - if (setup != null) { - mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); - } - } + mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport)); final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); - for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { + for (final RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { final String id = payloadType.getId(); if (Strings.isNullOrEmpty(id)) { throw new IllegalArgumentException("Payload type is missing id"); @@ -353,6 +374,69 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } + private static Multimap transportInfoMediaAttributes( + final IceUdpTransportInfo transport) { + final ArrayListMultimap mediaAttributes = ArrayListMultimap.create(); + final String ufrag = transport.getAttribute("ufrag"); + final String pwd = transport.getAttribute("pwd"); + if (Strings.isNullOrEmpty(ufrag)) { + throw new IllegalArgumentException( + "Transport element is missing required ufrag attribute"); + } + checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); + mediaAttributes.put("ice-ufrag", ufrag); + if (Strings.isNullOrEmpty(pwd)) { + throw new IllegalArgumentException( + "Transport element is missing required pwd attribute"); + } + checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); + mediaAttributes.put("ice-pwd", pwd); + final List negotiatedIceOptions = transport.getIceOptions(); + final Collection iceOptions = + negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions; + mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions)); + final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); + if (fingerprint != null) { + final String hashFunction = fingerprint.getHash(); + final String hash = fingerprint.getContent(); + if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) { + throw new IllegalArgumentException("DTLS-SRTP missing hash"); + } + checkNoWhitespace(hashFunction, "DTLS-SRTP hash function must not contain whitespace"); + checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace"); + mediaAttributes.put("fingerprint", hashFunction + " " + hash); + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup != null) { + mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); + } + } + return ImmutableMultimap.copyOf(mediaAttributes); + } + + private static Multimap transportInfoMediaAttributes( + final WebRTCDataChannelTransportInfo transport) { + final ArrayListMultimap mediaAttributes = ArrayListMultimap.create(); + final var iceUdpTransportInfo = transport.innerIceUdpTransportInfo(); + if (iceUdpTransportInfo == null) { + throw new IllegalArgumentException( + "Transport element is missing inner ice-udp transport"); + } + mediaAttributes.putAll(transportInfoMediaAttributes(iceUdpTransportInfo)); + final Integer sctpPort = transport.getSctpPort(); + if (sctpPort == null) { + throw new IllegalArgumentException( + "Transport element is missing required sctp-port attribute"); + } + mediaAttributes.put("sctp-port", String.valueOf(sctpPort)); + final Integer maxMessageSize = transport.getMaxMessageSize(); + if (maxMessageSize == null) { + throw new IllegalArgumentException( + "Transport element is missing required max-message-size"); + } + mediaAttributes.put("max-message-size", String.valueOf(maxMessageSize)); + return ImmutableMultimap.copyOf(mediaAttributes); + } + public static String checkNoWhitespace(final String input, final String message) { if (CharMatcher.whitespace().matchesAnyOf(input)) { throw new IllegalArgumentException(message); @@ -421,7 +505,7 @@ public class SessionDescription { .append(' ') .append(media.protocol) .append(' ') - .append(Joiner.on(' ').join(media.formats)) + .append(media.format) .append(LINE_DIVIDER); s.append("c=").append(media.connectionData).append(LINE_DIVIDER); appendAttributes(s, media.attributes); @@ -433,21 +517,21 @@ public class SessionDescription { public final String media; public final int port; public final String protocol; - public final List formats; + public final String format; public final String connectionData; - public final ArrayListMultimap attributes; + public final Multimap attributes; public Media( String media, int port, String protocol, - List formats, + String format, String connectionData, - ArrayListMultimap attributes) { + Multimap attributes) { this.media = media; this.port = port; this.protocol = protocol; - this.formats = formats; + this.format = format; this.connectionData = connectionData; this.attributes = attributes; } 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 2e53e4252..cb0c8579d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -406,7 +406,7 @@ public class WebRTCWrapper { } } - private static PeerConnection.RTCConfiguration buildConfiguration( + public static PeerConnection.RTCConfiguration buildConfiguration( final List iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); @@ -774,7 +774,7 @@ public class WebRTCWrapper { void onRenegotiationNeeded(); } - private abstract static class SetSdpObserver implements SdpObserver { + public abstract static class SetSdpObserver implements SdpObserver { @Override public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { @@ -800,12 +800,12 @@ public class WebRTCWrapper { public static class PeerConnectionNotInitialized extends IllegalStateException { - private PeerConnectionNotInitialized() { + public PeerConnectionNotInitialized() { super("initialize PeerConnection first"); } } - private static class FailureToSetDescriptionException extends IllegalArgumentException { + public static class FailureToSetDescriptionException extends IllegalArgumentException { public FailureToSetDescriptionException(String message) { super(message); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 0cca6527a..fbde212a3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -8,14 +8,14 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; -import java.util.Locale; -import java.util.Set; - import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.SessionDescription; +import java.util.Locale; +import java.util.Set; + public class Content extends Element { public Content(final Creator creator, final Senders senders, final String name) { @@ -65,7 +65,7 @@ public class Content extends Element { return null; } final String namespace = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(namespace)) { + if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) { return FileTransferDescription.upgrade(description); } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { return RtpDescription.upgrade(description); @@ -90,9 +90,11 @@ public class Content extends Element { if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { return IbbTransportInfo.upgrade(transport); } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { - return S5BTransportInfo.upgrade(transport); + return SocksByteStreamsTransportInfo.upgrade(transport); } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { return IceUdpTransportInfo.upgrade(transport); + } else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) { + return WebRTCDataChannelTransportInfo.upgrade(transport); } else if (transport != null) { return GenericTransportInfo.upgrade(transport); } else { @@ -100,7 +102,6 @@ public class Content extends Element { } } - public void setTransport(GenericTransportInfo transportInfo) { this.addChild(transportInfo); } @@ -141,7 +142,7 @@ public class Content extends Element { } else if (attributes.contains("recvonly")) { return initiator ? RESPONDER : INITIATOR; } - Log.w(Config.LOGTAG,"assuming default value for senders"); + Log.w(Config.LOGTAG, "assuming default value for senders"); // If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is // present, "sendrecv" SHOULD be assumed as the default // https://www.rfc-editor.org/rfc/rfc4566 diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java index 8e0f2ebad..3878d98d9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java @@ -1,89 +1,233 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.common.base.CaseFormat; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Longs; -import java.util.Arrays; -import java.util.List; - -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +import java.util.List; public class FileTransferDescription extends GenericDescription { - public static List NAMESPACES = Arrays.asList( - Version.FT_3.namespace, - Version.FT_4.namespace, - Version.FT_5.namespace - ); - - - private FileTransferDescription(String name, String namespace) { - super(name, namespace); + private FileTransferDescription() { + super("description", Namespace.JINGLE_APPS_FILE_TRANSFER); } - public Version getVersion() { - final String namespace = getNamespace(); - if (namespace.equals(Version.FT_3.namespace)) { - return Version.FT_3; - } else if (namespace.equals(Version.FT_4.namespace)) { - return Version.FT_4; - } else if (namespace.equals(Version.FT_5.namespace)) { - return Version.FT_5; - } else { - throw new IllegalStateException("Unknown namespace"); - } - } - - public Element getFileOffer() { - final Version version = getVersion(); - if (version == Version.FT_3) { - final Element offer = this.findChild("offer"); - return offer == null ? null : offer.findChild("file"); - } else { - return this.findChild("file"); - } - } - - public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) { - final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace()); - final Element fileElement; - if (version == Version.FT_3) { - Element offer = description.addChild("offer"); - fileElement = offer.addChild("file"); - } else { - fileElement = description.addChild("file"); - } - fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize())); - fileElement.addChild("name").setContent(file.getName()); - if (axolotlMessage != null) { - fileElement.addChild(axolotlMessage.toElement()); + public static FileTransferDescription of(final File fileDescription) { + final var description = new FileTransferDescription(); + final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER); + file.addChild("name").setContent(fileDescription.name); + file.addChild("size").setContent(Long.toString(fileDescription.size)); + if (fileDescription.mediaType != null) { + file.addChild("mediaType").setContent(fileDescription.mediaType); } return description; } + public File getFile() { + final Element fileElement = this.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER); + if (fileElement == null) { + Log.d(Config.LOGTAG,"no file? "+this); + throw new IllegalStateException("file transfer description has no file"); + } + final String name = fileElement.findChildContent("name"); + final String sizeAsString = fileElement.findChildContent("size"); + final String mediaType = fileElement.findChildContent("mediaType"); + if (Strings.isNullOrEmpty(name) || Strings.isNullOrEmpty(sizeAsString)) { + throw new IllegalStateException("File definition is missing name and/or size"); + } + final Long size = Longs.tryParse(sizeAsString); + if (size == null) { + throw new IllegalStateException("Invalid file size"); + } + final List hashes = findHashes(fileElement.getChildren()); + return new File(size, name, mediaType, hashes); + } + + public static SessionInfo getSessionInfo(@NonNull final JinglePacket jinglePacket) { + Preconditions.checkNotNull(jinglePacket); + Preconditions.checkArgument( + jinglePacket.getAction() == JinglePacket.Action.SESSION_INFO, + "jingle packet is not a session-info"); + final Element jingle = jinglePacket.findChild("jingle", Namespace.JINGLE); + if (jingle == null) { + return null; + } + final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER); + if (checksum != null) { + final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER); + final String name = checksum.getAttribute("name"); + if (file == null || Strings.isNullOrEmpty(name)) { + return null; + } + return new Checksum(name, findHashes(file.getChildren())); + } + final Element received = jingle.findChild("received", Namespace.JINGLE_APPS_FILE_TRANSFER); + if (received != null) { + final String name = received.getAttribute("name"); + if (Strings.isNullOrEmpty(name)) { + return new Received(name); + } + } + return null; + } + + private static List findHashes(final List elements) { + final ImmutableList.Builder hashes = new ImmutableList.Builder<>(); + for (final Element child : elements) { + if ("hash".equals(child.getName()) && Namespace.HASHES.equals(child.getNamespace())) { + final Algorithm algorithm; + try { + algorithm = Algorithm.of(child.getAttribute("algo")); + } catch (final IllegalArgumentException e) { + continue; + } + final String content = child.getContent(); + if (Strings.isNullOrEmpty(content)) { + continue; + } + if (BaseEncoding.base64().canDecode(content)) { + hashes.add(new Hash(BaseEncoding.base64().decode(content), algorithm)); + } + } + } + return hashes.build(); + } + public static FileTransferDescription upgrade(final Element element) { - Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); - Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace"); - final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace()); + Preconditions.checkArgument( + "description".equals(element.getName()), + "Name of provided element is not description"); + Preconditions.checkArgument( + element.getNamespace().equals(Namespace.JINGLE_APPS_FILE_TRANSFER), + "Element does not match a file transfer namespace"); + final FileTransferDescription description = new FileTransferDescription(); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); return description; } - public enum Version { - FT_3("urn:xmpp:jingle:apps:file-transfer:3"), - FT_4("urn:xmpp:jingle:apps:file-transfer:4"), - FT_5("urn:xmpp:jingle:apps:file-transfer:5"); + public static final class Checksum extends SessionInfo { + public final List hashes; - private final String namespace; - - Version(String namespace) { - this.namespace = namespace; + public Checksum(final String name, List hashes) { + super(name); + this.hashes = hashes; } - public String getNamespace() { - return namespace; + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this).add("hashes", hashes).toString(); + } + + @Override + public Element asElement() { + final var checksum = new Element("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER); + checksum.setAttribute("name", name); + final var file = checksum.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER); + for (final Hash hash : hashes) { + final var element = file.addChild("hash", Namespace.HASHES); + element.setAttribute( + "algo", + CaseFormat.UPPER_UNDERSCORE.to( + CaseFormat.LOWER_HYPHEN, hash.algorithm.toString())); + element.setContent(BaseEncoding.base64().encode(hash.hash)); + } + return checksum; + } + } + + public static final class Received extends SessionInfo { + + public Received(String name) { + super(name); + } + + @Override + public Element asElement() { + final var element = new Element("received", Namespace.JINGLE_APPS_FILE_TRANSFER); + element.setAttribute("name", name); + return element; + } + } + + public abstract static sealed class SessionInfo permits Checksum, Received { + + public final String name; + + protected SessionInfo(final String name) { + this.name = name; + } + + public abstract Element asElement(); + } + + public static class File { + public final long size; + public final String name; + public final String mediaType; + + public final List hashes; + + public File(long size, String name, String mediaType, List hashes) { + this.size = size; + this.name = name; + this.mediaType = mediaType; + this.hashes = hashes; + } + + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("size", size) + .add("name", name) + .add("mediaType", mediaType) + .add("hashes", hashes) + .toString(); + } + } + + public static class Hash { + public final byte[] hash; + public final Algorithm algorithm; + + public Hash(byte[] hash, Algorithm algorithm) { + this.hash = hash; + this.algorithm = algorithm; + } + + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("hash", hash) + .add("algorithm", algorithm) + .toString(); + } + } + + public enum Algorithm { + SHA_1, + SHA_256; + + public static Algorithm of(final String value) { + if (Strings.isNullOrEmpty(value)) { + return null; + } + return valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java index a8db0d09f..3bb3076a7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java @@ -8,6 +8,7 @@ public class GenericDescription extends Element { GenericDescription(String name, final String namespace) { super(name, namespace); + Preconditions.checkArgument("description".equals(name)); } public static GenericDescription upgrade(final Element element) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java index eb5c32252..1d2f7515f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java @@ -41,7 +41,7 @@ public class Group extends Element { } public static Group ofSdpString(final String input) { - ImmutableList.Builder tagBuilder = new ImmutableList.Builder<>(); + final ImmutableList.Builder tagBuilder = new ImmutableList.Builder<>(); final String[] parts = input.split(" "); if (parts.length >= 2) { final String semantics = parts[0]; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java index 90fb32903..ddab9640f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.primitives.Longs; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo { return this.getAttribute("sid"); } - public int getBlockSize() { + public Long getBlockSize() { final String blockSize = this.getAttribute("block-size"); - if (blockSize == null) { - return 0; - } - try { - return Integer.parseInt(blockSize); - } catch (NumberFormatException e) { - return 0; - } + return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize); } public static IbbTransportInfo upgrade(final Element element) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index ccaba56a6..8d90b1982 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -15,11 +15,13 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.SessionDescription; +import eu.siacs.conversations.xmpp.jingle.transports.Transport; import java.util.Arrays; import java.util.Collection; @@ -195,7 +197,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } } - public static class Candidate extends Element { + public static class Candidate extends Element implements Transport.Candidate { private Candidate() { super("candidate"); @@ -396,7 +398,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return fingerprint; } - private static Fingerprint of(ArrayListMultimap attributes) { + private static Fingerprint of(final Multimap attributes) { final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null); final String setup = Iterables.getFirst(attributes.get("setup"), null); if (setup != null && fingerprint != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 0863b29df..552046fb8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.common.base.CaseFormat; @@ -7,13 +9,16 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import java.util.Map; - +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import java.util.Map; + public class JinglePacket extends IqPacket { private JinglePacket() { @@ -36,7 +41,7 @@ public class JinglePacket extends IqPacket { return jinglePacket; } - //TODO deprecate this somehow and make file transfer fail if there are multiple (or something) + // TODO deprecate this somehow and make file transfer fail if there are multiple (or something) public Content getJingleContent() { final Element content = getJingleChild("content"); return content == null ? null : Content.upgrade(content); @@ -64,7 +69,7 @@ public class JinglePacket extends IqPacket { return builder.build(); } - public void addJingleContent(final Content content) { //take content interface + public void addJingleContent(final Content content) { // take content interface addJingleChild(content); } @@ -94,13 +99,13 @@ public class JinglePacket extends IqPacket { } } - //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise + // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise public void setInitiator(final Jid initiator) { Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID"); findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator); } - //RECOMMENDED for session-accept, NOT RECOMMENDED otherwise + // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise public void setResponder(Jid responder) { Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID"); findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder); @@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket { jingle.addChild(child); } + public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) { + final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); + security.setAttribute("name", name); + security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding"); + security.setAttribute("type", AxolotlService.PEP_PREFIX); + security.addChild(xmppAxolotlMessage.toElement()); + addJingleChild(security); + } + + public XmppAxolotlMessage getSecurity(final String nameNeedle) { + final Element jingle = findChild("jingle", Namespace.JINGLE); + if (jingle == null) { + return null; + } + for (final Element child : jingle.getChildren()) { + if ("security".equals(child.getName()) + && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) { + final String name = child.getAttribute("name"); + final String type = child.getAttribute("type"); + final String cipher = child.getAttribute("cipher"); + if (nameNeedle.equals(name) + && AxolotlService.PEP_PREFIX.equals(type) + && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) { + final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX); + if (encrypted != null) { + return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid()); + } + } + } + } + return null; + } + public String getSessionId() { return findChild("jingle", Namespace.JINGLE).getAttribute("sid"); } @@ -142,7 +180,7 @@ public class JinglePacket extends IqPacket { TRANSPORT_REPLACE; public static Action of(final String value) { - //TODO handle invalid + // TODO handle invalid return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); } @@ -153,7 +191,6 @@ public class JinglePacket extends IqPacket { } } - public static class ReasonWrapper { public final Reason reason; public final String text; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java index da3a93da3..4b513c7b9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java @@ -18,7 +18,7 @@ public class Propose extends Element { for (final Element child : this.children) { if ("description".equals(child.getName())) { final String namespace = child.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(namespace)) { + if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) { builder.add(FileTransferDescription.upgrade(child)); } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { builder.add(RtpDescription.upgrade(child)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java deleted file mode 100644 index 8f8f13416..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle.stanzas; - -import com.google.common.base.Preconditions; - -import java.util.Collection; -import java.util.List; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.jingle.JingleCandidate; - -public class S5BTransportInfo extends GenericTransportInfo { - - private S5BTransportInfo(final String name, final String xmlns) { - super(name, xmlns); - } - - public String getTransportId() { - return this.getAttribute("sid"); - } - - public S5BTransportInfo(final String transportId, final Collection candidates) { - super("transport", Namespace.JINGLE_TRANSPORTS_S5B); - Preconditions.checkNotNull(transportId,"transport id must not be null"); - for(JingleCandidate candidate : candidates) { - this.addChild(candidate.toElement()); - } - this.setAttribute("sid", transportId); - } - - public S5BTransportInfo(final String transportId, final Element child) { - super("transport", Namespace.JINGLE_TRANSPORTS_S5B); - Preconditions.checkNotNull(transportId,"transport id must not be null"); - this.addChild(child); - this.setAttribute("sid", transportId); - } - - public List getCandidates() { - return JingleCandidate.parse(this.getChildren()); - } - - public static S5BTransportInfo upgrade(final Element element) { - Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); - Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), "Element does not match s5b transport namespace"); - final S5BTransportInfo transportInfo = new S5BTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_S5B); - transportInfo.setAttributes(element.getAttributes()); - transportInfo.setChildren(element.getChildren()); - return transportInfo; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java new file mode 100644 index 000000000..4b1d85847 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import android.util.Log; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport; + +import java.util.Collection; +import java.util.List; + +public class SocksByteStreamsTransportInfo extends GenericTransportInfo { + + private SocksByteStreamsTransportInfo() { + super("transport", Namespace.JINGLE_TRANSPORTS_S5B); + } + + public String getTransportId() { + return this.getAttribute("sid"); + } + + public SocksByteStreamsTransportInfo( + final String transportId, + final Collection candidates) { + super("transport", Namespace.JINGLE_TRANSPORTS_S5B); + Preconditions.checkNotNull(transportId, "transport id must not be null"); + for (SocksByteStreamsTransport.Candidate candidate : candidates) { + this.addChild(candidate.asElement()); + } + this.setAttribute("sid", transportId); + } + + public TransportInfo getTransportInfo() { + if (hasChild("proxy-error")) { + return new ProxyError(); + } else if (hasChild("candidate-error")) { + return new CandidateError(); + } else if (hasChild("candidate-used")) { + final Element candidateUsed = findChild("candidate-used"); + final String cid = candidateUsed == null ? null : candidateUsed.getAttribute("cid"); + if (Strings.isNullOrEmpty(cid)) { + return null; + } else { + return new CandidateUsed(cid); + } + } else if (hasChild("activated")) { + final Element activated = findChild("activated"); + final String cid = activated == null ? null : activated.getAttribute("cid"); + if (Strings.isNullOrEmpty(cid)) { + return null; + } else { + return new Activated(cid); + } + } else { + return null; + } + } + + public List getCandidates() { + final ImmutableList.Builder candidateBuilder = + new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if ("candidate".equals(child.getName()) + && Namespace.JINGLE_TRANSPORTS_S5B.equals(child.getNamespace())) { + try { + candidateBuilder.add(SocksByteStreamsTransport.Candidate.of(child)); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "skip over broken candidate", e); + } + } + } + return candidateBuilder.build(); + } + + public static SocksByteStreamsTransportInfo upgrade(final Element element) { + Preconditions.checkArgument( + "transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument( + Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), + "Element does not match s5b transport namespace"); + final SocksByteStreamsTransportInfo transportInfo = new SocksByteStreamsTransportInfo(); + transportInfo.setAttributes(element.getAttributes()); + transportInfo.setChildren(element.getChildren()); + return transportInfo; + } + + public String getDestinationAddress() { + return this.getAttribute("dstaddr"); + } + + public abstract static class TransportInfo {} + + public static class CandidateUsed extends TransportInfo { + public final String cid; + + public CandidateUsed(String cid) { + this.cid = cid; + } + } + + public static class Activated extends TransportInfo { + public final String cid; + + public Activated(final String cid) { + this.cid = cid; + } + } + + public static class CandidateError extends TransportInfo {} + + public static class ProxyError extends TransportInfo {} +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java new file mode 100644 index 000000000..88c4f9f00 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java @@ -0,0 +1,111 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; + +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; +import eu.siacs.conversations.xmpp.jingle.transports.Transport; + +public class WebRTCDataChannelTransportInfo extends GenericTransportInfo { + + public static final WebRTCDataChannelTransportInfo STUB = new WebRTCDataChannelTransportInfo(); + + public WebRTCDataChannelTransportInfo() { + super("transport", Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL); + } + + public static WebRTCDataChannelTransportInfo upgrade(final Element element) { + Preconditions.checkArgument( + "transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument( + Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(element.getNamespace()), + "Element does not match ice-udp transport namespace"); + final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo(); + transportInfo.setAttributes(element.getAttributes()); + transportInfo.setChildren(element.getChildren()); + return transportInfo; + } + + public IceUdpTransportInfo innerIceUdpTransportInfo() { + final var iceUdpTransportInfo = + this.findChild("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); + if (iceUdpTransportInfo != null) { + return IceUdpTransportInfo.upgrade(iceUdpTransportInfo); + } + return null; + } + + public static Transport.InitialTransportInfo of(final SessionDescription sessionDescription) { + final SessionDescription.Media media = Iterables.getOnlyElement(sessionDescription.media); + final String id = Iterables.getFirst(media.attributes.get("mid"), null); + Preconditions.checkNotNull(id, "media has no mid"); + final String maxMessageSize = + Iterables.getFirst(media.attributes.get("max-message-size"), null); + final Integer maxMessageSizeInt = + maxMessageSize == null ? null : Ints.tryParse(maxMessageSize); + final String sctpPort = Iterables.getFirst(media.attributes.get("sctp-port"), null); + final Integer sctpPortInt = sctpPort == null ? null : Ints.tryParse(sctpPort); + final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo = + new WebRTCDataChannelTransportInfo(); + if (maxMessageSizeInt != null) { + webRTCDataChannelTransportInfo.setAttribute("max-message-size", maxMessageSizeInt); + } + if (sctpPortInt != null) { + webRTCDataChannelTransportInfo.setAttribute("sctp-port", sctpPortInt); + } + webRTCDataChannelTransportInfo.addChild(IceUdpTransportInfo.of(sessionDescription, media)); + + final String groupAttribute = + Iterables.getFirst(sessionDescription.attributes.get("group"), null); + final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute); + return new Transport.InitialTransportInfo(id, webRTCDataChannelTransportInfo, group); + } + + public Integer getSctpPort() { + final var attribute = this.getAttribute("sctp-port"); + if (attribute == null) { + return null; + } + return Ints.tryParse(attribute); + } + + public Integer getMaxMessageSize() { + final var attribute = this.getAttribute("max-message-size"); + if (attribute == null) { + return null; + } + return Ints.tryParse(attribute); + } + + public WebRTCDataChannelTransportInfo cloneWrapper() { + final var iceUdpTransport = this.innerIceUdpTransportInfo(); + final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo(); + transportInfo.setAttributes(new Hashtable<>(getAttributes())); + transportInfo.addChild(iceUdpTransport.cloneWrapper()); + return transportInfo; + } + + public void addCandidate(final IceUdpTransportInfo.Candidate candidate) { + this.innerIceUdpTransportInfo().addChild(candidate); + } + + public List getCandidates() { + final var innerTransportInfo = this.innerIceUdpTransportInfo(); + if (innerTransportInfo == null) { + return Collections.emptyList(); + } + return innerTransportInfo.getCandidates(); + } + + public IceUdpTransportInfo.Credentials getCredentials() { + final var innerTransportInfo = this.innerIceUdpTransportInfo(); + return innerTransportInfo == null ? null : innerTransportInfo.getCredentials(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java new file mode 100644 index 000000000..ce2d4b31f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java @@ -0,0 +1,321 @@ +package eu.siacs.conversations.xmpp.jingle.transports; + +import android.util.Log; + +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; +import com.google.common.io.Closeables; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class InbandBytestreamsTransport implements Transport { + + private static final int DEFAULT_BLOCK_SIZE = 8192; + + private final PipedInputStream pipedInputStream = new PipedInputStream(DEFAULT_BLOCK_SIZE); + private final PipedOutputStream pipedOutputStream = new PipedOutputStream(); + private final CountDownLatch terminationLatch = new CountDownLatch(1); + + private final XmppConnection xmppConnection; + + private final Jid with; + + private final boolean initiator; + + private final String streamId; + + private int blockSize; + private Callback transportCallback; + private final BlockSender blockSender; + + private final Thread blockSenderThread; + + private final AtomicBoolean isReceiving = new AtomicBoolean(false); + + public InbandBytestreamsTransport( + final XmppConnection xmppConnection, final Jid with, final boolean initiator) { + this(xmppConnection, with, initiator, UUID.randomUUID().toString(), DEFAULT_BLOCK_SIZE); + } + + public InbandBytestreamsTransport( + final XmppConnection xmppConnection, + final Jid with, + final boolean initiator, + final String streamId, + final int blockSize) { + this.xmppConnection = xmppConnection; + this.with = with; + this.initiator = initiator; + this.streamId = streamId; + this.blockSize = Math.min(DEFAULT_BLOCK_SIZE, blockSize); + this.blockSender = + new BlockSender(xmppConnection, with, streamId, this.blockSize, pipedInputStream); + this.blockSenderThread = new Thread(blockSender); + } + + public void setTransportCallback(final Callback callback) { + this.transportCallback = callback; + } + + public String getStreamId() { + return this.streamId; + } + + public void connect() { + if (initiator) { + openInBandTransport(); + } + } + + @Override + public CountDownLatch getTerminationLatch() { + return this.terminationLatch; + } + + private void openInBandTransport() { + final var iqPacket = new IqPacket(IqPacket.TYPE.SET); + iqPacket.setTo(with); + final var open = iqPacket.addChild("open", Namespace.IBB); + open.setAttribute("block-size", this.blockSize); + open.setAttribute("sid", this.streamId); + Log.d(Config.LOGTAG, "sending ibb open"); + Log.d(Config.LOGTAG, iqPacket.toString()); + xmppConnection.sendIqPacket(iqPacket, this::receiveResponseToOpen); + } + + private void receiveResponseToOpen(final Account account, final IqPacket response) { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "ibb open was accepted"); + this.transportCallback.onTransportEstablished(); + this.blockSenderThread.start(); + } else { + this.transportCallback.onTransportSetupFailed(); + } + } + + public boolean deliverPacket( + final PacketType packetType, final Jid from, final Element payload) { + if (from == null || !from.equals(with)) { + Log.d( + Config.LOGTAG, + "ibb packet received from wrong address. was " + from + " expected " + with); + return false; + } + return switch (packetType) { + case OPEN -> receiveOpen(); + case DATA -> receiveData(payload.getContent()); + case CLOSE -> receiveClose(); + default -> throw new IllegalArgumentException("Invalid packet type"); + }; + } + + private boolean receiveData(final String encoded) { + final byte[] buffer; + if (Strings.isNullOrEmpty(encoded)) { + buffer = new byte[0]; + } else { + buffer = BaseEncoding.base64().decode(encoded); + } + Log.d(Config.LOGTAG, "ibb received " + buffer.length + " bytes"); + try { + pipedOutputStream.write(buffer); + pipedOutputStream.flush(); + return true; + } catch (final IOException e) { + Log.d(Config.LOGTAG, "unable to receive ibb data", e); + return false; + } + } + + private boolean receiveClose() { + if (this.isReceiving.compareAndSet(true, false)) { + try { + this.pipedOutputStream.close(); + return true; + } catch (final IOException e) { + Log.d(Config.LOGTAG, "could not close pipedOutStream"); + return false; + } + } else { + Log.d(Config.LOGTAG, "received ibb close but was not receiving"); + return false; + } + } + + private boolean receiveOpen() { + Log.d(Config.LOGTAG, "receiveOpen()"); + if (this.isReceiving.get()) { + Log.d(Config.LOGTAG, "ibb received open even though we were already open"); + return false; + } + this.isReceiving.set(true); + transportCallback.onTransportEstablished(); + return true; + } + + public void terminate() { + // TODO send close + Log.d(Config.LOGTAG, "IbbTransport.terminate()"); + this.terminationLatch.countDown(); + this.blockSender.close(); + this.blockSenderThread.interrupt(); + closeQuietly(this.pipedOutputStream); + } + + private static void closeQuietly(final OutputStream outputStream) { + try { + outputStream.close(); + } catch (final IOException ignored) { + + } + } + + @Override + public OutputStream getOutputStream() throws IOException { + final var outputStream = new PipedOutputStream(); + this.pipedInputStream.connect(outputStream); + return outputStream; + } + + @Override + public InputStream getInputStream() throws IOException { + final var inputStream = new PipedInputStream(); + this.pipedOutputStream.connect(inputStream); + return inputStream; + } + + @Override + public ListenableFuture asTransportInfo() { + return Futures.immediateFuture( + new TransportInfo(new IbbTransportInfo(streamId, blockSize), null)); + } + + @Override + public ListenableFuture asInitialTransportInfo() { + return Futures.immediateFuture( + new InitialTransportInfo( + UUID.randomUUID().toString(), + new IbbTransportInfo(streamId, blockSize), + null)); + } + + public void setPeerBlockSize(long peerBlockSize) { + this.blockSize = Math.min(Ints.saturatedCast(peerBlockSize), DEFAULT_BLOCK_SIZE); + if (this.blockSize < DEFAULT_BLOCK_SIZE) { + Log.d(Config.LOGTAG, "peer reconfigured IBB block size to " + this.blockSize); + } + this.blockSender.setBlockSize(this.blockSize); + } + + private static class BlockSender implements Runnable, Closeable { + + private final XmppConnection xmppConnection; + + private final Jid with; + private final String streamId; + + private int blockSize; + private final PipedInputStream inputStream; + private final Semaphore semaphore = new Semaphore(3); + private final AtomicInteger sequencer = new AtomicInteger(); + private final AtomicBoolean isSending = new AtomicBoolean(true); + + private BlockSender( + XmppConnection xmppConnection, + final Jid with, + String streamId, + int blockSize, + PipedInputStream inputStream) { + this.xmppConnection = xmppConnection; + this.with = with; + this.streamId = streamId; + this.blockSize = blockSize; + this.inputStream = inputStream; + } + + @Override + public void run() { + final var buffer = new byte[blockSize]; + try { + while (isSending.get()) { + final int count = this.inputStream.read(buffer); + if (count < 0) { + Log.d(Config.LOGTAG, "block sender reached EOF"); + return; + } + this.semaphore.acquire(); + final var block = new byte[count]; + System.arraycopy(buffer, 0, block, 0, block.length); + sendIbbBlock(sequencer.getAndIncrement(), block); + } + } catch (final InterruptedException | InterruptedIOException e) { + if (isSending.get()) { + Log.w(Config.LOGTAG, "IbbBlockSender got interrupted while sending", e); + } + } catch (final IOException e) { + Log.d(Config.LOGTAG, "block sender terminated", e); + } finally { + Closeables.closeQuietly(inputStream); + } + } + + private void sendIbbBlock(final int sequence, final byte[] block) { + Log.d(Config.LOGTAG, "sending ibb block #" + sequence + " " + block.length + " bytes"); + final var iqPacket = new IqPacket(IqPacket.TYPE.SET); + iqPacket.setTo(with); + final var data = iqPacket.addChild("data", Namespace.IBB); + data.setAttribute("sid", this.streamId); + data.setAttribute("seq", sequence); + data.setContent(BaseEncoding.base64().encode(block)); + this.xmppConnection.sendIqPacket( + iqPacket, + (a, response) -> { + if (response.getType() != IqPacket.TYPE.RESULT) { + Log.d( + Config.LOGTAG, + "received iq error in response to data block #" + sequence); + isSending.set(false); + } + semaphore.release(); + }); + } + + @Override + public void close() { + this.isSending.set(false); + } + + public void setBlockSize(final int blockSize) { + this.blockSize = blockSize; + } + } + + public enum PacketType { + OPEN, + DATA, + CLOSE + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java new file mode 100644 index 000000000..2fe8949a1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -0,0 +1,870 @@ +package eu.siacs.conversations.xmpp.jingle.transports; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; +import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.SocksSocketFactory; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils; +import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SocksByteStreamsTransport implements Transport { + + private final XmppConnection xmppConnection; + + private final AbstractJingleConnection.Id id; + + private final boolean initiator; + private final boolean useTor; + + private final String streamId; + + private ImmutableList theirCandidates; + private final String theirDestination; + private final SettableFuture selectedByThemCandidate = SettableFuture.create(); + private final SettableFuture theirProxyActivation = SettableFuture.create(); + + private final CountDownLatch terminationLatch = new CountDownLatch(1); + + private final ConnectionProvider connectionProvider; + private final ListenableFuture ourProxyConnection; + + private Connection connection; + + private Callback transportCallback; + + public SocksByteStreamsTransport( + final XmppConnection xmppConnection, + final AbstractJingleConnection.Id id, + final boolean initiator, + final boolean useTor, + final String streamId, + final Collection theirCandidates) { + this.xmppConnection = xmppConnection; + this.id = id; + this.initiator = initiator; + this.useTor = useTor; + this.streamId = streamId; + this.theirDestination = + Hashing.sha1() + .hashString( + Joiner.on("") + .join( + Arrays.asList( + streamId, + id.with.toEscapedString(), + id.account.getJid().toEscapedString())), + StandardCharsets.UTF_8) + .toString(); + final var ourDestination = + Hashing.sha1() + .hashString( + Joiner.on("") + .join( + Arrays.asList( + streamId, + id.account.getJid().toEscapedString(), + id.with.toEscapedString())), + StandardCharsets.UTF_8) + .toString(); + + this.connectionProvider = + new ConnectionProvider(id.account.getJid(), ourDestination, useTor); + new Thread(connectionProvider).start(); + this.ourProxyConnection = getOurProxyConnection(ourDestination); + setTheirCandidates(theirCandidates); + } + + public SocksByteStreamsTransport( + final XmppConnection xmppConnection, + final AbstractJingleConnection.Id id, + final boolean initiator, + final boolean useTor) { + this( + xmppConnection, + id, + initiator, + useTor, + UUID.randomUUID().toString(), + Collections.emptyList()); + } + + public void connectTheirCandidates() { + Preconditions.checkState( + this.transportCallback != null, "transport callback needs to be set"); + // TODO this needs to go into a variable so we can cancel it + final var connectionFinder = + new ConnectionFinder(theirCandidates, theirDestination, useTor); + new Thread(connectionFinder).start(); + Futures.addCallback( + connectionFinder.connectionFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final Connection connection) { + final Candidate candidate = connection.candidate; + transportCallback.onCandidateUsed(streamId, candidate); + establishTransport(connection); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + if (throwable instanceof CandidateErrorException) { + transportCallback.onCandidateError(streamId); + } + establishTransport(null); + } + }, + MoreExecutors.directExecutor()); + } + + private void establishTransport(final Connection selectedByUs) { + Futures.addCallback( + selectedByThemCandidate, + new FutureCallback<>() { + @Override + public void onSuccess(Connection result) { + establishTransport(selectedByUs, result); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + establishTransport(selectedByUs, null); + } + }, + MoreExecutors.directExecutor()); + } + + private void establishTransport( + final Connection selectedByUs, final Connection selectedByThem) { + final var selection = selectConnection(selectedByUs, selectedByThem); + if (selection == null) { + transportCallback.onTransportSetupFailed(); + return; + } + if (selection.connection.candidate.type == CandidateType.DIRECT) { + Log.d(Config.LOGTAG, "final selection " + selection.connection.candidate); + this.connection = selection.connection; + this.transportCallback.onTransportEstablished(); + } else { + final ListenableFuture proxyActivation; + if (selection.owner == Owner.THEIRS) { + proxyActivation = this.theirProxyActivation; + } else { + proxyActivation = activateProxy(selection.connection.candidate); + } + Log.d(Config.LOGTAG, "waiting for proxy activation"); + Futures.addCallback( + proxyActivation, + new FutureCallback<>() { + @Override + public void onSuccess(final String cid) { + // TODO compare cid to selection.connection.candidate + connection = selection.connection; + transportCallback.onTransportEstablished(); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "failed to activate proxy"); + } + }, + MoreExecutors.directExecutor()); + } + } + + private ConnectionWithOwner selectConnection( + final Connection selectedByUs, final Connection selectedByThem) { + if (selectedByUs != null && selectedByThem != null) { + if (selectedByUs.candidate.priority == selectedByThem.candidate.priority) { + return initiator + ? new ConnectionWithOwner(selectedByUs, Owner.THEIRS) + : new ConnectionWithOwner(selectedByThem, Owner.OURS); + } else if (selectedByUs.candidate.priority > selectedByThem.candidate.priority) { + return new ConnectionWithOwner(selectedByUs, Owner.THEIRS); + } else { + return new ConnectionWithOwner(selectedByThem, Owner.OURS); + } + } + if (selectedByUs != null) { + return new ConnectionWithOwner(selectedByUs, Owner.THEIRS); + } + if (selectedByThem != null) { + return new ConnectionWithOwner(selectedByThem, Owner.OURS); + } + return null; + } + + private ListenableFuture activateProxy(final Candidate candidate) { + Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate); + final SettableFuture iqFuture = SettableFuture.create(); + final IqPacket proxyActivation = new IqPacket(IqPacket.TYPE.SET); + proxyActivation.setTo(candidate.jid); + final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS); + query.setAttribute("sid", this.streamId); + final Element activate = query.addChild("activate"); + activate.setContent(id.with.toEscapedString()); + xmppConnection.sendIqPacket( + proxyActivation, + (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "our proxy has been activated"); + transportCallback.onProxyActivated(this.streamId, candidate); + iqFuture.set(candidate.cid); + } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + iqFuture.setException(new TimeoutException()); + } else { + Log.d( + Config.LOGTAG, + a.getJid().asBareJid() + + ": failed to activate proxy on " + + candidate.jid); + iqFuture.setException(new IllegalStateException("Proxy activation failed")); + } + }); + return iqFuture; + } + + private ListenableFuture getOurProxyConnection(final String ourDestination) { + final var proxyFuture = getProxyCandidate(); + return Futures.transformAsync( + proxyFuture, + proxy -> { + final var connectionFinder = + new ConnectionFinder(ImmutableList.of(proxy), ourDestination, useTor); + new Thread(connectionFinder).start(); + return Futures.transform( + connectionFinder.connectionFuture, + c -> { + try { + c.socket.setKeepAlive(true); + Log.d( + Config.LOGTAG, + "set keep alive on our own proxy connection"); + } catch (final SocketException e) { + throw new RuntimeException(e); + } + return c; + }, + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture getProxyCandidate() { + if (Config.DISABLE_PROXY_LOOKUP) { + return Futures.immediateFailedFuture( + new IllegalStateException("Proxy look up is disabled")); + } + final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS); + if (streamer == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("No proxy/streamer found")); + } + final IqPacket iqRequest = new IqPacket(IqPacket.TYPE.GET); + iqRequest.setTo(streamer); + iqRequest.query(Namespace.BYTE_STREAMS); + final SettableFuture candidateFuture = SettableFuture.create(); + xmppConnection.sendIqPacket( + iqRequest, + (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element query = response.findChild("query", Namespace.BYTE_STREAMS); + final Element streamHost = + query == null + ? null + : query.findChild("streamhost", Namespace.BYTE_STREAMS); + final String host = + streamHost == null ? null : streamHost.getAttribute("host"); + final Integer port = + Ints.tryParse( + Strings.nullToEmpty( + streamHost == null + ? null + : streamHost.getAttribute("port"))); + if (Strings.isNullOrEmpty(host) || port == null) { + candidateFuture.setException( + new IOException("Proxy response is missing attributes")); + return; + } + candidateFuture.set( + new Candidate( + UUID.randomUUID().toString(), + host, + streamer, + port, + 655360 + (initiator ? 0 : 15), + CandidateType.PROXY)); + + } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + candidateFuture.setException(new TimeoutException()); + } else { + candidateFuture.setException( + new IOException( + "received iq error in response to proxy discovery")); + } + }); + return candidateFuture; + } + + @Override + public OutputStream getOutputStream() throws IOException { + final var connection = this.connection; + if (connection == null) { + throw new IOException("No candidate has been selected yet"); + } + return connection.socket.getOutputStream(); + } + + @Override + public InputStream getInputStream() throws IOException { + final var connection = this.connection; + if (connection == null) { + throw new IOException("No candidate has been selected yet"); + } + return connection.socket.getInputStream(); + } + + @Override + public ListenableFuture asTransportInfo() { + final ListenableFuture> proxyConnections = + getOurProxyConnectionsFuture(); + return Futures.transform( + proxyConnections, + proxies -> { + final var candidateBuilder = new ImmutableList.Builder(); + candidateBuilder.addAll(this.connectionProvider.candidates); + candidateBuilder.addAll(Collections2.transform(proxies, p -> p.candidate)); + final var transportInfo = + new SocksByteStreamsTransportInfo( + this.streamId, candidateBuilder.build()); + return new TransportInfo(transportInfo, null); + }, + MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture asInitialTransportInfo() { + return Futures.transform( + asTransportInfo(), + ti -> + new InitialTransportInfo( + UUID.randomUUID().toString(), ti.transportInfo, ti.group), + MoreExecutors.directExecutor()); + } + + private ListenableFuture> getOurProxyConnectionsFuture() { + return Futures.catching( + Futures.transform( + this.ourProxyConnection, + Collections::singleton, + MoreExecutors.directExecutor()), + Exception.class, + ex -> { + Log.d(Config.LOGTAG, "could not find a proxy of our own", ex); + return Collections.emptyList(); + }, + MoreExecutors.directExecutor()); + } + + private Collection getOurProxyConnections() { + final var future = getOurProxyConnectionsFuture(); + if (future.isDone()) { + try { + return future.get(); + } catch (final Exception e) { + return Collections.emptyList(); + } + } else { + return Collections.emptyList(); + } + } + + @Override + public void terminate() { + Log.d(Config.LOGTAG, "terminating socks transport"); + this.terminationLatch.countDown(); + final var connection = this.connection; + if (connection != null) { + closeSocket(connection.socket); + } + this.connectionProvider.close(); + } + + @Override + public void setTransportCallback(final Callback callback) { + this.transportCallback = callback; + } + + @Override + public void connect() { + this.connectTheirCandidates(); + } + + @Override + public CountDownLatch getTerminationLatch() { + return this.terminationLatch; + } + + public boolean setCandidateUsed(final String cid) { + final var ourProxyConnections = getOurProxyConnections(); + final var proxyConnection = + Iterables.tryFind(ourProxyConnections, c -> c.candidate.cid.equals(cid)); + if (proxyConnection.isPresent()) { + this.selectedByThemCandidate.set(proxyConnection.get()); + return true; + } + + // the peer selected a connection that is not our proxy. so we can close our proxies + closeConnections(ourProxyConnections); + + final var connection = this.connectionProvider.findPeerConnection(cid); + if (connection.isPresent()) { + this.selectedByThemCandidate.set(connection.get()); + return true; + } else { + Log.d(Config.LOGTAG, "none of the connected candidates has cid " + cid); + return false; + } + } + + public void setCandidateError() { + this.selectedByThemCandidate.setException( + new CandidateErrorException("Remote could not connect to any of our candidates")); + } + + public void setProxyActivated(final String cid) { + this.theirProxyActivation.set(cid); + } + + public void setProxyError() { + this.theirProxyActivation.setException( + new IllegalStateException("Remote could not activate their proxy")); + } + + public void setTheirCandidates(Collection candidates) { + this.theirCandidates = + Ordering.from( + (Comparator) + (o1, o2) -> Integer.compare(o2.priority, o1.priority)) + .immutableSortedCopy(candidates); + } + + private static void closeSocket(final Socket socket) { + try { + socket.close(); + } catch (final IOException e) { + Log.w(Config.LOGTAG, "error closing socket", e); + } + } + + private static class ConnectionProvider implements Runnable { + + private final ExecutorService clientConnectionExecutorService = + Executors.newFixedThreadPool(4); + + private final ImmutableList candidates; + + private final int port; + + private final AtomicBoolean acceptingConnections = new AtomicBoolean(true); + + private ServerSocket serverSocket; + + private final String destination; + + private final ArrayList peerConnections = new ArrayList<>(); + + private ConnectionProvider( + final Jid account, final String destination, final boolean useTor) { + final SecureRandom secureRandom = new SecureRandom(); + this.port = secureRandom.nextInt(60_000) + 1024; + this.destination = destination; + final InetAddress[] localAddresses; + if (Config.USE_DIRECT_JINGLE_CANDIDATES && !useTor) { + localAddresses = + DirectConnectionUtils.getLocalAddresses().toArray(new InetAddress[0]); + } else { + localAddresses = new InetAddress[0]; + } + final var candidateBuilder = new ImmutableList.Builder(); + for (int i = 0; i < localAddresses.length; ++i) { + final var inetAddress = localAddresses[i]; + candidateBuilder.add( + new Candidate( + UUID.randomUUID().toString(), + inetAddress.getHostAddress(), + account, + port, + 8257536 + i, + CandidateType.DIRECT)); + } + this.candidates = candidateBuilder.build(); + } + + @Override + public void run() { + if (this.candidates.isEmpty()) { + Log.d(Config.LOGTAG, "no direct candidates. stopping ConnectionProvider"); + return; + } + try (final ServerSocket serverSocket = new ServerSocket(this.port)) { + this.serverSocket = serverSocket; + while (acceptingConnections.get()) { + final Socket clientSocket; + try { + clientSocket = serverSocket.accept(); + } catch (final SocketException ignored) { + Log.d(Config.LOGTAG, "server socket has been closed."); + return; + } + clientConnectionExecutorService.execute( + () -> acceptClientConnection(clientSocket)); + } + } catch (final IOException e) { + Log.d(Config.LOGTAG, "could not create server socket", e); + } + } + + private void acceptClientConnection(final Socket socket) { + final var localAddress = socket.getLocalAddress(); + final var hostAddress = localAddress == null ? null : localAddress.getHostAddress(); + final var candidate = + Iterables.tryFind(this.candidates, c -> c.host.equals(hostAddress)); + if (candidate.isPresent()) { + acceptingConnections(socket, candidate.get()); + + } else { + closeSocket(socket); + Log.d(Config.LOGTAG, "no local candidate found for connection on " + hostAddress); + } + } + + private void acceptingConnections(final Socket socket, final Candidate candidate) { + final var remoteAddress = socket.getRemoteSocketAddress(); + Log.d( + Config.LOGTAG, + "accepted client connection from " + remoteAddress + " to " + candidate); + try { + socket.setSoTimeout(3000); + final byte[] authBegin = new byte[2]; + final InputStream inputStream = socket.getInputStream(); + final OutputStream outputStream = socket.getOutputStream(); + ByteStreams.readFully(inputStream, authBegin); + if (authBegin[0] != 0x5) { + socket.close(); + } + final short methodCount = authBegin[1]; + final byte[] methods = new byte[methodCount]; + ByteStreams.readFully(inputStream, methods); + if (SocksSocketFactory.contains((byte) 0x00, methods)) { + outputStream.write(new byte[] {0x05, 0x00}); + } else { + outputStream.write(new byte[] {0x05, (byte) 0xff}); + } + final byte[] connectCommand = new byte[4]; + ByteStreams.readFully(inputStream, connectCommand); + if (connectCommand[0] == 0x05 + && connectCommand[1] == 0x01 + && connectCommand[3] == 0x03) { + int destinationCount = inputStream.read(); + final byte[] destination = new byte[destinationCount]; + ByteStreams.readFully(inputStream, destination); + final byte[] port = new byte[2]; + ByteStreams.readFully(inputStream, port); + final String receivedDestination = new String(destination); + final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); + final byte[] responseHeader; + final boolean success; + if (receivedDestination.equals(this.destination)) { + responseHeader = new byte[] {0x05, 0x00, 0x00, 0x03}; + synchronized (this.peerConnections) { + peerConnections.add(new Connection(candidate, socket)); + } + success = true; + } else { + Log.d( + Config.LOGTAG, + "destination mismatch. received " + + receivedDestination + + " (expected " + + this.destination + + ")"); + responseHeader = new byte[] {0x05, 0x04, 0x00, 0x03}; + success = false; + } + response.put(responseHeader); + response.put((byte) destination.length); + response.put(destination); + response.put(port); + outputStream.write(response.array()); + outputStream.flush(); + if (success) { + Log.d( + Config.LOGTAG, + remoteAddress + " successfully connected to " + candidate); + } else { + closeSocket(socket); + } + } + } catch (final IOException e) { + Log.d(Config.LOGTAG, "failed to accept client connection to " + candidate, e); + closeSocket(socket); + } + } + + private static void closeServerSocket(@Nullable final ServerSocket serverSocket) { + if (serverSocket == null) { + return; + } + try { + serverSocket.close(); + } catch (final IOException ignored) { + + } + } + + public Optional findPeerConnection(String cid) { + synchronized (this.peerConnections) { + return Iterables.tryFind( + this.peerConnections, connection -> connection.candidate.cid.equals(cid)); + } + } + + public void close() { + this.acceptingConnections.set(false); // we have probably done this earlier already + closeServerSocket(this.serverSocket); + synchronized (this.peerConnections) { + closeConnections(this.peerConnections); + this.peerConnections.clear(); + } + } + } + + private static void closeConnections(final Iterable connections) { + for (final var connection : connections) { + closeSocket(connection.socket); + } + } + + private static class ConnectionFinder implements Runnable { + + private final SettableFuture connectionFuture = SettableFuture.create(); + + private final ImmutableList candidates; + private final String destination; + private final boolean useTor; + + private ConnectionFinder( + final ImmutableList candidates, + final String destination, + final boolean useTor) { + this.candidates = candidates; + this.destination = destination; + this.useTor = useTor; + } + + @Override + public void run() { + for (final Candidate candidate : this.candidates) { + // TODO we can check if there is already something in `selectedByThemCandidate` with + // a higher priority and abort + try { + connectionFuture.set(connect(candidate)); + Log.d(Config.LOGTAG, "connected to " + candidate); + return; + } catch (final IOException e) { + Log.d(Config.LOGTAG, "could not connect to candidate " + candidate); + } + } + connectionFuture.setException( + new CandidateErrorException( + String.format( + Locale.US, + "Gave up after %d candidates", + this.candidates.size()))); + } + + private Connection connect(final Candidate candidate) throws IOException { + final var timeout = 3000; + final Socket socket; + if (useTor) { + Log.d(Config.LOGTAG, "using Tor to connect to candidate " + candidate.host); + socket = SocksSocketFactory.createSocketOverTor(candidate.host, candidate.port); + } else { + socket = new Socket(); + final SocketAddress address = new InetSocketAddress(candidate.host, candidate.port); + socket.connect(address, timeout); + } + socket.setSoTimeout(timeout); + SocksSocketFactory.createSocksConnection(socket, destination, 0); + socket.setSoTimeout(0); + return new Connection(candidate, socket); + } + } + + public static class CandidateErrorException extends IllegalStateException { + private CandidateErrorException(final String message) { + super(message); + } + } + + private enum Owner { + THEIRS, + OURS + } + + public static class ConnectionWithOwner { + public final Connection connection; + public final Owner owner; + + public ConnectionWithOwner(Connection connection, Owner owner) { + this.connection = connection; + this.owner = owner; + } + } + + public static class Connection { + + public final Candidate candidate; + public final Socket socket; + + public Connection(Candidate candidate, Socket socket) { + this.candidate = candidate; + this.socket = socket; + } + } + + public static class Candidate implements Transport.Candidate { + public final String cid; + public final String host; + public final Jid jid; + public final int port; + public final int priority; + public final CandidateType type; + + public Candidate( + final String cid, + final String host, + final Jid jid, + int port, + int priority, + final CandidateType type) { + this.cid = cid; + this.host = host; + this.jid = jid; + this.port = port; + this.priority = priority; + this.type = type; + } + + public static Candidate of(final Element element) { + Preconditions.checkArgument( + "candidate".equals(element.getName()), + "trying to construct candidate from non candidate element"); + Preconditions.checkArgument( + Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), + "candidate element is in correct namespace"); + final String cid = element.getAttribute("cid"); + final String host = element.getAttribute("host"); + final String jid = element.getAttribute("jid"); + final String port = element.getAttribute("port"); + final String priority = element.getAttribute("priority"); + final String type = element.getAttribute("type"); + if (Strings.isNullOrEmpty(cid) + || Strings.isNullOrEmpty(host) + || Strings.isNullOrEmpty(jid) + || Strings.isNullOrEmpty(port) + || Strings.isNullOrEmpty(priority) + || Strings.isNullOrEmpty(type)) { + throw new IllegalArgumentException("Candidate is missing non optional attribute"); + } + return new Candidate( + cid, + host, + Jid.ofEscaped(jid), + Integer.parseInt(port), + Integer.parseInt(priority), + CandidateType.valueOf(type.toUpperCase(Locale.ROOT))); + } + + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("cid", cid) + .add("host", host) + .add("jid", jid) + .add("port", port) + .add("priority", priority) + .add("type", type) + .toString(); + } + + public Element asElement() { + final var element = new Element("candidate", Namespace.JINGLE_TRANSPORTS_S5B); + element.setAttribute("cid", this.cid); + element.setAttribute("host", this.host); + element.setAttribute("jid", this.jid); + element.setAttribute("port", this.port); + element.setAttribute("priority", this.priority); + element.setAttribute("type", this.type.toString().toLowerCase(Locale.ROOT)); + return element; + } + } + + public enum CandidateType { + DIRECT, + PROXY + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/Transport.java new file mode 100644 index 000000000..ce99ac8cc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/Transport.java @@ -0,0 +1,80 @@ +package eu.siacs.conversations.xmpp.jingle.transports; + +import com.google.common.util.concurrent.ListenableFuture; + +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; + +public interface Transport { + + OutputStream getOutputStream() throws IOException; + + InputStream getInputStream() throws IOException; + + ListenableFuture asTransportInfo(); + + ListenableFuture asInitialTransportInfo(); + + default void readyToSentAdditionalCandidates() {} + + void terminate(); + + void setTransportCallback(final Callback callback); + + void connect(); + + CountDownLatch getTerminationLatch(); + + interface Callback { + void onTransportEstablished(); + + void onTransportSetupFailed(); + + void onAdditionalCandidate(final String contentName, final Candidate candidate); + + void onCandidateUsed(String streamId, SocksByteStreamsTransport.Candidate candidate); + + void onCandidateError(String streamId); + + void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate); + } + + enum Direction { + SEND, + RECEIVE, + SEND_RECEIVE + } + + class InitialTransportInfo extends TransportInfo { + public final String contentName; + + public InitialTransportInfo( + String contentName, GenericTransportInfo transportInfo, Group group) { + super(transportInfo, group); + this.contentName = contentName; + } + } + + class TransportInfo { + + public final GenericTransportInfo transportInfo; + public final Group group; + + public TransportInfo(final GenericTransportInfo transportInfo, final Group group) { + this.transportInfo = transportInfo; + this.group = group; + } + + public TransportInfo(final GenericTransportInfo transportInfo) { + this.transportInfo = transportInfo; + this.group = null; + } + } + + interface Candidate {} +} 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 new file mode 100644 index 000000000..0773610fb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -0,0 +1,617 @@ +package eu.siacs.conversations.xmpp.jingle.transports; + +import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.buildConfiguration; +import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription; + +import android.content.Context; +import android.util.Log; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Closeables; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.IceServers; +import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import org.webrtc.CandidatePairChangeEvent; +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SessionDescription; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +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; + +public class WebRTCDataChannelTransport implements Transport { + + private static final int BUFFER_SIZE = 16_384; + private static final int MAX_SENT_BUFFER = 256 * 1024; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final ExecutorService localDescriptionExecutorService = + Executors.newSingleThreadExecutor(); + + private final AtomicBoolean readyToSentIceCandidates = new AtomicBoolean(false); + private final Queue pendingOutgoingIceCandidates = new LinkedList<>(); + + private final PipedOutputStream pipedOutputStream = new PipedOutputStream(); + private final WritableByteChannel writableByteChannel = Channels.newChannel(pipedOutputStream); + private final PipedInputStream pipedInputStream = new PipedInputStream(BUFFER_SIZE); + + private final AtomicBoolean connected = new AtomicBoolean(false); + + private final CountDownLatch terminationLatch = new CountDownLatch(1); + + private final Queue stateHistory = new LinkedList<>(); + + private final XmppConnection xmppConnection; + private final Account account; + private PeerConnectionFactory peerConnectionFactory; + private ListenableFuture peerConnectionFuture; + + private ListenableFuture localDescriptionFuture; + + private DataChannel dataChannel; + + private Callback transportCallback; + + private final PeerConnection.Observer peerConnectionObserver = + new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(Config.LOGTAG, "onSignalChange(" + signalingState + ")"); + } + + @Override + public void onConnectionChange(final PeerConnection.PeerConnectionState state) { + stateHistory.add(state); + Log.d(Config.LOGTAG, "onConnectionChange(" + state + ")"); + if (state == PeerConnection.PeerConnectionState.CONNECTED) { + if (connected.compareAndSet(false, true)) { + executorService.execute(() -> onIceConnectionConnected()); + } + } + if (state == PeerConnection.PeerConnectionState.FAILED) { + final boolean neverConnected = + !stateHistory.contains( + PeerConnection.PeerConnectionState.CONNECTED); + // we want to terminate the connection a) to properly fail if a connection + // drops during a transfer and b) to avoid race conditions if we find a + // connection after failure while waiting for the initiator to replace + // transport + executorService.execute(() -> terminate()); + if (neverConnected) { + executorService.execute(() -> onIceConnectionFailed()); + } + } + } + + @Override + public void onIceConnectionChange( + final PeerConnection.IceConnectionState newState) {} + + @Override + public void onIceConnectionReceivingChange(boolean b) {} + + @Override + public void onIceGatheringChange( + final PeerConnection.IceGatheringState iceGatheringState) { + Log.d(Config.LOGTAG, "onIceGatheringChange(" + iceGatheringState + ")"); + } + + @Override + public void onIceCandidate(final IceCandidate iceCandidate) { + if (readyToSentIceCandidates.get()) { + WebRTCDataChannelTransport.this.onIceCandidate( + iceCandidate.sdpMid, iceCandidate.sdp); + } else { + pendingOutgoingIceCandidates.add(iceCandidate); + } + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {} + + @Override + public void onAddStream(MediaStream mediaStream) {} + + @Override + public void onRemoveStream(MediaStream mediaStream) {} + + @Override + public void onDataChannel(final DataChannel dataChannel) { + Log.d(Config.LOGTAG, "onDataChannel()"); + WebRTCDataChannelTransport.this.setDataChannel(dataChannel); + } + + @Override + public void onRenegotiationNeeded() { + Log.d(Config.LOGTAG, "onRenegotiationNeeded"); + } + + @Override + public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { + Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); + Log.d(Config.LOGTAG, "local candidate selected: " + event.local); + } + }; + + private DataChannelWriter dataChannelWriter; + + private void onIceConnectionConnected() { + this.transportCallback.onTransportEstablished(); + } + + private void onIceConnectionFailed() { + this.transportCallback.onTransportSetupFailed(); + } + + private void setDataChannel(final DataChannel dataChannel) { + Log.d(Config.LOGTAG, "the 'receiving' data channel has id " + dataChannel.id()); + this.dataChannel = dataChannel; + this.dataChannel.registerObserver( + new OnMessageObserver() { + @Override + public void onMessage(final DataChannel.Buffer buffer) { + Log.d(Config.LOGTAG, "onMessage() (the other one)"); + try { + WebRTCDataChannelTransport.this.writableByteChannel.write(buffer.data); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "error writing to output stream"); + } + } + }); + } + + protected void onIceCandidate(final String mid, final String sdp) { + final var candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(sdp, null); + this.transportCallback.onAdditionalCandidate(mid, candidate); + } + + public WebRTCDataChannelTransport( + final Context context, + final XmppConnection xmppConnection, + final Account account, + final boolean initiator) { + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(context) + .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/") + .createInitializationOptions()); + this.peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); + this.xmppConnection = xmppConnection; + this.account = account; + this.peerConnectionFuture = + Futures.transform( + getIceServers(), + iceServers -> createPeerConnection(iceServers, true), + MoreExecutors.directExecutor()); + if (initiator) { + this.localDescriptionFuture = setLocalDescription(); + } + } + + private ListenableFuture> getIceServers() { + if (Config.DISABLE_PROXY_LOOKUP) { + return Futures.immediateFuture(Collections.emptyList()); + } + if (xmppConnection.getFeatures().externalServiceDiscovery()) { + final SettableFuture> iceServerFuture = + SettableFuture.create(); + final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(this.account.getDomain()); + request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + xmppConnection.sendIqPacket( + request, + (account, response) -> { + final var iceServers = IceServers.parse(response); + if (iceServers.size() == 0) { + Log.w( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no ICE server found " + + response); + } + iceServerFuture.set(iceServers); + }); + return iceServerFuture; + } else { + return Futures.immediateFuture(Collections.emptyList()); + } + } + + private PeerConnection createPeerConnection( + final List iceServers, final boolean trickle) { + final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle); + final PeerConnection peerConnection = + requirePeerConnectionFactory() + .createPeerConnection(rtcConfig, peerConnectionObserver); + if (peerConnection == null) { + throw new IllegalStateException("Unable to create PeerConnection"); + } + final var dataChannelInit = new DataChannel.Init(); + dataChannelInit.protocol = "xmpp-jingle"; + final var dataChannel = peerConnection.createDataChannel("test", dataChannelInit); + this.dataChannelWriter = new DataChannelWriter(this.pipedInputStream, dataChannel); + Log.d(Config.LOGTAG, "the 'sending' data channel has id " + dataChannel.id()); + new Thread(this.dataChannelWriter).start(); + return peerConnection; + } + + @Override + public OutputStream getOutputStream() throws IOException { + final var outputStream = new PipedOutputStream(); + this.pipedInputStream.connect(outputStream); + this.dataChannelWriter.pipedInputStreamLatch.countDown(); + return outputStream; + } + + @Override + public InputStream getInputStream() throws IOException { + final var inputStream = new PipedInputStream(BUFFER_SIZE); + this.pipedOutputStream.connect(inputStream); + return inputStream; + } + + @Override + public ListenableFuture asTransportInfo() { + Preconditions.checkState( + this.localDescriptionFuture != null, + "Make sure you are setting initiator description first"); + return Futures.transform( + asInitialTransportInfo(), info -> info, MoreExecutors.directExecutor()); + } + + @Override + public ListenableFuture asInitialTransportInfo() { + return Futures.transform( + localDescriptionFuture, + sdp -> + WebRTCDataChannelTransportInfo.of( + eu.siacs.conversations.xmpp.jingle.SessionDescription.parse( + sdp.description)), + MoreExecutors.directExecutor()); + } + + @Override + public void readyToSentAdditionalCandidates() { + readyToSentIceCandidates.set(true); + while (this.pendingOutgoingIceCandidates.peek() != null) { + final var candidate = pendingOutgoingIceCandidates.poll(); + if (candidate == null) { + continue; + } + onIceCandidate(candidate.sdpMid, candidate.sdp); + } + } + + @Override + public void terminate() { + terminate(this.dataChannel); + this.dataChannel = null; + final var dataChannelWriter = this.dataChannelWriter; + if (dataChannelWriter != null) { + dataChannelWriter.close(); + } + this.dataChannelWriter = null; + final var future = this.peerConnectionFuture; + if (future != null) { + future.cancel(true); + } + try { + final PeerConnection peerConnection = requirePeerConnection(); + terminate(peerConnection); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + Log.d(Config.LOGTAG, "peer connection was not initialized during termination"); + } + this.peerConnectionFuture = null; + final var peerConnectionFactory = this.peerConnectionFactory; + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + } + this.peerConnectionFactory = null; + closeQuietly(this.pipedOutputStream); + this.terminationLatch.countDown(); + Log.d(Config.LOGTAG, WebRTCDataChannelTransport.class.getSimpleName() + " terminated"); + } + + private static void closeQuietly(final OutputStream outputStream) { + try { + outputStream.close(); + } catch (final IOException ignored) { + + } + } + + private static void terminate(final DataChannel dataChannel) { + if (dataChannel == null) { + Log.d(Config.LOGTAG, "nothing to terminate. data channel is already null"); + return; + } + try { + dataChannel.close(); + } catch (final IllegalStateException e) { + Log.w(Config.LOGTAG, "could not close data channel"); + } + try { + dataChannel.dispose(); + } catch (final IllegalStateException e) { + Log.w(Config.LOGTAG, "could not dispose data channel"); + } + } + + private static void terminate(final PeerConnection peerConnection) { + if (peerConnection == null) { + return; + } + try { + peerConnection.dispose(); + Log.d(Config.LOGTAG, "terminated peer connection!"); + } catch (final IllegalStateException e) { + Log.w(Config.LOGTAG, "could not dispose of peer connection"); + } + } + + @Override + public void setTransportCallback(final Callback callback) { + this.transportCallback = callback; + } + + @Override + public void connect() {} + + @Override + public CountDownLatch getTerminationLatch() { + return this.terminationLatch; + } + + synchronized ListenableFuture setLocalDescription() { + return Futures.transformAsync( + peerConnectionFuture, + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setLocalDescription( + new WebRTCWrapper.SetSdpObserver() { + @Override + public void onSetSuccess() { + future.setFuture(getLocalDescriptionFuture(peerConnection)); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new WebRTCWrapper.FailureToSetDescriptionException( + message)); + } + }); + return future; + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture getLocalDescriptionFuture( + final PeerConnection peerConnection) { + return Futures.submit( + () -> { + final SessionDescription description = peerConnection.getLocalDescription(); + WebRTCWrapper.logDescription(description); + return description; + }, + localDescriptionExecutorService); + } + + @Nonnull + private PeerConnectionFactory requirePeerConnectionFactory() { + final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; + if (peerConnectionFactory == null) { + throw new IllegalStateException("Make sure PeerConnectionFactory is initialized"); + } + return peerConnectionFactory; + } + + @Nonnull + private PeerConnection requirePeerConnection() { + final var future = this.peerConnectionFuture; + if (future != null && future.isDone()) { + try { + return future.get(); + } catch (final InterruptedException | ExecutionException e) { + throw new WebRTCWrapper.PeerConnectionNotInitialized(); + } + } else { + throw new WebRTCWrapper.PeerConnectionNotInitialized(); + } + } + + public static List iceCandidatesOf( + final String contentName, + final IceUdpTransportInfo.Credentials credentials, + final List candidates) { + final ImmutableList.Builder iceCandidateBuilder = + new ImmutableList.Builder<>(); + for (final IceUdpTransportInfo.Candidate candidate : candidates) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(credentials.ufrag); + } catch (final IllegalArgumentException e) { + continue; + } + // TODO mLneIndex should probably not be hard coded + iceCandidateBuilder.add(new IceCandidate(contentName, 0, sdp)); + } + return iceCandidateBuilder.build(); + } + + public void addIceCandidates(final List iceCandidates) { + try { + for (final var candidate : iceCandidates) { + requirePeerConnection().addIceCandidate(candidate); + } + } catch (WebRTCWrapper.PeerConnectionNotInitialized e) { + Log.w(Config.LOGTAG, "could not add ice candidate. peer connection is not initialized"); + } + } + + public void setInitiatorDescription( + final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) { + final var sdp = + new SessionDescription( + SessionDescription.Type.OFFER, sessionDescription.toString()); + final var setFuture = setRemoteDescriptionFuture(sdp); + this.localDescriptionFuture = + Futures.transformAsync( + setFuture, v -> setLocalDescription(), MoreExecutors.directExecutor()); + } + + public void setResponderDescription( + final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) { + Log.d(Config.LOGTAG, "setResponder description"); + final var sdp = + new SessionDescription( + SessionDescription.Type.ANSWER, sessionDescription.toString()); + logDescription(sdp); + setRemoteDescriptionFuture(sdp); + } + + synchronized ListenableFuture setRemoteDescriptionFuture( + final SessionDescription sessionDescription) { + return Futures.transformAsync( + this.peerConnectionFuture, + peerConnection -> { + if (peerConnection == null) { + return Futures.immediateFailedFuture( + new IllegalStateException("PeerConnection was null")); + } + final SettableFuture future = SettableFuture.create(); + peerConnection.setRemoteDescription( + new WebRTCWrapper.SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + } + + @Override + public void onSetFailure(final String message) { + future.setException( + new WebRTCWrapper.FailureToSetDescriptionException( + message)); + } + }, + sessionDescription); + return future; + }, + MoreExecutors.directExecutor()); + } + + private static class DataChannelWriter implements Runnable { + + private final CountDownLatch pipedInputStreamLatch = new CountDownLatch(1); + private final CountDownLatch dataChannelLatch = new CountDownLatch(1); + private final AtomicBoolean isSending = new AtomicBoolean(true); + private final InputStream inputStream; + private final DataChannel dataChannel; + + private DataChannelWriter(InputStream inputStream, DataChannel dataChannel) { + this.inputStream = inputStream; + this.dataChannel = dataChannel; + final StateChangeObserver stateChangeObserver = + new StateChangeObserver() { + + @Override + public void onStateChange() { + if (dataChannel.state() == DataChannel.State.OPEN) { + dataChannelLatch.countDown(); + } + } + }; + this.dataChannel.registerObserver(stateChangeObserver); + } + + public void run() { + try { + this.pipedInputStreamLatch.await(); + this.dataChannelLatch.await(); + final var buffer = new byte[4096]; + while (isSending.get()) { + final long bufferedAmount = dataChannel.bufferedAmount(); + if (bufferedAmount > MAX_SENT_BUFFER) { + Thread.sleep(50); + continue; + } + final int count = this.inputStream.read(buffer); + if (count < 0) { + Log.d(Config.LOGTAG, "DataChannelWriter reached EOF"); + return; + } + dataChannel.send( + new DataChannel.Buffer(ByteBuffer.wrap(buffer, 0, count), true)); + } + } catch (final InterruptedException | InterruptedIOException e) { + if (isSending.get()) { + Log.w(Config.LOGTAG, "DataChannelWriter got interrupted while sending", e); + } + } catch (final IOException e) { + Log.d(Config.LOGTAG, "DataChannelWriter terminated", e); + } finally { + Closeables.closeQuietly(inputStream); + } + } + + public void close() { + this.isSending.set(false); + terminate(this.dataChannel); + } + } + + private abstract static class StateChangeObserver implements DataChannel.Observer { + + @Override + public void onBufferedAmountChange(final long change) {} + + @Override + public void onMessage(final DataChannel.Buffer buffer) {} + } + + private abstract static class OnMessageObserver implements DataChannel.Observer { + + @Override + public void onBufferedAmountChange(long l) {} + + @Override + public void onStateChange() {} + } +} From eec01c9e7bea9f8576f3e518a6c68a56fa411e37 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 20 Dec 2023 09:23:09 +0100 Subject: [PATCH 002/556] disable quick log --- src/main/java/eu/siacs/conversations/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 343f3376a..982bd4781 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -41,7 +41,7 @@ public final class Config { public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US); - public static final boolean QUICK_LOG = true; + public static final boolean QUICK_LOG = false; public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im"); public static final Uri HELP = Uri.parse("https://help.conversations.im"); From aeb805a3ca9672b5c090aff66ac78073f11f5e4f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 20 Dec 2023 11:23:04 +0100 Subject: [PATCH 003/556] abort socks candidate search if peer selected something with higher priority --- .../transports/SocksByteStreamsTransport.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java index 2fe8949a1..bbda1c622 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -54,6 +54,7 @@ import java.util.Comparator; import java.util.Locale; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; @@ -145,7 +146,8 @@ public class SocksByteStreamsTransport implements Transport { this.transportCallback != null, "transport callback needs to be set"); // TODO this needs to go into a variable so we can cancel it final var connectionFinder = - new ConnectionFinder(theirCandidates, theirDestination, useTor); + new ConnectionFinder( + theirCandidates, theirDestination, selectedByThemCandidate, useTor); new Thread(connectionFinder).start(); Futures.addCallback( connectionFinder.connectionFuture, @@ -281,7 +283,8 @@ public class SocksByteStreamsTransport implements Transport { proxyFuture, proxy -> { final var connectionFinder = - new ConnectionFinder(ImmutableList.of(proxy), ourDestination, useTor); + new ConnectionFinder( + ImmutableList.of(proxy), ourDestination, null, useTor); new Thread(connectionFinder).start(); return Futures.transform( connectionFinder.connectionFuture, @@ -703,22 +706,36 @@ public class SocksByteStreamsTransport implements Transport { private final ImmutableList candidates; private final String destination; + + private final ListenableFuture selectedByThemCandidate; private final boolean useTor; private ConnectionFinder( final ImmutableList candidates, final String destination, + final ListenableFuture selectedByThemCandidate, final boolean useTor) { this.candidates = candidates; this.destination = destination; + this.selectedByThemCandidate = selectedByThemCandidate; this.useTor = useTor; } @Override public void run() { for (final Candidate candidate : this.candidates) { - // TODO we can check if there is already something in `selectedByThemCandidate` with - // a higher priority and abort + final Integer selectedByThemCandidatePriority = + getSelectedByThemCandidatePriority(); + if (selectedByThemCandidatePriority != null + && selectedByThemCandidatePriority > candidate.priority) { + Log.d( + Config.LOGTAG, + "The candidate selected by peer had a higher priority then anything we could try"); + connectionFuture.setException( + new CandidateErrorException( + "The candidate selected by peer had a higher priority then anything we could try")); + return; + } try { connectionFuture.set(connect(candidate)); Log.d(Config.LOGTAG, "connected to " + candidate); @@ -751,6 +768,20 @@ public class SocksByteStreamsTransport implements Transport { socket.setSoTimeout(0); return new Connection(candidate, socket); } + + private Integer getSelectedByThemCandidatePriority() { + final var future = this.selectedByThemCandidate; + if (future != null && future.isDone()) { + try { + final var connection = future.get(); + return connection.candidate.priority; + } catch (ExecutionException | InterruptedException e) { + return null; + } + } else { + return null; + } + } } public static class CandidateErrorException extends IllegalStateException { From 3cf59b41817f869396717ef148811f5dfe56762e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Jan 2024 16:51:46 +0100 Subject: [PATCH 004/556] bump various dependencies --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 45443fe33..b4281a6b9 100644 --- a/build.gradle +++ b/build.gradle @@ -31,11 +31,11 @@ configurations { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.3.1') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.4.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -45,10 +45,10 @@ dependencies { implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.exifinterface:exifinterface:1.3.6' + implementation 'androidx.exifinterface:exifinterface:1.3.7' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.10.0' + implementation 'com.google.android.material:material:1.11.0' implementation "androidx.emoji2:emoji2:1.4.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0" @@ -78,7 +78,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.11.0" + implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation 'com.google.guava:guava:32.1.3-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.17' From 88851ea12a9ea9d546a04321742a9656733291c3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 10:53:33 +0100 Subject: [PATCH 005/556] bundle letsencrypt root certificates as per https://letsencrypt.org/2023/07/10/cross-sign-expiration.html Letsencrypt is dropping support for Android <= 7 by removing cross signing. to keep supporting older Android versions we need to bundle the root cert ourselves. (Firefox for example does this too) The KeyStore file is in BKS-V1 format. A good tools to edit the file is https://keystore-explorer.org/ To keep the attack and bug surface as low as possible we only run the check against the bundled keystore on Android <= 7 --- .../crypto/BundledTrustManager.java | 65 ++++ .../crypto/CombiningTrustManager.java | 96 ++++++ .../conversations/crypto/TrustManagers.java | 38 +++ .../services/MemorizingTrustManager.java | 323 +++++++++++------- src/main/res/raw/letsencrypt.bks | Bin 0 -> 2043 bytes 5 files changed, 392 insertions(+), 130 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/TrustManagers.java create mode 100644 src/main/res/raw/letsencrypt.bks diff --git a/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java b/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java new file mode 100644 index 000000000..9eb20cc30 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java @@ -0,0 +1,65 @@ +package eu.siacs.conversations.crypto; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +public class BundledTrustManager implements X509TrustManager { + + private final X509TrustManager delegate; + + private BundledTrustManager(final KeyStore keyStore) + throws NoSuchAlgorithmException, KeyStoreException { + this.delegate = TrustManagers.createTrustManager(keyStore); + } + + public static Builder builder() throws KeyStoreException { + return new Builder(); + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + this.delegate.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + this.delegate.checkServerTrusted(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return this.delegate.getAcceptedIssuers(); + } + + public static class Builder { + + private KeyStore keyStore; + + private Builder() {} + + public Builder loadKeyStore(final InputStream inputStream, final String password) + throws CertificateException, IOException, NoSuchAlgorithmException, + KeyStoreException { + if (this.keyStore != null) { + throw new IllegalStateException("KeyStore has already been loaded"); + } + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(inputStream, password.toCharArray()); + this.keyStore = keyStore; + return this; + } + + public BundledTrustManager build() throws NoSuchAlgorithmException, KeyStoreException { + return new BundledTrustManager(keyStore); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java b/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java new file mode 100644 index 000000000..0f3c0e044 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java @@ -0,0 +1,96 @@ +package eu.siacs.conversations.crypto; + +import android.util.Log; + +import com.google.common.collect.ImmutableList; + +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.net.ssl.X509TrustManager; + +import eu.siacs.conversations.Config; + +public class CombiningTrustManager implements X509TrustManager { + + private final List trustManagers; + + private CombiningTrustManager(final List trustManagers) { + this.trustManagers = trustManagers; + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + for (final Iterator iterator = this.trustManagers.iterator(); + iterator.hasNext(); ) { + final X509TrustManager trustManager = iterator.next(); + try { + trustManager.checkClientTrusted(chain, authType); + } catch (final CertificateException certificateException) { + if (iterator.hasNext()) { + continue; + } + throw certificateException; + } + } + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) + throws CertificateException { + Log.d( + Config.LOGTAG, + CombiningTrustManager.class.getSimpleName() + + " is configured with " + + this.trustManagers.size() + + " TrustManagers"); + int i = 0; + for (final Iterator iterator = this.trustManagers.iterator(); + iterator.hasNext(); ) { + final X509TrustManager trustManager = iterator.next(); + try { + trustManager.checkServerTrusted(chain, authType); + Log.d( + Config.LOGTAG, + "certificate check passed on " + trustManager.getClass().getName()+". chain length was "+chain.length); + return; + } catch (final CertificateException certificateException) { + Log.d( + Config.LOGTAG, + "failed to verify in [" + i + "]/" + trustManager.getClass().getName(), + certificateException); + if (iterator.hasNext()) { + continue; + } + throw certificateException; + } finally { + ++i; + } + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + final ImmutableList.Builder certificates = ImmutableList.builder(); + for (final X509TrustManager trustManager : this.trustManagers) { + for (final X509Certificate certificate : trustManager.getAcceptedIssuers()) { + certificates.add(certificate); + } + } + return certificates.build().toArray(new X509Certificate[0]); + } + + public static X509TrustManager combineWithDefault(final X509TrustManager... trustManagers) + throws NoSuchAlgorithmException, KeyStoreException { + final ImmutableList.Builder builder = ImmutableList.builder(); + builder.addAll(Arrays.asList(trustManagers)); + builder.add(TrustManagers.createDefaultTrustManager()); + return new CombiningTrustManager(builder.build()); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java new file mode 100644 index 000000000..11fe182dd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java @@ -0,0 +1,38 @@ +package eu.siacs.conversations.crypto; + +import androidx.annotation.Nullable; + +import com.google.common.collect.Iterables; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public final class TrustManagers { + + private TrustManagers() { + throw new IllegalStateException("Do not instantiate me"); + } + + public static X509TrustManager createTrustManager(@Nullable final KeyStore keyStore) + throws NoSuchAlgorithmException, KeyStoreException { + final TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return Iterables.getOnlyElement( + Iterables.filter( + Arrays.asList(trustManagerFactory.getTrustManagers()), + X509TrustManager.class)); + } + + public static X509TrustManager createDefaultTrustManager() + throws NoSuchAlgorithmException, KeyStoreException { + return createTrustManager(null); + } + + +} diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 520348943..81cfb951f 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Base64; @@ -43,9 +44,21 @@ import androidx.appcompat.app.AppCompatActivity; import com.google.common.base.Charsets; import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.BundledTrustManager; +import eu.siacs.conversations.crypto.CombiningTrustManager; +import eu.siacs.conversations.crypto.TrustManagers; +import eu.siacs.conversations.crypto.XmppDomainVerifier; +import eu.siacs.conversations.entities.MTMDecision; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.MemorizingActivity; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -78,39 +91,40 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.XmppDomainVerifier; -import eu.siacs.conversations.entities.MTMDecision; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.MemorizingActivity; - /** - * A X509 trust manager implementation which asks the user about invalid - * certificates and memorizes their decision. - *

- * The certificate validity is checked using the system default X509 - * TrustManager, creating a query Dialog if the check fails. - *

- * WARNING: This only works if a dedicated thread is used for - * opening sockets! + * A X509 trust manager implementation which asks the user about invalid certificates and memorizes + * their decision. + * + *

The certificate validity is checked using the system default X509 TrustManager, creating a + * query Dialog if the check fails. + * + *

WARNING: This only works if a dedicated thread is used for opening sockets! */ public class MemorizingTrustManager { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); - final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; - public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; - public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; - public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId"; - final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found."; - private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName()); + static final String DECISION_INTENT = "de.duenndns.ssl.DECISION"; + public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; + public static final String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; + public static final String DECISION_TITLE_ID = DECISION_INTENT + ".titleId"; + static final String NO_TRUST_ANCHOR = "Trust anchor for certification path not found."; + private static final Pattern PATTERN_IPV4 = + Pattern.compile( + "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = + Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + private static final Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName()); static String KEYSTORE_DIR = "KeyStore"; static String KEYSTORE_FILE = "KeyStore.bks"; private static int decisionId = 0; @@ -126,54 +140,76 @@ public class MemorizingTrustManager { private String poshCacheDir; /** - * Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. - *

- * You need to supply the application context. This has to be one of: - * - Application - * - Activity - * - Service - *

- * The context is used for file management, to display the dialog / - * notification and for obtaining translated strings. + * Creates an instance of the MemorizingTrustManager class that falls back to a custom + * TrustManager. * - * @param m Context for the application. - * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate. + *

You need to supply the application context. This has to be one of: - Application - + * Activity - Service + * + *

The context is used for file management, to display the dialog / notification and for + * obtaining translated strings. + * + * @param context Context for the application. + * @param defaultTrustManager Delegate trust management to this TM. If null, the user must + * accept every certificate. */ - public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) { - init(m); + public MemorizingTrustManager( + final Context context, final X509TrustManager defaultTrustManager) { + init(context); this.appTrustManager = getTrustManager(appKeyStore); this.defaultTrustManager = defaultTrustManager; } /** * Creates an instance of the MemorizingTrustManager class using the system X509TrustManager. - *

- * You need to supply the application context. This has to be one of: - * - Application - * - Activity - * - Service - *

- * The context is used for file management, to display the dialog / - * notification and for obtaining translated strings. * - * @param m Context for the application. + *

You need to supply the application context. This has to be one of: - Application - + * Activity - Service + * + *

The context is used for file management, to display the dialog / notification and for + * obtaining translated strings. + * + * @param context Context for the application. */ - public MemorizingTrustManager(Context m) { - init(m); + public MemorizingTrustManager(final Context context) { + init(context); this.appTrustManager = getTrustManager(appKeyStore); - this.defaultTrustManager = getTrustManager(null); + try { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + this.defaultTrustManager = defaultWithBundledLetsEncrypt(context); + } else { + this.defaultTrustManager = TrustManagers.createDefaultTrustManager(); + } + } catch (final NoSuchAlgorithmException + | KeyStoreException + | CertificateException + | IOException e) { + throw new RuntimeException(e); + } + } + + private static X509TrustManager defaultWithBundledLetsEncrypt(final Context context) + throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { + final BundledTrustManager bundleTrustManager = + BundledTrustManager.builder() + .loadKeyStore( + context.getResources().openRawResource(R.raw.letsencrypt), + "letsencrypt") + .build(); + return CombiningTrustManager.combineWithDefault(bundleTrustManager); } private static boolean isIp(final String server) { - return server != null && ( - PATTERN_IPV4.matcher(server).matches() + return server != null + && (PATTERN_IPV4.matcher(server).matches() || PATTERN_IPV6.matcher(server).matches() || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); } - private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException { + private static String getBase64Hash(X509Certificate certificate, String digest) + throws CertificateEncodingException { MessageDigest md; try { md = MessageDigest.getInstance(digest); @@ -188,8 +224,7 @@ public class MemorizingTrustManager { StringBuffer si = new StringBuffer(); for (int i = 0; i < data.length; i++) { si.append(String.format("%02x", data[i])); - if (i < data.length - 1) - si.append(":"); + if (i < data.length - 1) si.append(":"); } return si.toString(); } @@ -220,20 +255,22 @@ public class MemorizingTrustManager { } } - void init(final Context m) { - master = m; - masterHandler = new Handler(m.getMainLooper()); - notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE); + void init(final Context context) { + master = context; + masterHandler = new Handler(context.getMainLooper()); + notificationManager = + (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE); Application app; - if (m instanceof Application) { - app = (Application) m; - } else if (m instanceof Service) { - app = ((Service) m).getApplication(); - } else if (m instanceof AppCompatActivity) { - app = ((AppCompatActivity) m).getApplication(); + if (context instanceof Application) { + app = (Application) context; + } else if (context instanceof Service) { + app = ((Service) context).getApplication(); + } else if (context instanceof AppCompatActivity) { + app = ((AppCompatActivity) context).getApplication(); } else - throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!"); + throw new ClassCastException( + "MemorizingTrustManager context must be either Activity or Service!"); File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); @@ -260,12 +297,9 @@ public class MemorizingTrustManager { /** * Removes the given certificate from MTMs key store. * - *

- * WARNING: this does not immediately invalidate the certificate. It is - * well possible that (a) data is transmitted over still existing connections or - * (b) new connections are created using TLS renegotiation, without a new cert - * check. - *

+ *

WARNING: this does not immediately invalidate the certificate. It is well possible + * that (a) data is transmitted over still existing connections or (b) new connections are + * created using TLS renegotiation, without a new cert check. * * @param alias the certificate's alias as returned by {@link #getCertificates()}. * @throws KeyStoreException if the certificate could not be deleted. @@ -275,20 +309,21 @@ public class MemorizingTrustManager { keyStoreUpdated(); } - X509TrustManager getTrustManager(KeyStore ks) { + private X509TrustManager getTrustManager(final KeyStore keyStore) { + Preconditions.checkNotNull(keyStore); try { TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); - tmf.init(ks); + tmf.init(keyStore); for (TrustManager t : tmf.getTrustManagers()) { if (t instanceof X509TrustManager) { return (X509TrustManager) t; } } - } catch (Exception e) { + } catch (final Exception e) { // Here, we are covering up errors. It might be more useful // however to throw them out of the constructor so the // embedding app knows something went wrong. - LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e); + LOGGER.log(Level.SEVERE, "getTrustManager(" + keyStore + ")", e); } return null; } @@ -361,45 +396,60 @@ public class MemorizingTrustManager { } } - - private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) + private void checkCertTrusted( + X509Certificate[] chain, + String authType, + String domain, + boolean isServer, + boolean interactive) throws CertificateException { - LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); + LOGGER.log( + Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); try { LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager"); - if (isServer) - appTrustManager.checkServerTrusted(chain, authType); - else - appTrustManager.checkClientTrusted(chain, authType); + if (isServer) appTrustManager.checkServerTrusted(chain, authType); + else appTrustManager.checkClientTrusted(chain, authType); } catch (final CertificateException ae) { LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae); if (isCertKnown(chain[0])) { - LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore"); + LOGGER.log( + Level.INFO, "checkCertTrusted: accepting cert already stored in keystore"); return; } try { - if (defaultTrustManager == null) - throw ae; + if (defaultTrustManager == null) throw ae; LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager"); - if (isServer) - defaultTrustManager.checkServerTrusted(chain, authType); - else - defaultTrustManager.checkClientTrusted(chain, authType); + if (isServer) defaultTrustManager.checkServerTrusted(chain, authType); + else defaultTrustManager.checkClientTrusted(chain, authType); } catch (final CertificateException e) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master); - final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false); - if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) { + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(master); + final boolean trustSystemCAs = + !preferences.getBoolean("dont_trust_system_cas", false); + if (domain != null + && isServer + && trustSystemCAs + && !isIp(domain) + && !domain.endsWith(".onion")) { final String hash = getBase64Hash(chain[0], "SHA-256"); final List fingerprints = getPoshFingerprints(domain); if (hash != null && fingerprints.size() > 0) { if (fingerprints.contains(hash)) { - Log.d(Config.LOGTAG, "trusted cert fingerprint of " + domain + " via posh"); + Log.d( + Config.LOGTAG, + "trusted cert fingerprint of " + domain + " via posh"); return; } else { - Log.d(Config.LOGTAG, "fingerprint " + hash + " not found in " + fingerprints); + Log.d( + Config.LOGTAG, + "fingerprint " + hash + " not found in " + fingerprints); } if (getPoshCacheFile(domain).delete()) { - Log.d(Config.LOGTAG, "deleted posh file for " + domain + " after not being able to verify"); + Log.d( + Config.LOGTAG, + "deleted posh file for " + + domain + + " after not being able to verify"); } } } @@ -422,17 +472,25 @@ public class MemorizingTrustManager { } private List getPoshFingerprintsFromServer(String domain) { - return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true); + return getPoshFingerprintsFromServer( + domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true); } - private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { + private List getPoshFingerprintsFromServer( + String domain, String url, int maxTtl, boolean followUrl) { Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master); - final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor)); + final boolean useTor = + QuickConversationsService.isConversations() + && preferences.getBoolean( + "use_tor", master.getResources().getBoolean(R.bool.use_tor)); try { final List results = new ArrayList<>(); final InputStream inputStream = HttpConnectionManager.open(url, useTor); - final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8)); + final String body = + CharStreams.toString( + new InputStreamReader( + ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8)); final JSONObject jsonObject = new JSONObject(body); int expires = jsonObject.getInt("expires"); if (expires <= 0) { @@ -459,7 +517,7 @@ public class MemorizingTrustManager { writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); return results; } catch (final Exception e) { - Log.d(Config.LOGTAG, "error fetching posh",e); + Log.d(Config.LOGTAG, "error fetching posh", e); return new ArrayList<>(); } } @@ -489,7 +547,8 @@ public class MemorizingTrustManager { final File file = getPoshCacheFile(domain); try { final InputStream inputStream = new FileInputStream(file); - final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + final String json = + CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); final JSONObject jsonObject = new JSONObject(json); long expires = jsonObject.getLong("expires"); long expiresIn = expires - System.currentTimeMillis(); @@ -514,7 +573,9 @@ public class MemorizingTrustManager { } private X509Certificate[] getAcceptedIssuers() { - return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers(); + return defaultTrustManager == null + ? new X509Certificate[0] + : defaultTrustManager.getAcceptedIssuers(); } private int createDecisionId(MTMDecision d) { @@ -527,7 +588,8 @@ public class MemorizingTrustManager { return myId; } - private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) { + private void certDetails( + final StringBuffer si, final X509Certificate c, final boolean showValidFor) { si.append("\n"); if (showValidFor) { @@ -564,8 +626,7 @@ public class MemorizingTrustManager { // not found", so we use string comparison. if (NO_TRUST_ANCHOR.equals(e.getMessage())) { si.append(master.getString(R.string.mtm_trust_anchor)); - } else - si.append(e.getLocalizedMessage()); + } else si.append(e.getLocalizedMessage()); si.append("\n"); } si.append("\n"); @@ -573,7 +634,7 @@ public class MemorizingTrustManager { si.append("\n\n"); si.append(master.getString(R.string.mtm_cert_details)); si.append('\n'); - for(int i = 0; i < chain.length; ++i) { + for (int i = 0; i < chain.length; ++i) { certDetails(si, chain[i], i == 0); } return si.toString(); @@ -593,24 +654,25 @@ public class MemorizingTrustManager { MTMDecision choice = new MTMDecision(); final int myId = createDecisionId(choice); - masterHandler.post(new Runnable() { - public void run() { - Intent ni = new Intent(master, MemorizingActivity.class); - ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId)); - ni.putExtra(DECISION_INTENT_ID, myId); - ni.putExtra(DECISION_INTENT_CERT, message); - ni.putExtra(DECISION_TITLE_ID, titleId); + masterHandler.post( + new Runnable() { + public void run() { + Intent ni = new Intent(master, MemorizingActivity.class); + ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId)); + ni.putExtra(DECISION_INTENT_ID, myId); + ni.putExtra(DECISION_INTENT_CERT, message); + ni.putExtra(DECISION_TITLE_ID, titleId); - // we try to directly start the activity and fall back to - // making a notification - try { - getUI().startActivity(ni); - } catch (Exception e) { - LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); - } - } - }); + // we try to directly start the activity and fall back to + // making a notification + try { + getUI().startActivity(ni); + } catch (Exception e) { + LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); + } + } + }); LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId); try { @@ -661,7 +723,8 @@ public class MemorizingTrustManager { } @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); } @@ -675,7 +738,6 @@ public class MemorizingTrustManager { public X509Certificate[] getAcceptedIssuers() { return MemorizingTrustManager.this.getAcceptedIssuers(); } - } private class InteractiveMemorizingTrustManager implements X509TrustManager { @@ -686,7 +748,8 @@ public class MemorizingTrustManager { } @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true); } diff --git a/src/main/res/raw/letsencrypt.bks b/src/main/res/raw/letsencrypt.bks new file mode 100644 index 0000000000000000000000000000000000000000..32fd83a6d275cbefdfdc8ee7c210baacb167d775 GIT binary patch literal 2043 zcmb`GdpOi-8^?c#IWW%SxJkq0Z5+aH8aYHlIV*=*p;3~wXm*A%Ll)CCyvlLxZY*Yz z)6%fObO9ohruWEQ5JLwA+=lu~JM$RPNy>~APh7ljK0wRxr15g-sD1b^fMo60i zr>^h8U`MbR5o~>GL^y{P#ZJ`skBEy1V}(cRyRn#@7_ug!zG;Qk6+Ni_Zu}5-~CR=UlBVWH@Jfe!h{@o7*V$i1NPyi7=0igaA4wvy# zaZB=^@3cG}Li!TwCcZ-}>0w*MTGAs~8e`M3$M3YcjM74BBs&*0OnAB5ef;S|X?EZDS(jPX`Cm=~js%bh2uVsj z-04x5L7?|UwfM5O?=3^5i7)! zOhzmzHZpuDU&^mvkPi{RYzjmJFdtF@WIKd|_z(!x&l|?Q*RXykEHw=@+IqfS+&XEK zm(8%B4ZL(_jni4Nz7ow?1$>OgCL+`62Yzm84F9ZwKYfcZxJVWna0e*%ZSE`MiIZ1E z8~C>d#a9J9sr%urYT>F??^~OZZyXUuOc@5KEg4QDLMYwFp?U&>t_wCeqn+6{o zGu3>+BQTrCZh30aZ^TdG$)v-f3GhWSm@oyT5$A0E48KFy>Ro}0=Du}q}LzqZRe zkvKb6Rcx{2Zolf3^28I>TrFwqTJ2ib41_HdSjpwjOpqm_$Jhytqq4A&Y zElPaVllTA3zH*;EmrDTU`jJPpSgU4iTL*%KxVQ4{3V%yEo>@_8%zxN+GZm#8(W%6B zow=9`^@nDkWbL^(PV!@H+&H;H+3y3vAOQZq%bwUAJe$-q^Bj!h$zAB)612J?WPC}P zY;|Omfj|Hu=<iCgTHtCdy)X^|049QJiY}RB%)qyJUP4gqbadbTUoH2X-Nwj{RH-Wdrh?CVde!&hPPRo zJ%935K_I$q=k>KatzC!g`0eCwVqDx$1D(7oTU%AicD}cLS$b1Ip{hT^0)BZ8@z3?y${{0{PRuQMAG^} z3N>X$@8I{t<&9m=%vnd$?vP5OR&35(4kd4HdzsYt*F?JOaI@x>0tQ#8Sg=RrZ?hQ&!`lI>8t+MGU4V@hHyc9|0HcJbUk-lV8x$~CEOFlrxQ9P ZfJc-#)6K97d1r4suDCHp%Lk8D{s%LBDIfp< literal 0 HcmV?d00001 From fe0cbc48ae6e4e2448c9e96e6db9e34cd0a48ee2 Mon Sep 17 00:00:00 2001 From: ghose Date: Thu, 21 Dec 2023 13:20:20 +0000 Subject: [PATCH 006/556] Translated using Weblate (Galician) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 5fc956acb..1ed177ab0 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -157,7 +157,7 @@ \nUsa un xestor de ficheiros diferente para escoller a imaxe. A app que usaches para compartir este ficheiro non concedeu os permisos suficientes. Descoñecido - Desactivado temporalmente + Desactivada temporalmente Conectado Conectando\u2026 Desconectado @@ -251,7 +251,7 @@ Non se desfixo a conversa en grupo Non se puido eliminar a canle Editar o tema da conversa en grupo - Asunto + Tema Entrando na conversa en grupo… Saír Contacto engadido a túa lista de contactos @@ -792,7 +792,7 @@ número de teléfono Valida o teu número de teléfono Quicksy vaiche enviar unha mensaxe SMS (podería ter custos) para validar o teu número de teléfono. Escribe o código de país e número de teléfono: -
%s

É correcto, ou quere modificar o número?]]>
+ Validaremos o número de teléfono

%s

É correcto, ou queres modificar o número?
%s non é un número de teléfono válido. Por favor escribe o teu número de teléfono. Buscar países @@ -863,11 +863,11 @@ Esta canle xa existe Entraches nunha canle existente Non se gardaron os axustes da canle - Permitir que calquera cambie o asunto + Permitir que calquera cambie o tema Permitir que calquera poida convidar - Calquera pode editar o asunto. - As propietarias poden editar o asunto. - Admins poden editar o asunto. + Calquera pode editar o tema. + As propietarias poden editar o tema. + Admins poden editar o tema. Propietarias poden convidar a outras. Calquera pode convidar a outras. Os enderezos XMPP son visibles para a administración. From bc3843524b125aad041d4e33c2b3fdf411385397 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Wed, 27 Dec 2023 20:22:55 +0000 Subject: [PATCH 007/556] Translated using Weblate (German) Currently translated at 100.0% (53 of 53 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4208104.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4208104.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4208104.txt b/fastlane/metadata/android/de-DE/changelogs/4208104.txt new file mode 100644 index 000000000..9f9787c70 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Leichterer Zugriff auf 'QR-Code anzeigen' +* Unterstützung von PEP Native Bookmarks +* Unterstützung für SDP Offer / Answer-Model (wird von SIP Gateways verwendet) +* Anhebung der Ziel-API auf Android 14 From dd4e9ee7cb4b24c6c2018b5daa83fb84bf9261c2 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sat, 30 Dec 2023 22:44:20 +0000 Subject: [PATCH 008/556] Added translation using Weblate (Persian) --- src/quicksy/res/values-fa/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/quicksy/res/values-fa/strings.xml diff --git a/src/quicksy/res/values-fa/strings.xml b/src/quicksy/res/values-fa/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/src/quicksy/res/values-fa/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 165617aa73a3d7532eb80c9aec45143e00050de6 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 31 Dec 2023 11:57:02 +0000 Subject: [PATCH 009/556] Translated using Weblate (Persian) Currently translated at 57.9% (567 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fa/ --- src/main/res/values-fa-rIR/strings.xml | 534 +++++++++++++++++++++++-- 1 file changed, 494 insertions(+), 40 deletions(-) diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index 1c0c91897..d5634f8de 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -24,8 +24,8 @@ هم اکنون 1 دقیقه قبل %d دقیقه قبل - در حال ارسال... - در حال رمزگشایی پیام. لطفا صبور باشید... + در حال ارسال… + در حال رمزگشایی پیام. لطفا صبور باشید… پیام رمز شده به وسیله OpenPGP نام مستعار قبلا استفاده شده نام مستعار نادرست است @@ -38,7 +38,7 @@ مسدود شده ثبت نام حساب جدید بر روی سرور تغییر رمز عبور بر روی سرور - به اشتراک گذاری با ... + هم‌رسانی با… شروع گفتگو دعوت از مخاطب مخاطبین @@ -56,7 +56,7 @@ پیوست فایل افزودن مخاطب ارسال ناموفق بود - در حال به اشتراک گذاری فایل ها. لطفا صبور باشید... + در حال هم‌رسانی پرونده‌ها. لطفاً صبر کنید… پاک سازی تاریخچه پاک سازی تاریخچه گفتگو ها انتخاب دستگاه @@ -72,37 +72,37 @@ راه اندازی مجدد نصب لطفا OpenKeychain را نصب نمایید - ارائه ... - انتظار... + پیشنهاد می‌شود… + در انتظار… کلید OpenPGP یافت نشد کلید های OpenPGP یافت نشدند عمومی - پذیرفتن فایل ها - پذیرفتن خودکار فایل های کوچکتر از ... - پیوست ها + پذیرفتن پرونده‌ها + پذیرفتن خودکار پرونده‌های کوچک‌تر از… + پیوست‌ها اعلان لرزش هنگام دریافت پیام جدید بلرز - اعلان از طریق LED - چشمک زدن چراغ اعلان هنگام رسیدن پیام جدید + اعلان از راه LED + چشمک زدن چراغ اعلان هنگام رسیدن پیام تازه آهنگ زنگ - مهلت + مدت خاموشی پیشرفته هیچ وقت گزارش خرابی را ارسال نکن - پیام ها را تایید کن + تأیید دریافت پیام رابط کاربری پذیرفتن - فایلی که انتخاب نموده اید تصویر نیست - فایل پیدا نشد + پرونده‌ای که انتخاب کرده‌اید تصویر نیست + پرونده پیدا نشد ناشناخته - موقتا غیر فعال شد + موقتاً غیرفعال شد آنلاین آفلاین - غیر مجاز + غیرمجاز سرور پیدا نشد عدم اتصال ثبت نام ناموفق بود - نام کاربری قبلا استفاده شده + نام کاربری قبلاً استفاده شده ثبت نام به پایان رسید برقرای ارتباط امن با شکست مواجه شد نقض سیاست @@ -113,36 +113,36 @@ OpenPGP OMEMO حذف حساب کاربری - موقتا غیر فعال کن - انتشار آواتار + موقتاً غیر فعال کن + انتشار تصویر نمایه انتشار کلید عمومی OpenPGP حذف کلید عمومی OpenPGP - فعال سازی حساب کاربری + فعال‌سازی حساب کاربری ضبط صدا - username@example.com - کلمه عبور - آیا می خواهید %s را به مخاطبان خود اضافه کنید؟ + ‪username@example.com‬ + رمز + آیا می خواهید %s را به مخاطبان خود بیفزایید؟ مشخصات سرور در دسترس خارج از دسترس - آخرین بار لحظاتی قبل مشاهده شده - آخرین بار %d دقیقه قبل مشاهده شده - آخرین بار %d ساعت قبل مشاهده شده - آخرین بار %d روز قبل مشاهده شده - دیگر دستگاه ها - در حال دریافت کلید ها... - رمز گشایی + آخرین بار همین الان + آخرین بار %d دقیقه پیش + آخرین بار %d ساعت پیش + آخرین بار %d روز پیش + دستگاه‌های دیگر + در حال دریافت کلیدها… + رمزگشایی جستجو - وارد کردن مخاطب + واردکردن مخاطب حذف مخاطب مشاهده جزییات مخاطب - بلاک مخاطب - غیر بلاک کردن مخاطب - ارسال مجدد - آدرس وب + مسدودسازی مخاطب + برداشتن مسدودسازی مخاطب + فرستادن دوباره + نشانی وب آفلاین مخاطب - نمایش موقعیت + نمایش موقعیت مکانی لغو آنلاین متوسط @@ -154,13 +154,467 @@ کوچک متوسط بزرگ - پیام برای این دستگاه رمزگذاری نشده + پیام برای این دستگاه رمز نشده. کپی کردن موقعیت به اشتراک گذاری موقعیت به اشتراک گذاری موقعیت نمایش موقعیت به اشتراک گذاری - لطفا صبر کنید... + لطفا صبر کنید… جستجو در پیام ها مشاهده گفتگو - + اثر انگشت OMEMO کپی شد + نمایش مسدودسازی‌ها + سپس این گفتگو را ببند + سرور push دلخواه کاربر برای رساندن اعلان‌های push از راه XMPP به دستگاه شما. + وصل شدن + ورود + شما وصل نیستید. بعداً دوباره تلاش کنید + ضبط پیام صوتی + گرفتن اجازه‌های عضویت + هیچ نشانی XMPPای پیدا نشد + لطفاً با احتیاط تغییر دهید + XEP-0363: HTTP File Upload + مخاطب تازه‌ای بیفزایید، گفتگوی گروهی بسازید یا بپیوندید، یا کانال‌های تازه بیابید + حساب شما + فرستاده می‌شود (%1$d%% انجام شده) + پاسخ + خموشاندن + %s دارند تایپ می‌کنند… + %s فرستاده می‌شود + گزینه‌های پیام + شکست موقتی برای تأیید هویت + گرفتن عکس + تاریخچه از سرور دریافت می‌شود + آیا می‌خواهید %s را از فهرست مخاطبان خود پاک کنید؟ گفتگوها با این مخاطب پاک نخواهند شد. + تنظیم + XEP-0280: Message Carbons + برنامه‌ای که با آن این عکس را هم‌رسانی کرده‌اید اجازهٔ کافی نمی‌دهد. + اثر انگشت‌های OMEMO مورد اعتماد + کنش سریع را برگزینید + خصوصی، فقط برای اعضا + نهفتن اعلان‌ها + آیا می‌خواهید حساب کاربری خود را حذف کنید؟ این کار تاریخچهٔ همهٔ گفتگوهای شما را از بین می‌برد + این ویژگی با هیچ حساب فعالی ممکن نیست + نگذارید سیستم‌عامل اتصال شما را قطع کند + نشانی XMPP کپی شد + بیرون‌انداخته‌شده + %s تا این‌جا خوانده است + v\\اثر انگشت OMEMO (منبع پیام) + تماس ویدیویی + جایگزینی دکمهٔ «ارسال» با کنش‌های سریع + انتقال پرونده لغو شد + %s برای بارگیری پیشنهاد شده + تماس ویدیویی جاری + تماس دریافتی + تماس جاری + هشدار: فرستادن بدون به‌روزرسانی وصل‌بودن از هر دو طرف مشکلاتی به وجود می‌آورد. +\n +\nبه «جزئیات مخاطب» بروید تا مطمئن شوید که به‌روزرسانی وصل‌بودن شما فعال است. + بازیابی پشتیبان + ‪ channel@conference.example.com‬ + تغییر رمز + نوشتن + جزئیات کانال + سند متنی + بگذارید مخاطبان‌تان بدانند که پیام‌های آن‌ها را گرفته یا خوانده‌اید + فعال‌سازی همهٔ حساب‌ها + XEP-0357: Push + مخاطب شما دستگاه‌های تأییدنشده به کار می‌برد. بارکد دوبعدی آن‌ها را برای تأیید اسکن کنید تا امکان شنود با «حملهٔ مرد میانی» را از بین ببرید. + مشغول + قطع + هم‌رسانی پرونده ممکن نبود + حذف حساب از سرور + گواهینامه‌های امنیتی را حذف کن + تبدیل تصویر ممکن نبود + پشتیبان‌گیری + عضویت + تا اطلاع ثانوی + موضوع + پرونده حذف شد + ادامه + Username + تنظیمات گفتگوی گروهی تغییر کرد! + ثبت نام با این سرور ممکن نیست + پاسخ + تماس ویدیویی دوباره وصل می‌شود + تغییر دوربین ممکن نبود + پشتیبان‌گیری آغاز شد. شما از پایانش باخبر خواهید شد. + ثبت‌نام حساب تازه ممکن نیست + برای تماس Tor را خاموش کنید + ساختن + سنجاق‌کردن به بالا + شبکهٔ Tor در دسترس نیست + آیا می‌خواهید مسدودکردن %s را بردارید تا او دوباره بتواند به شما پیام بفرستد؟ + آیا می‌خواهید این کانال عمومی را حذف کنید؟ +\n +\nهشدار: این کانال کاملاً از سرور حذف خواهد شد. + گزارش هرزنامه و مسدودسازی فرستنده + %1$s شکست خورد + مشکل در تأیید + شما از این گفتگوی گروهی به خاطر دلایل فنی خارج شده‌اید + رمز تازه + جستجوی مخاطبان + تازگی به‌کاررفته + ساختن دوبارهٔ کلید OMEMO + گشودن %s + بارگیری %s + انجام کنش با + شناسهٔ کلید OpenPGP + لغو انتقال + %s تایپ می‌کند… + رد درخواست تبدیل به ویدیو + این گفتگوی گروهی فقط برای اعضاست + این گفتگوی گروهی تعطیل شده است + + %1$d عضو را ببینید + %1$d عضو را ببینید + + برگزیدن پرونده + تصویر نمایه منتشر شد! + حذف گفتگوی گروهی ممکن نبود + + %d گواهینامهٔ امنیتی حذف شد + %d گواهینامهٔ امنیتی حذف شد + + حذف %s + فرستادن موقعیت مکانی + دریافت به‌روزرسانی وصل‌بودن + %s تا این‌جا خوانده‌اند + بگذارید مخاطبانتان پیام‌ها را پس از ارسال ویرایش کنند + تنها یک تماس در هر زمان ممکن است. + %1$s +%2$d تا این‌جا خوانده‌اند + XEP-0191: Blocking Command + هیچ + حافظه کم آمد. تصویر خیلی بزرگ است + دربارهٔ %s + این مخاطب از قبل وجود دارد + پخش صدا + پاک‌کردن پرونده + (یا نگه دارید تا به حالت پیش‌فرض برگردد) + فعال‌کردن ساعت‌های خاموشی + حذف از کانال + شما می‌خواهید %s را از یک کانال عمومی بیرون کنید. تنها راهش این است که این کاربر را برای همیشه ممنوع کنید. + مسدودکردن همهٔ مخاطبان از %s را بردارید؟ + نشانی پرونده + حذف کانال ممکن نبود + تماس دریافتی (%s) · %s + سرور شما نمی‌تواند تصویر نمایه را منتشر کند + خروج از حساب کاربری + تماس ازدست‌رفته · %s + پاک‌کردن دستگاه‌ها + XEP-0215: External Service Discovery + آیا می‌خواهید کلید عمومی OpenPGP خود را از اعلان حضور خود بردارید؟ +\nمخاطبان شما نخواهند توانست پیام‌های رمزشده با OpenPGP بفرستند. + حالت پیشرفته + بگذارید مخاطبانتان بدانند که دارید برای آن‌ها تایپ می‌کنید + آیا می‌خواهید همهٔ دستگاه‌های دیگر را از اعلام OMEMO دربیاورید؟ دفعهٔ بعدی که آن دستگاه‌ها وصل شوند، دوباره خودشان را اعلام می‌کنند، ولی ممکن است پیغام‌هایی را که در این بین فرستاده می‌شود نگیرند. + حساب XMPP + رمز فعلی + گفتگوی گروهی + تغییر نقش %s ممکن نبود + رمز نمی‌تواند خالی باشد + رمز عوض شد! + همهٔ گواهینامه‌های امنیتی باید مستقیماً تأیید شوند + رمزگذاری پیام ممکن نبود چون مخاطبان شما کلید عمومی خود را منتشر نمی‌کنند. +\n +\nلطفاً از آن‌ها بخواهید تا OpenPGP را راه بیندازند. + با فرستادن گزارش‌های خرابی به توسعهٔ این برنامه کمک می‌کنید + نام کاربری + نسخهٔ پشتیبان شما ساخته شده است + جستجوی گفتگوی گروهی + رمزشده با OMEMO + تصویر + %1$s دریافت می‌شود (%2$d%% انجام شده) + ویرایش موضوع گفتگوی گروهی + راهنما + انتخاب عکس + XEP-0163: PEP (Avatars / OMEMO) + برنامهٔ ناسازگار + هیچ (غیرفعال) + روی تصویر نمایه بزنید تا یک تصویر تازه از گالری برگزینید + اجازهٔ اصلاح پیام‌ها + خطای عمومی خواندن/نوشتن. شاید فضای ذخیره‌سازی شما پرشده؟ + مدیریت حساب + صدای زنگ برای تماس‌های دریافتی + ذخیره به عنوان نشانک + درخواست دهید + رد کردن + تصویر نمایهٔ شما + ساختن پرونده‌های پشتیبان + تماس پایان می‌یابد + برای مدیریت حساب‌ها این‌جا بروید + اعطای اجازه‌های عضویت + ممنوع کن + تماس دوباره وصل می‌شود + بارگیری شکست خورد: نوشتن پرونده ممکن نبود + نادیده بگیرید + تماس ازدست‌رفته + بررسی اندازهٔ %1$s روی %2$s + هم‌گام‌سازی نشانک‌ها + بررسی اندازهٔ %s + فرستادن با Enter + پرونده‌های پشتیبان در %s ذخیره می‌شوند + لطفاً از مخاطب خود درخواست به‌روزرسانی وصل‌بودن کنید. +\n +\nبا این کار می‌شود فهمید که او چه برنامه‌ای را به کار می‌برد. + همه تا این‌جا خوانده‌اند + v\\اثر انگشت OMEMO + %s پیشنهاد می‌شود + OpenKeychain یک خطا داشت. + خروج + این نام کاربری معتبر نیست + دامنه قابل تأیید نیست + همهٔ مخاطبان از %s را مسدود کنید؟ + تماس‌ها هنگام به‌کارگیری Tor غیرفعال هستند + آفلاین‌ها را پنهان کن + تماس خروجی (%s) · %s + نمایش برچسب‌های فقط-خواندنی زیر مخاطبان + سرور انتشار شما را نپذیرفت + نقل‌قول + جزئیات حساب + آماده‌سازی برای هم‌رسانی پرونده + شما از این گفتگوی گروهی اخراج شده‌اید + امنیت + ارسال‌های شکست‌خورده + بارگیری شکست خورد: پروندهٔ نامعتبر + غیرفعال‌سازی همهٔ حساب‌ها + به صادرکنندگان گواهینامه‌های امنیتی سیستم اعتماد نکن + اعطای اجازه‌های مالکیت + تصویر نمایهٔ %s + کلید عموم OpenPGP منتشر شد. + %s دیگر تایپ نمی‌کند + مخاطب جدید به فهرست مخاطبانتان افزوده شود؟ + زمان پایان + اتصال به حساب ممکن نبود + کنش سریع + هیچ برنامه‌ای پیدا نشد + لغو مسدودسازی عضو + ممنوعیت نماگرفت + تماس ویدیویی دریافتی + محدودیت منابع + ترک + چشم‌پوشی + بستن گفتگو + پروندهٔ چندرسانه‌ای + آماده‌سازی برای فرستادن عکس + فرستادن پیام خصوصی به %s + موقعیت مکانی + آیا می‌خواهید همهٔ پیام‌ها در این گفتگو را پاک کنید؟ +\n +\nهشدار: این کار تأثیری روی پیام‌هایی که روی دستگاه‌ها یا سرورهای دیگر ذخیره شده‌اند ندارد. + آیا می‌خواهید %s را مسدود کنید تا نتواند پیامی به شما بدهد؟ + کپی اثر انگشت OMEMO + کانال را مدیریت‌شده کن + برنامهٔ اندروید + بارگیری شکست خورد: اتصال به میزبان ممکن نبود + شما دیگر در این گفتگوی گروهی نیستید + افزودن متقابل + خطا در گشودن ارتباط + اتصال از بین رفت + گفتگوی گروهی را ترک کرد + رمزگذاری پیام ممکن نبود چون مخاطب شما کلید عمومی‌اش را منتشر نمی‌کند. +\n +\nلطفاً از مخاطب خود بخواهید تا OpenPGP را راه بیندازد. + تماس وصل نشد + اعطای اجازه‌های مدیریت + تصویر برداری + میکروفون شما دردسترس نیست + آخرین یک روز پیش + ذخیرهٔ تصویر نمایه ممکن نبود + با این سرور نمی‌توان دعوت‌نامه ساخت + هیچ کلید قابل استفاده‌ای برای این مخاطب وجود ندارد. +\nدریافت کلیدهای تازه از سرور ممکن نبود. شاید سرور مخاطب شما خراب است؟ + با حساب %s + نشانی XMPP را مسدود کن + تبدیل پروندهٔ عکس ممکن نبود + هیچ سروری برای گفتگوی گروهی پیدا نشد + خارج‌شده از حساب کاربری + + %d تماس ازدست‌رفته + %d تماس ازدست‌رفته + + برگزیدن + آماده‌سازی برای فرستادن عکس‌‌ها + به‌روز می‌شود… + ‪ channel@conference.example.com/nick‬ + اتصال به برخی از حساب‌ها ممکن نبود + تلاش دوباره + محتوای برنامه را در منوی تغییر برنامه‌ها سیاه کن و جلوی ذخیرهٔ نماگرفت را بگیر + حذف از گفتگوی گروهی + مسیر GPX + تماس رد شد + در حال عضویت در گفتگوی گروهی… + تبدیل به ویدیو + نام نمایشی تازه + این حساب وجود دارد + حذف نشانک + شما دستگاه‌های تأییدنشده به کار می‌برید. بارکد دوبعدی دستگاه‌های دیگرتان را برای تأیید اسکن کنید تا امکان شنود با «حملهٔ مرد میانی» را از بین ببرید. + پیکربندی کانال عمومی + دعوت‌نامه مفهوم نیست + توقف پخش صدا + اعلان تایپ‌کردن + فرستادن به‌روزرسانی وصل‌بودن + درخواست دریافت به‌روزرسانی وصل‌بودن + پیام رمز شده است. برای بازکردنش OpenKeychain را نصب کنید. + انتشار + نمایش کلید Enter + حذف کانال + زمان آغاز + به %s + دوباره وصل می‌شود + گرفتن اجازه‌های مدیریت + آیا می‌خواهید این پرونده را پاک کنید؟ +\n +\nهشدار: با این کار نسخه‌های این پرونده که روی دستگاه‌ها یا سرورهای دیگر هستند پاک نمی‌شود. + تماس خروجی · %s + سرور Push + + %1$d تماس ازدست‌رفته از %2$s + %1$d تماس ازدست‌رفته از %2$s + + در حال بررسی %s روی میزبان HTTP + حذف تصویر نمایه + تغییر کلید شکلک‌ها به کلید Enter + حذف انتخاب‌ها + برنامه‌ای برای گشودن پرونده وجود ندارد + درخواست عضویت را خودبه‌خود بپذیر + گواهینامه‌های امنیتی تأییدشده را حذف کن + آخرین بار یک ساعت پیش + تأیید + رمزشده با OpenPGP + تماس صوتی + بازگشت به گفتگو + فعال‌کردن + تغییر تنظیمات گفتگوی گروهی ممکن نبود + تنظیمات پیشرفته + شما از این گفتگوی گروهی منع شده‌اید + میزبانی شده روی %s + + %1$d تماس ازدست‌رفته از %2$d مخاطب + %1$d تماس ازدست‌رفته از %2$d مخاطب + + خطای برنامه + فرستادن پیام خصوصی + فعال‌سازی حساب را فراموش نکنید. + ذخیره به عنوان گفتگوی گروهی + کتاب صوتی + نشانی‌های XMPP را برای همه نمایان کن + وصل می‌شود… + پشتیبان شما بازیابی شد + متفرقه + وصل + خطایی رخ داد + اصلاح پیام ممکن نبود + برنامه‌ای برای دیدن مخاطب وجود ندارد + تماس دریافتی + سند PDF + توزیع‌کنندهٔ UnifiedPush + پرونده‌های پشتیبان در %s ذخیره شده‌اند + فعال‌کردن ویدیو ممکن نبود. + اسکن بارکد دوبعدی + XEP-0237: Roster Versioning + پیغام خصوصی + صدای اعلان + کد ثبت‌نام اشتباه + کلید اشتباه برای رمرگذاری. + ویدیو + + ارسال یک پیام ممکن نبود + ارسال برخی از پیام‌ها ممکن نبود + + زنگ می‌خورد + گزارش هرزنامه + کانال عمومی را ترک کرد + برداشتن سنجاق از بالا + رمزنشده + گفتگو بسته شد + حسابی که از راه آن اعلان‌های push دریافت می‌شوند. + مدت زمانی که اعلان‌های این برنامه، پس از این که شما روی دستگاه‌های دیگرتان فعالیت می‌کنید، خاموش می‌مانند. + عضو + شما عضو نیستید + ساخت گفتگوی گروهی ممکن نبود + جاگذاری مثل نقل‌قول + گواهینامه‌های امنیتی را حذف کن + نشانی کپی شد + هرگز + منع‌کردن از گفتگوی گروهی + اثر انگشت OMEMO (منبع پیام) + %1$s برنامهٔ <b>OpenKeychain</b> را به کار می‌برد تا پیام‌ها را رمزنگاری کند و کلید عمومی شما را مدیریت کند.<br><br>با پروانهٔ ‪GPLv3+‬ منتشر شده است و روی اف-دروید و گوگل‌پلی در دسترس است.<br><br><small>(لطفاً پس از آن %1$s را دوباره اجرا کنید.)</small> + پیکربندی گفتگوی گروهی خصوصی + مخاطب به فهرست مخاطبان افزوده شد + حذف گفتگوی گروهی + این یک نشانی XMPP معتبر نیست + منتشر می‌شود… + گزینه‌های بیشتر + XEP-0313: MAM + نمایش بارکد دوبعدی + ساعت‌های خاموشی + یکی چیزی کار نکرد + با کلید Enter پیام فرستاده می‌شود. حتی اگر این گزینه فعال نباشد، همیشه می‌توانید با Ctrl+Enter پیغام را بفرستید. + مسدودسازی عضو + گفتگوی گروهی رمز می‌خواهد + تنظیم ‪“autojoin”‬ را فعال کنید تا ورود و خروج در گفتگوهای گروهی از دستگاه‌های دیگر این‌جا هم اعمال شود. + تغییر رمز ممکن نبود + برچسب‌های پویا + این گفتگو + کپی نشانی اصلی + برنامه‌ای برای نمایش موقعیت مکانی پیدا نشد + %s دیگر تایپ نمی‌کنند + تماس خروجی + رد + برنامه‌ای که با آن این عکس را انتخاب کرده‌اید اجازهٔ خواندنش را نمی‌دهد. +\n +\nبرنامهٔ دیگری برای انتخاب عکس به کار ببرید. + تماس پذیرفته می‌شود + تصویر نمایهٔ حساب + اثر انگشت OMEMO + پیغام خطا کپی شد + پرونده + برنامه‌ای برای گشودن پیوند وجود ندارد + حذف حساب از سرور ممکن نبود + تاریخچهٔ بیشتری در سرور نیست + نشست ایجاد شد + ورود رمز + صدای اعلان برای پیام‌های تازه + خطا + بارگیری شکست خورد: سرور پیدا نشد + XEP-0198: Stream Management + همهٔ گفتگوها + قطع اعلان‌ها + آیا می‌خواهید نشانک‌گذاری %s را بردارید؟ گفتگوها با این نشانک پاک نخواهند شد. + بازگشت به تماس جاری + آخرین بار یک دقیقه پیش + تغییر نسبت %s ممکن نبود + هیچ گواهینامهٔ امنیتی‌ای مستقیماً تأیید نشده + تبدیل به تماس ویدیویی؟ + علامت‌گذاری به عنوان خوانده‌شده + گرفتن اجازه‌های مالکیت + هیچ کلید قابل استفاده‌ای برای این مخاطب وجود ندارد. +\nمطمئن شوید که هر دوی شما به‌روزرسانی وصل‌شدن را می‌گیرید. + آیا می‌خواهید این گفتگوی گروهی را حذف کنید؟ +\n +\nهشدار: گفتگوی گروهی کاملاً از سرور حذف خواهد شد. + پیام تازهٔ رمزشده با OpenPGP پیدا شد + + %d گفتگوی خوانده‌نشده + %d گفتگوی خوانده‌نشده + + صدا + بدون نسبت + وصل می‌شود + فرستادن اطلاعات مربوط به اشکال برنامه از راه حساب XMPP شما به توسعهٔ %1$s در آینده کمک می‌کند. + سرویس پیش‌زمینه + منع‌کردن از کانال + نشانی XMPP + اعلان‌ها در این مدت خاموش خواهند ماند + XEP-0352: Client State Indication + فعال‌سازی اعلان‌ها + بارگیری شکست خورد: پرونده پیدا نشد + دعوت + یافتن دستگاه‌ها + %1$s از گفتگوی گروهی رفت + دعوت به Conversations + انجام شد + کلید عمومی اطلاع‌رسانی نشده + \ No newline at end of file From 1cc13d175f98fe573210cb377234bc95d93396cf Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 31 Dec 2023 08:46:25 +0000 Subject: [PATCH 010/556] Translated using Weblate (Persian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/fa/ --- src/conversations/res/values-fa-rIR/strings.xml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-fa-rIR/strings.xml b/src/conversations/res/values-fa-rIR/strings.xml index 0f3362506..dcf72204c 100644 --- a/src/conversations/res/values-fa-rIR/strings.xml +++ b/src/conversations/res/values-fa-rIR/strings.xml @@ -3,4 +3,18 @@ لطفا سرویس دهنده پیام خود را انتخاب نمائید. برای مثال artalk.im از Conversations.im استفاده کنید حساب کاربری جدیدی بسازید - \ No newline at end of file + عضو %1$s شو و با من گفتگو کن: %2$s + شما به %1$s دعوت شده‌اید. ما شما را در ساخت حسابتان راهنمایی می‌کنیم. +\nوقتی شما %1$s را به عنوان سرویس‌دهندهٔ خود برگزینید، خواهید توانست با کاربران بقیهٔ سرویس‌دهنده‌ها نیز ارتباط داشته باشید. کافی است نشانی کامل XMPP خود را به آن‌ها بدهید. + آیا خودتان حساب XMPP دارید؟ اگر جایی دیگری با همین برنامه یا برنامه‌های مشابه کار می‌کنید باید داشته باشید. اگر حساب XMPP ندارید، همین‌جا می‌توانید یکی بسازید. +\nنکته: برخی از سرویس‌دهنده‌های ایمیل به شما حساب XMPP هم ارائه می‌دهند. + اگر مخاطب شما نزدیکتان است، می‌تواند برای پذیرش دعوت‌نامه کد زیر را اسکن کند. + هم‌رسانی دعوت‌نامه با… + شبکهٔ XMPP مستقل از سرویس‌دهنده‌هایش است. بنابراین شما می‌توانید این برنامه را با هر سرویس‌دهنده‌ای که می‌خواهید به کار ببرید. +\nولی برای راحتی شما، ما امکان ساخت حساب روی سرور conversations.im را می‌دهیم؛ سرویس‌دهنده‌ای که برای کار با این برنامه بهینه شده است. + شما به %1$s دعوت شده‌اید و یک نام کاربری برایتان برگزیده شده است. ما شما را در ساخت حسابتان راهنمایی می‌کنیم. +\nشما خواهید توانست با کاربران بقیهٔ سرویس‌دهنده‌ها نیز ارتباط داشته باشید. کافی است نشانی کامل XMPP خود را به آن‌ها بدهید. + کد مورد نظر اشتباه ساخته شده است + روی گزینهٔ هم‌رسانی بزنید تا مخاطب خود را به %1$s دعوت کنید. + دعوت‌نامهٔ سرور شما + \ No newline at end of file From 53a7ea362dc30958a13f31d9ea6a6a755b1d0d90 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 31 Dec 2023 23:39:44 +0000 Subject: [PATCH 011/556] Translated using Weblate (German) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index cfc29734b..f3100e3f7 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -91,12 +91,12 @@ \nAchtung: Dies löscht keine Kopien dieser Datei, die auf anderen Geräten oder Servern gespeichert sind.
Diese Unterhaltung danach beenden Gerät auswählen - Unverschlüsselt schreiben… + Unverschlüsselt schreiben Nachricht senden Nachricht an %s senden - OMEMO-verschlüsselt schreiben… + OMEMO-verschlüsselt schreiben v\\OMEMO-verschlüsselte Nachricht senden - OpenPGP-verschlüsselt schreiben… + OpenPGP-verschlüsselt schreiben Neuer Nickname wird jetzt verwendet Unverschlüsselt senden Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel. From 7b9b90df4d8f8afef3c42345c4010bdad3b82918 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 31 Dec 2023 22:57:06 +0000 Subject: [PATCH 012/556] Translated using Weblate (Persian) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fa/ --- src/main/res/values-fa-rIR/strings.xml | 455 ++++++++++++++++++++++++- 1 file changed, 443 insertions(+), 12 deletions(-) diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index d5634f8de..0de5326b6 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -146,23 +146,23 @@ لغو آنلاین متوسط - گواهی ناشناخته را بپذیر؟ - هم اکنون غیر فعال کن - سایز فونت - فعال به صورت پیش فرض - غیر فعال به صورت پیش فرض + تأییدیهٔ امنیتی ناشناخته پذیرفته شود؟ + هم اکنون غیرفعال کن + اندازهٔ قلم + فعال به صورت پیش‌فرض + غیرفعال به صورت پیش‌فرض کوچک متوسط بزرگ پیام برای این دستگاه رمز نشده. - کپی کردن موقعیت - به اشتراک گذاری موقعیت - به اشتراک گذاری موقعیت - نمایش موقعیت - به اشتراک گذاری + کپی موقعیت مکانی + هم‌رسانی موقعیت مکانی + هم‌رسانی موقعیت مکانی + نمایش موقعیت مکانی + هم‌رسانی لطفا صبر کنید… - جستجو در پیام ها - مشاهده گفتگو + جستجوی پیام‌ها + دیدن گفتگو اثر انگشت OMEMO کپی شد نمایش مسدودسازی‌ها سپس این گفتگو را ببند @@ -617,4 +617,435 @@ دعوت به Conversations انجام شد کلید عمومی اطلاع‌رسانی نشده + در حال تأیید… + شما از این حساب خارج شده‌اید + پیغام‌ها به خاطر زمان نگه‌داری آفلاین دریافت نمی‌شوند. + تأییدیه‌های امنیتی سرورهایی که دارای نام میزبان تأییدشده هستند تأیید می‌شوند + عضو کانال عمومی شوید + پشتیان Conversations + دور از دسترس + این دستگاه دیگر فعالیتی ندارد + مسیریابی + + %d ساعت + %d ساعت + + فشرده‌سازی ویدیو + فقط تصاویر بزرگ + این دستگاه تأیید شده است + دستگاه شما از غیرفعال‌سازی صرفه‌جویی داده برای %1$s پشتیبانی نمی‌کند. + مخاطب مسدود شده است. + نام سرور با وجود عدم تطابق پذیرفته شود؟ + مسدودسازی غریبه‌ها + حذف خودکار پیام‌ها + کپی + اجازهٔ استفاده از اینترنت را بده + + %d هفته + %d هفته + + غیرفعال‌سازی رمزگذاری + تأیید %s + ویرایش پیغام وضعیت + آیا می‌خواهید در هر حال وصل شوید؟ + سیستم‌عامل شما %1$s را وقتی در پس‌زمینه است برای دسترسی به اینترنت محدود می‌کند. برای دریافت اعلان پیغام‌های تازه باید در این حالت به %1$s اجازهٔ نامحدود بدهید. +\n%1$s در صورت امکان تلاش می‌کند که در مصرف داده صرفه‌جویی کند. + هم‌رسانی URI با… + %1$s نمی‌تواند به %2$s پیام‌های رمزشده بفرستد. شاید چون مخاطب شما سرور قدیمی‌ای به کار می‌برد یا برنامه‌ای دارد که از OMEMO پشتیبانی نمی‌کند. + متوسط (360p) + ارسال‌های شکست‌خورده + ذخیرهٔ پیکربندی کانال ممکن نبود + تیره + اعضا + لطفاً رمز این حساب را وارد کنید + تماس‌های ازدست‌رفته + تأییدیهٔ امنیتی دارای نشانی XMPP نیست + سلب اعتماد از دستگاه + هیچ برنامه‌ای برای نصب برنامه‌ها نصب نیست. + تصویر با %s هم‌رسانی شد + بارگیری پیام‌های بیشتر + فشرده‌سازی تصاویر + مدیریت دسترسی‌ها + روی میزبان دیگر دوباره وصل شو + تنظیمات اعلان تماس‌های دریافتی + میزبان‌ها را از راه DNSSEC راستی‌آزمایی کن + کپی نشانی XMPP + این نام میزبان معتبر نیست + پیام + دریافت فهرست دستگاه‌ها ممکن نبود + موقتاً در دسترس نیست. بعداً دوباره تلاش کنید. + برای انتشار تصویر نمایه باید وصل باشید. + برای بازیابی پشتیبان رمز خود را برای حساب %s وارد کنید. + اثر انگشت تأییدشده + مشغول + همهٔ اتصال‌ها را از شبکهٔ Tor رد کنید. نیاز به Orbot دارد + نشانی XMPP را وارد کنید + نام شما + نمایش به عنوان غایب اگر گوشی قفل شده باشد + پس از فرستادن پیام به پایین فهرست پیام‌ها برو + ساخت گفتگوی گروهی… + پیام‌ها از این دستگاه را که قدیمی‌تر زمان تعیین‌شده هستند به طور خودکار پاک کن. + با همهٔ پیام‌ها اعلان بده + اصلی (فشرده‌نشده) + بیشتر کاربران باید برای پیشنهادهای بهتر از همهٔ شبکهٔ عمومی XMPP گزینهٔ ‪‘jabber.network’‬ را برگزینند. + بارکد دوبعدی نامعتبر + بگذارید همه دیگران را دعوت کنند + نصب Orbot + برنامه‌های محافظت‌شده + کش را خالی کن + کپی اثر انگشت دیجیتال + اعلان‌های شناور + انتخاب حساب + این گفتگوی گروهی خصوصی هیچ عضوی ندارد. + بازیابی پشتیبان + برای دریافت اعلان‌ها حتی وقتی که صفحه خاموش است باید این برنامه را به فهرست برنامه‌های محافظت‌شده بیفزایید. + بارکد را به کمک دوربین اسکن کنید + برای تنظیم نام دکمهٔ ویرایش را به‌کار ببرید. + همیشه برای گفتگوهای تکی و گروهای خصوصی OMEMO به کار برود. + به حافظهٔ خارجی دسترسی %1$s را بدهید + پیام‌های دریافتی از غریبه‌ها + این شمارهٔ تلفن هم‌اینک با یک دستگاه دیگر وارد شده است. + غایب با گوشی قفل‌شده + نام گفتگوی گروهی + مسدودسازی کل دامنه + خیر + اتصال با Tor + ترجیحات آرشیوگیری + تنظیمات وضعیت را دستی انجام دهید + هم‌رسانی به شکل ‪XMPP URI‬ + هم‌اینک آنلاین + وضعیت + نمایش غیرفعال‌ها + پاک‌کردن فضای ذخیرهٔ خصوصی + تنظیمات اعلان پیام‌ها + سرور محلی + شما قرار است کلیدهای OMEMO متعلق به %1$s را با کلیک روی یک پیوند تأیید کنید. این کار فقط وقتی امن است که شما این پیوند را از منبع مورداعتمادی گرفته باشید که فقط %2$s می‌توانسته منتشر کند. + خراب + همه می‌توانند دیگران را دعوت کنند. + آیا می‌خواهید رمزگذاری OMEMO را برای این گفتگو غیرفعال کنید؟ +\nبا این کار مدیر سرور شما می‌تواند پیام‌هایتان را بخواند، ولی شاید این تنها راهی باشد که با آن بتوانید با افراد دارای برنامهٔ قدیمی پیام بفرستید. + + %d روز + %d روز + + jabber.network + تماس‌های دریافتی + این کانال عمومی هیچ عضوی ندارد. مخاطبان خود را به آن بیفزایید یا با دکمهٔ هم‌رسانی نشانی XMPP آن را پخش کنید. + اطلاعات وصل‌بودن + جستجوی کانال‌ها + گوشی همراه + ساخت حساب + سرور دیگر سافته نشد + به جای نقشهٔ توکار، افزونهٔ هم‌رسانی موقعیت مکانی را به‌کار ببر + یافتن کانال‌ها + کنسول + یک بار + یک راهنما برای ساخت حساب در conversations.im فراهم است. +\nاگر conversations.im را به عنوان سرویس‌دهنده برگزینید خواهید توانست با کاربران بقیهٔ سرویس‌دهنده‌ها ارتباط داشته باشید اگر نشانی XMPP کامل خود را به آن‌ها بدهید. + فشرده‌سازی ویدیو + سرویس پیش‌زمینه + همه می‌توانند موضوع را ویرایش کنند. + ما به شما پیامک دیگری با یک کد ۶ رقمی فرستاده‌ایم. + خطا در دریافت کلید OMEMO! + نیاز به CAPTCHA + بازیابی + سازوکار SASL به عقب بازگردانده شد + GIF + پرونده زیادی بزرگ + مدیران می‌توانند موضوع را ویرایش کنند. + مرورگر رسانه + فضای ذخیرهٔ خصوصی را که پرونده‌ها را نگه می‌دارد پاک کن (پرونده‌ها دوباره از سرور بارگرفته می‌شوند) + به‌روزرسانی + برای گفتگوهای تازه OMEMO باید صریحاً روشن شود. + یک کشور برگزینید + این بخش اجباری است + بازسازی تأییدیهٔ امنیتی + (هیچ حساب فعالی نیست) + + %d پیام + %d پیام + + جستجوی اعضا + اعلان‌ها غیرفعال است + روشن + ثبت حساب تازه + تماس‌های خروجی + لطفاً صبر کنید تا کلیدها دریافت شوند + برای پیام‌ها و تماس‌ها از غریبه‌ها اعلان نشان بده. + دعوت دوباره + پورت + کلیدهای OMEMO خود را دوباره بسازید. همهٔ مخاطبان باید دوباره تأییدتان کنند. فقط به عنوان آخرین راه حل این کار را بکنید. + پیغام وضعیت + گفتگوی گروهی خصوصی بسازید + این ویژگی پیاده‌سازی نشده است + هم‌رسانی به شکل بارکد + به دستگاه‌های تازهٔ مخاطبان تأییدنشده آسان‌گیرانه اعتماد کن، ولی برای دستگاه‌های تازهٔ مخاطبان تأییدشده درخواست تأیید بفرست. + پرداخت ضروری است + این کانال از قبل وجود دارد + نام کانال + برنامهٔ هم‌رساننده اجازهٔ دسترسی به این پرونده را نداد. + بله + + %d ثانیه + %d ثانیه + + آیا می‌خواهید فرایند ثبت‌نام را متوقف کنید؟ + ما به شما پیامکی به %s فرستاده‌ایم. + شما دارید یک قالب قدیمی نسخهٔ پشتیبان را وارد می‌کنید + کانال عمومی ساخته می‌شود… + هم‌رسانی موقعیت مکانی غیرفعال است + دریافت ترجیحات آرشیوگیری ممکن نبود + نام نمایشی + بارکد شامل اثر انگشت دیجیتالی این گفتگو نیست. + حذف هویت‌های OMEMO + شما قرار است کلیدهای OMEMO حساب خودتان را تأیید کنید. این کار فقط وقتی امن است که شما این پیوند را از منبع مورد اعتمادی گرفته باشید که فقط خودتان می‌توانستید منتشرش کنید. + این حساب قبلاً ساخته شده است + آغاز ضبط ممکن نبود + بهینه‌سازی مصرف باتری فعال + بازگشت + هنگام پردازش درخواست شما یک چیزی کار نکرد. + به کلیدهای OMEMO آسان‌گیرانه اعتماد شد، یعنی ممکن است مال شخص دیگری باشد یا کسی دارد شنود می‌کند. + ذخیرهٔ ضبط ممکن نبود + کیفیت پایین‌تر یعنی پرونده‌های کوچک‌تر + این یک نشانی XMPP است. لطفاً یک نام بدهید. + مرورگر + به دوربین دسترسی %1$s را بدهید + جستجوی کشورها + پیام‌ها + برقراری ارتباط امن ممکن نبود. + ویژگی پشتیبان‌گیری را برای کپی کردن برنامه (اجرای همزمان روی دستگاه‌های مختلف) به‌کار نبرید. بازیابی نسخهٔ پشتیان فقط برای مهاجرت به دستگاه تازه یا برای وقتی است که دستگاه قبلی را گم کرده‌اید. + نمایش اعلان‌های شناور + بلند + نوشتهٔ داخل عکس بالا را وارد کنید + یک رمز امن ساخته شده است + نشانی کامل XMPP شما این خواهد بود: %s + لطفاً کد ۶ رقمی خود را وارد کنید. + رویداد + پرونده‌ای که برگزیده‌اید یک نسخهٔ پشتیبان این برنامه نیست + باید از وب روی سرور ثبت‌نام کنید + پذیرفتن و ادامه + دستگاه شما شدیداً‌مصرف باتری را برای %1$s بهینه می‌کند که ممکن است باعث تأخیر در اعلان‌ها یا حتی ازدست‌رفتن پیام‌ها شود. +\nتوصیه می‌شود که آن را غیرفعال کنید. + کانال عمومی بسازید + برای گفتگوهای تازه OMEMO به کار برود. + رمزگذاری پیام‌ها + نام مخاطب + ضبط ویدیو + بازیابی پشتیبان ممکن نبود. + این شبیه یک نشانی دامنه است + دریافت ترجیحات آرشیوگیری. لطفاً صبر کنید… + پشتیبان‌گیری و بازیابی + این شمارهٔ پورت معتبر نیست + نشانی XMPP + من این پیوند را از منبع مورد اعتمادی پی گرفته‌ام + رد درخواست + نمایش به عنوان مشغول اگر گوشی بی‌صدا شده باشد + نقض احتمالی حریم خصوصی! + لطفاً شمارهٔ تلفن خود را وارد کنید. + نوشته با %s هم‌رسانی شد + لطفاً %s دیگر دوباره تلاش کنید + %1$s نیاز به دسترسی به فهرست مخاطبانتان دارد تا آن را با فهرست مخاطبان XMPP شما مقایسه کند. +\nبا این کار نام کامل و تصویر نمایهٔ مخاطبانتان نشان داده خواهد شد. +\n +\n%1$s فهرست مخاطبان شما را فقط به طور آفلاین مقایسه می‌کند، بی‌آن‌که چیزی را به سرور بفرستد. + اعتمادنشده + ‪xmpp.example.com‬ + لرزش معادل بی‌صدا + لطفاً کد ۶ رقمی را در زیر وارد کنید. + نشانی‌های XMPP را مدیران می‌بینند. + لطفاً یک نشانی XMPP بدهید + انتشار فعالیت + دریافت کلیدهای رمزگذاری ممکن نبود + پیام‌ها + تأییدیهٔ امنیتی سرور را هیچ مرکز تأیید شناخته‌شده‌ای امضا نکرده است. + پشتیبان + گشودن وبسایت + کوتاه + ادامه + گشودن با… + این نوع از اعلان‌ها برای اعلان‌هایی است که هیچ صدایی تولید نمی‌کنند. مثلاً برای وقتی که شما روی دستگاه دیگری فعالیت می‌کنید (زمان خاموشی). + بگذارید همه موضوع را ویرایش کنند + ترجیحات آرشیوگیری سمت سرور + این حساب XMPP را برای فرستادن هرزنامه گزارش کنید. + در حال درخواست پیامک… + نکته: اگر «فرستادن پرونده» را به جای «فرستادن تصویر» به کار ببرید، تصاویر شما همیشه فشرده‌نشده فرستاده خواهند شد. + تنظیمات بیشتر اتصال + شما عضو کانال موجودی شده‌اید + در صفحهٔ «آغاز گفتگو» صفحه‌کلید را باز کن و مکان‌نما را روی کادر جستجو بگذار + لطفاً نام خود را وارد کنید تا کسانی که شما را در فهرست مخاطبانشان ندارند بدانند که شما چه کسی هستید. + برنامه‌ای برای گشودن وبسایت نبود + من خودم حساب دارم + پیام‌های خصوصی فعال نیستند + من + واگردانی + ثابت‌کردن موقعیت مکانی + اجازه‌ای برای دسترسی به %s نیست + هم‌رسانی HTTP پرونده‌ها برای S3 + رمزگذاری OMEMO + پاسخ ناشناخته از سرور. + یافتن سرور ممکن نبود. + کیفیت ویدیو + به هر حال اضافه کن + فرستادن پیام ویرایش‌شده + خطای نشست + پیام کپی شد + گفتگوهای گروهی و کانال‌ها + اجرای Orbot + کد کشور نامعتبر + گفتگوی گروهی بسازید + ثبت نام شکست خورد: رمز زیادی ضعیف است + تلاش‌های بیش از حد + این برنامه به شما پیامکی می‌فرستد (ممکن است برایتان هزینه داشته باشد) تا شمارهٔ تلفن شما را تأیید کند. کد کشور و شمارهٔ تلفن خود را وارد کنید: + همگام‌سازی با مخاطبان + خطای اتصال + تأییدیهٔ امنیتی قابل خواندن نبود + پیش‌نویس: + %1$d حساب از %2$d تا وصل هستند + همیشه + سرور دیگر پاسخ نداد + اعلان‌ها متوقف شده‌اند + شما در حال نوشتن پیامی هستید. + این نوع از اعلان به طور دائمی نشان می‌دهد که %1$s در حال اجراست. + تماس‌ها + اتصال به سرور ممکن نبود. + رمزگشایی پیام OMEMO موفق نبود. + هیچ برنامه‌ای برای هم‌رسانی URI یافت نشد + این کانال نشانی XMPP شما را عمومی می‌کند + لطفاً صبر کنید (%s) + بخش برگزیده زیادی بزرگ است + وضعیت خود را هنگام نوشتن پیغام وضعیت خود تنظیم کنید. + مالکان می‌توانند موضوع را ویرایش کنند. + فرستادن دوبارهٔ پیامک + نشانی‌های XMPP را همه می‌بینند. + دیروز + دستگاه شما از مجموعهٔ تأییدیه‌های امنیتی پشتیبانی نمی‌کند! + جستجوی مستقیم + سرویس‌دهندهٔ خودم را به‌کار ببر + کامپیوتر + نمایش پیغام خطا + شما نسخهٔ قدیمی از این برنامه را به‌کار می‌برید. + + %d دقیقه + %d دقیقه + + تصویرها با %s هم‌رسانی شدند + پوشهٔ کش را (برای دوربین) خالی کن + نشانی سرور یا ‪.onion‬ + نام کاربری خود را برگزینید + غیرفعال + %s یک شماره تلفن معتبر نیست. + دستگاه شما از غیرفعال‌سازی بهینه‌سازی باتری پشتیبانی نمی‌کند + نام خود را وارد کنید + انجام این کنش ممکن نبود + ورود با تأییدیهٔ امنیتی + گشودن پشتیبان + لغوشده + غیرفعال + تنها وقتی نام‌برده شدم اعلان بده + تماسی بگیرید + اعتماد آسان‌گیرانه پیش از تأیید دستی + حذف کلیدهای انتخاب‌شده + خطای امنیتی: دسترسی نامعتبر به پرونده! + شمار تلاش‌های شما محدود شده است + لطفاً حسابی را فعال کنید + کلید OMEMO با تأییدیهٔ امنیتی تأیید شد! + اعلان‌ها از غریبه‌ها + ورودی نامعتبر از کاربر + گفتگوهای مربوطه بسته شده‌اند. + دستگاه شما شدیداً‌مصرف باتری را برای %1$s بهینه می‌کند که ممکن است باعث تأخیر در اعلان‌ها یا حتی ازدست‌رفتن پیام‌ها شود. +\n +\nهم‌اینک از شما خواسته می‌شود که آن را غیرفعال کنید. + ثبت نام شکست خورد: بعداً دوباره تلاش کنید + غایب + نام + افزودن صوت تازه؟ + جزئیات تأییدیهٔ امنیتی: + شمارهٔ تلفن خود را تأیید کنید + رمزگشایی دوباره + تنها مالک می‌تواند تصویر نمایهٔ گفتگوی عمومی را عوض کند + این نوع از اعلان وقتی نمایش می‌یابد که در اتصال یک حساب مشکلی وجود داشته باشد. + برای پیام‌های ورودی پس‌زمینهٔ سبز بگذار + تصویر نمایهٔ Conversations + پرونده با %s هم‌رسانی شد + تبلت + دیدن رسانه + به %1$s اجازهٔ دسترسی به میکروفن بده + پس‌زمینهٔ سبز + ارائهٔ نام اختیاری است + هم‌رسانی به شکل پیوند HTTP + این سرور مسئول این دامنه نیست + مشغول با گوشی بی‌صدا + اجازه + نمایش تنظیمات نام میزبان و پورت هنگام ساخت حساب + افزودن حساب فعلی + رمزگشایی از پشتیبان ممکن نبود. رمز درست است؟ + ما این شماره را تأیید خواهیم کرد

%s

. آیا درست است یا می‌خواهید شماره را ویرایش کنید؟
+ خطای ناشناخته در شبکه. + شما این حساب را غیرفعال کرده‌اید + اتصال + مخاطب می‌خواهد وضعیت وصل‌بودن شما را بداند + کاوش کانال‌ها یک سرویس خارجی به نام <a href=https://search.jabber.network>search.jabber.network</a> را به‌کار می‌برد.<br><br>به‌کاربردن این ویژگی نشانی IP و کلمات مورد جستجوی شما را به این سرویس می‌فرستد. برای اطلاعات بیشتر <a href=https://search.jabber.network/privacy>سیاست محرمانگی</a> آن‌ها را ببینید. + کدی که وارد کردید نادرست است. + شماره تلفن + خودکار + پیام‌های بی‌صدا + نمایش به عنوان مشغول اگر گوشی در حالت لرزش باشد + محرمانگی + + %d ماه + %d ماه + + افزونهٔ هم‌رسانی موقعیت مکانی + شما همهٔ کلیدهای OMEMO را که در اختیارتان است تأیید کرده‌اید + ساخت پروندهٔ موقتی ممکن نبود + اندازهٔ نسبی قلم به کار رفته در برنامه. + نکته: این مشکل را می‌توان گاهی با افزودن متقابل به فهرست مخاطبانتان حل کرد. + آیا می‌خواهید تأیید این دستگاه را بردارید؟ +\nاین دستگاه و پیغام‌ها از آن به عنوان اعتمادنشده علامت‌گذاری خواهند شد. + برداشتن موقعیت مکانی ثابت + برگزیدن اعضا + این شبیه نشانی یک کانال است + پیوست + پیغام خطا + نسخه‌های پشتیبان را که خودتان نساخته‌اید بازیابی نکنید! + اسکن‌کنندهٔ کد QR باید به دوربین دسترسی داشته باشد + ساخت میان‌بر + اتصال به OpenKeychain ممکن نبود + زنجیرهٔ تأییدیهٔ امنیتی غیرقابل‌اعتماد + نهفتن غیرفعال‌ها + کتاب الکترونیک + کد احتمالی خودبه‌خود جاگذاری شد. + مشکلات اتصال + پرونده به خاطر نقض امنیت نادیده گرفته شد. + هم‌رسانی پرونده‌های پشتیبان + تصویر نمایهٔ گفتگوی گروهی + روش کشف کانال‌ها + شما پیش از این به اثر انگشت دیجیتالی این شخص اعتماد کرده‌اید. با انتخاب «انجام» شما فقط تأیید می‌کنید که %s بخشی از این گفتگوی گروهی است. + الگوی رنگی را برگزینید + مالکان می‌توانند دیگران را دعوت کنند. + این گفتگوی گروهی حذف شده است + کلیدهای OMEMO را تأیید کن + میزبان از تصویر نمایهٔ گفتگوهای عمومی پشتیبانی نمی‌کند + لطفاً نامی به کانال بدهید + صرفه‌جویی در داده فعال است + ناکامل + اتصال شبکه موجود نیست. + درباره + ویرایش پیام + اهمیت، صدا، لرزش + رفتن به پایین + سرور نمی‌تواند به عنوان %s اعتبارسنجی شود. تأییدیهٔ امنیتی فقط برای این معتبر است: + فرستادن دوبارهٔ پیامک (%s) + نشانی XMPP با تأییدیهٔ امنیتی جور نیست + عضویت در کانال عمومی… + آماده برای گپ + بگذارید مخاطبانتان بدانند که چه وقتی با این برنامه کار می‌کنید + کدی که وارد کردید منقضی شده است. + زیاد (720p) + امروز + ویرایش پیغام وضعیت + تم + نام میزبان + کپی نشانی وب + به‌روزرسانی حساب ممکن نبود \ No newline at end of file From 85d582acf551f0be337f3806a1e2c02085a15c68 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 31 Dec 2023 23:35:10 +0000 Subject: [PATCH 013/556] Translated using Weblate (Persian) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/fa/ --- src/quicksy/res/values-fa/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/quicksy/res/values-fa/strings.xml b/src/quicksy/res/values-fa/strings.xml index a6b3daec9..b2faea67c 100644 --- a/src/quicksy/res/values-fa/strings.xml +++ b/src/quicksy/res/values-fa/strings.xml @@ -1,2 +1,12 @@ - \ No newline at end of file + + برای دریافت اعلان‌ها حتی وقتی که صفحه خاموش است باید این برنامه را به فهرست برنامه‌های محافظت‌شده بیفزایید. + با فرستادن گزارش خطا به توسعهٔ این برنامه کمک خواهید کرد + تأیید هویت سرور ممکن نیست. + خطای امنیتی ناشناخته. + پاسخی هنگام اتصال به سرور دریافت نشد. + این برنامه در کشور شما دردسترس نیست. + تصویر نمایهٔ Quicksy + مدت زمانی که این برنامه خاموش می‌ماند پس از این که فعالیت روی دستگاه دیگری کشف شد + بگذارید همهٔ مخاطبانتان بدانند چه وقتی این برنامه را به‌کار می‌برید + \ No newline at end of file From e7e174f4d733b5abc1618838ebeb4d7b0bb80a07 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 1 Jan 2024 17:21:22 +0000 Subject: [PATCH 014/556] Translated using Weblate (German) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index f3100e3f7..cfc29734b 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -91,12 +91,12 @@ \nAchtung: Dies löscht keine Kopien dieser Datei, die auf anderen Geräten oder Servern gespeichert sind.
Diese Unterhaltung danach beenden Gerät auswählen - Unverschlüsselt schreiben + Unverschlüsselt schreiben… Nachricht senden Nachricht an %s senden - OMEMO-verschlüsselt schreiben + OMEMO-verschlüsselt schreiben… v\\OMEMO-verschlüsselte Nachricht senden - OpenPGP-verschlüsselt schreiben + OpenPGP-verschlüsselt schreiben… Neuer Nickname wird jetzt verwendet Unverschlüsselt senden Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel. From 84fbd07fb22680307e31f76e42bce75178fed812 Mon Sep 17 00:00:00 2001 From: glut4 Date: Tue, 2 Jan 2024 22:03:56 +0000 Subject: [PATCH 015/556] Translated using Weblate (Dutch) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/nl/ --- src/conversations/res/values-nl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index f04a6b2de..bc7dbc2fa 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -11,4 +11,5 @@ Tik op de delen knop om een uitnodiging te versturen naar %1$s Als je contactpersoon in de buurt is, kan deze ook onderstaande code scannen om de uitnodiging te aanvaarden. Deel de uitnodiging met ... + Vergezel %1$s en chat met mij: %2$s \ No newline at end of file From ce645bbd41c497a4c678260d47016a8e5f4a3a17 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Thu, 11 Jan 2024 18:04:09 +0000 Subject: [PATCH 016/556] Translated using Weblate (Persian) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/fa/ --- src/quicksy/res/values-fa/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-fa/strings.xml b/src/quicksy/res/values-fa/strings.xml index b2faea67c..ac627b332 100644 --- a/src/quicksy/res/values-fa/strings.xml +++ b/src/quicksy/res/values-fa/strings.xml @@ -1,7 +1,7 @@ برای دریافت اعلان‌ها حتی وقتی که صفحه خاموش است باید این برنامه را به فهرست برنامه‌های محافظت‌شده بیفزایید. - با فرستادن گزارش خطا به توسعهٔ این برنامه کمک خواهید کرد + با فرستادن گزارش‌های خرابی به توسعهٔ این برنامه کمک خواهید کرد تأیید هویت سرور ممکن نیست. خطای امنیتی ناشناخته. پاسخی هنگام اتصال به سرور دریافت نشد. From dd0c82302b0b6987e80d553a4d9fda1b017e49b9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 15:36:00 +0100 Subject: [PATCH 017/556] version bump to 2.13.1-beta.2 and changelog --- CHANGELOG.md | 6 ++++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4208304.txt | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4208304.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9eb28bd..47cf9350f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.13.1 + +* Support P2P file transfer via WebRTC data channels +* Fix interoperability issues with Bind 2.0 on ejabberd +* Bundle Let’s Encrypt root certificates for Android <= 7 + ### Version 2.13.0 * Easier access to 'Show QR code' diff --git a/build.gradle b/build.gradle index b4281a6b9..c477d8453 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42081 - versionName "2.13.0" + versionCode 42083 + versionName "2.13.1-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4208304.txt b/fastlane/metadata/android/en-US/changelogs/4208304.txt new file mode 100644 index 000000000..b6a7bb98f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Support P2P file transfer via WebRTC data channels +* Fix interoperability issues with Bind 2.0 on ejabberd +* Bundle Let’s Encrypt root certificates for Android <= 7 From aa558f83f2719b82f972422cfd2c27c134fd3114 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 19:25:53 +0100 Subject: [PATCH 018/556] bump copyright year to 2024 --- src/main/res/values/about.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml index d6e064d57..b5c2ba960 100644 --- a/src/main/res/values/about.xml +++ b/src/main/res/values/about.xml @@ -31,7 +31,7 @@ Conversations • the very last word in instant messaging. - \n\nCopyright © 2014-2023 Daniel Gultsch + \n\nCopyright © 2014-2024 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or From 15a3c163eb74c47915ccba1047651f15053388e2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 10:40:32 +0100 Subject: [PATCH 019/556] respond OK to JFT session terminate --- .../xmpp/jingle/JingleFileTransferConnection.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 983ce433f..e7847cdd2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -556,6 +556,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void receiveSessionTerminate(final JinglePacket jinglePacket) { + respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final State previous = this.state; Log.d( @@ -745,16 +746,17 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void receiveTransportReplace( final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) { respondOk(jinglePacket); - final Transport transport; + // TODO kill current transport + final Transport nextTransport; try { - transport = setupTransport(transportInfo); + nextTransport = setupTransport(transportInfo); } catch (final RuntimeException e) { sendSessionTerminate(Reason.of(e), e.getMessage()); return; } - this.transport = transport; + this.transport = nextTransport; this.transport.setTransportCallback(this); - final var transportInfoFuture = transport.asTransportInfo(); + final var transportInfoFuture = nextTransport.asTransportInfo(); Futures.addCallback( transportInfoFuture, new FutureCallback<>() { From b2e1f9b3d8f14bf77e2384b3b2031579a67b0a1a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 11:43:23 +0100 Subject: [PATCH 020/556] call current transport when receiving transport-replace --- .../jingle/JingleFileTransferConnection.java | 27 ++++++++++++------- .../WebRTCDataChannelTransport.java | 9 ++++++- 2 files changed, 25 insertions(+), 11 deletions(-) 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 e7847cdd2..0f7247fdc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -212,7 +212,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection jinglePacket.setSecurity( Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage); } - Log.d(Config.LOGTAG, "--> " + jinglePacket.toString()); jinglePacket.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, @@ -235,7 +234,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void receiveSessionAccept(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, "receive session accept " + jinglePacket); + Log.d(Config.LOGTAG, "receive file transfer session accept"); if (isResponder()) { receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); return; @@ -450,7 +449,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection setLocalContentMap(contentMap); final var jinglePacket = contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); - Log.d(Config.LOGTAG, "--> " + jinglePacket.toString()); send(jinglePacket); // this needs to come after session-accept or else our candidate-error might arrive first this.transport.connect(); @@ -537,7 +535,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void receiveSessionInfo(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, "<-- " + jinglePacket); respondOk(jinglePacket); final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket); if (sessionInfo instanceof FileTransferDescription.Checksum checksum) { @@ -723,7 +720,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE); return; } - Log.d(Config.LOGTAG, "receive transport replace " + jinglePacket); final GenericTransportInfo transportInfo; try { transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo(); @@ -746,7 +742,16 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void receiveTransportReplace( final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) { respondOk(jinglePacket); - // TODO kill current transport + final Transport currentTransport = this.transport; + if (currentTransport != null) { + Log.d( + Config.LOGTAG, + "terminating " + + currentTransport.getClass().getSimpleName() + + " upon receiving transport-replace"); + currentTransport.setTransportCallback(null); + currentTransport.terminate(); + } final Transport nextTransport; try { nextTransport = setupTransport(transportInfo); @@ -755,6 +760,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection return; } this.transport = nextTransport; + Log.d( + Config.LOGTAG, + "replacing transport with " + nextTransport.getClass().getSimpleName()); this.transport.setTransportCallback(this); final var transportInfoFuture = nextTransport.asTransportInfo(); Futures.addCallback( @@ -781,7 +789,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection contentMap .transportInfo() .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId); - Log.d(Config.LOGTAG, "sending transport accept " + jinglePacket); send(jinglePacket); transport.connect(); } @@ -961,7 +968,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId); jinglePacket.addJingleChild(sessionInfo.asElement()); jinglePacket.setTo(this.id.with); - Log.d(Config.LOGTAG, "--> " + jinglePacket); send(jinglePacket); } @@ -984,6 +990,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection transport.terminate(); if (isInitiator()) { this.transport = setupLastResortTransport(); + Log.d( + Config.LOGTAG, + "replacing transport with " + this.transport.getClass().getSimpleName()); this.transport.setTransportCallback(this); final var transportInfoFuture = this.transport.asTransportInfo(); Futures.addCallback( @@ -1013,7 +1022,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection contentMap .transportInfo() .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId); - Log.d(Config.LOGTAG, "sending transport replace " + jinglePacket); send(jinglePacket); } @@ -1041,7 +1049,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, "--> " + jinglePacket); send(jinglePacket); } 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 0773610fb..74d9d91bf 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 @@ -174,7 +174,14 @@ public class WebRTCDataChannelTransport implements Transport { } private void onIceConnectionFailed() { - this.transportCallback.onTransportSetupFailed(); + final var callback = this.transportCallback; + if (callback == null) { + Log.d( + Config.LOGTAG, + "not calling onTransportSetupFailed(). Transport likely has been replaced"); + return; + } + callback.onTransportSetupFailed(); } private void setDataChannel(final DataChannel dataChannel) { From 4168bc466687194955c8f0b9949e6c1ac5b0d50b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 15:20:47 +0100 Subject: [PATCH 021/556] less noisy logcat. catch illegal state exception as result of race condition --- .../transports/WebRTCDataChannelTransport.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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 74d9d91bf..e4dd730d5 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 @@ -191,7 +191,6 @@ public class WebRTCDataChannelTransport implements Transport { new OnMessageObserver() { @Override public void onMessage(final DataChannel.Buffer buffer) { - Log.d(Config.LOGTAG, "onMessage() (the other one)"); try { WebRTCDataChannelTransport.this.writableByteChannel.write(buffer.data); } catch (final IOException e) { @@ -584,8 +583,7 @@ public class WebRTCDataChannelTransport implements Transport { Log.d(Config.LOGTAG, "DataChannelWriter reached EOF"); return; } - dataChannel.send( - new DataChannel.Buffer(ByteBuffer.wrap(buffer, 0, count), true)); + send(ByteBuffer.wrap(buffer, 0, count)); } } catch (final InterruptedException | InterruptedIOException e) { if (isSending.get()) { @@ -598,6 +596,16 @@ public class WebRTCDataChannelTransport implements Transport { } } + private void send(final ByteBuffer byteBuffer) throws IOException { + try { + dataChannel.send(new DataChannel.Buffer(byteBuffer, true)); + } catch (final IllegalStateException e) { + // dataChannel can be 'disposed' if we waited too long between `isSending` check and + // actually trying to send + throw new IOException(e); + } + } + public void close() { this.isSending.set(false); terminate(this.dataChannel); From a6ba65849476b36b50b0290b7fcbfb3ed37a4023 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 18:10:30 +0100 Subject: [PATCH 022/556] version bump to 2.13.1-beta.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c477d8453..b6acf28d6 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42083 - versionName "2.13.1-beta.2" + versionCode 42084 + versionName "2.13.1-beta.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 5ab8912cb4160b97b9ffac928e7d7763bfbbb604 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 4 Feb 2024 19:49:55 +0100 Subject: [PATCH 023/556] simplify loginInfo null check --- .../conversations/xmpp/XmppConnection.java | 244 +++++++++++------- 1 file changed, 146 insertions(+), 98 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 731513147..6d77ca800 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -22,47 +22,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.ConnectException; -import java.net.IDN; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -110,8 +69,50 @@ import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + import okhttp3.HttpUrl; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.ConnectException; +import java.net.IDN; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + public class XmppConnection implements Runnable { private static final int PACKET_IQ = 0; @@ -283,12 +284,14 @@ public class XmppConnection implements Runnable { mXmppConnectionService.resetSendingToWaiting(account); } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); - features.encryptionEnabled = false; + this.loginInfo = null; + this.features.encryptionEnabled = false; this.inSmacksSession = false; this.quickStartInProgress = false; this.isBound = false; this.attempt++; - this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified + this.verifiedHostname = + null; // will be set if user entered hostname is being used or hostname was verified // with dnssec try { Socket localSocket; @@ -370,12 +373,18 @@ public class XmppConnection implements Runnable { final StreamId streamId = this.streamId; final Resolver.Result resumeLocation = streamId == null ? null : streamId.location; if (resumeLocation != null) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected resume location on position 0"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": injected resume location on position 0"); results.add(0, resumeLocation); } final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult; if (seeOtherHost != null) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": injected see-other-host on position 0"); results.add(0, seeOtherHost); } for (final Iterator iterator = results.iterator(); @@ -585,13 +594,18 @@ public class XmppConnection implements Runnable { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(); + } else if (nextTag.isStart("failure", Namespace.TLS)) { + throw new StateChangingException(Account.State.TLS_ERROR); + } else if (account.isOptionSet(Account.OPTION_REGISTER) + && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { + processIq(nextTag); + } else if (!isSecure() || this.loginInfo == null) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (nextTag.isStart("success")) { final Element success = tagReader.readElement(nextTag); if (processSuccess(success)) { break; } - } else if (nextTag.isStart("failure", Namespace.TLS)) { - throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { final Element failure = tagReader.readElement(nextTag); processFailure(failure); @@ -599,16 +613,8 @@ public class XmppConnection implements Runnable { // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { - if (isSecure() && this.loginInfo != null) { - final Element challenge = tagReader.readElement(nextTag); - processChallenge(challenge); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received 'challenge on an unsecure connection"); - throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); - } + final Element challenge = tagReader.readElement(nextTag); + processChallenge(challenge); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); @@ -674,6 +680,8 @@ public class XmppConnection implements Runnable { processFailed(failed, true); } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { processIq(nextTag); + } else if (!isBound) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) { processMessage(nextTag); } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { @@ -709,7 +717,9 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - response.setContent(this.loginInfo.saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); + response.setContent( + this.loginInfo.saslMechanism.getResponse( + challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); @@ -804,7 +814,10 @@ public class XmppConnection implements Runnable { if (resumed != null && streamId != null) { if (this.boundStreamFeatures != null) { this.streamFeatures = this.boundStreamFeatures; - Log.d(Config.LOGTAG, "putting previous stream features back in place: " + XmlHelper.printElementNames(this.boundStreamFeatures)); + Log.d( + Config.LOGTAG, + "putting previous stream features back in place: " + + XmlHelper.printElementNames(this.boundStreamFeatures)); } processResumed(resumed); } else if (failed != null) { @@ -824,7 +837,7 @@ public class XmppConnection implements Runnable { processEnabled(streamManagementEnabled); waitForDisco = true; } else { - //if we did not enable stream management in bind do it now + // if we did not enable stream management in bind do it now waitForDisco = enableStreamManagement(); } final boolean negotiatedCarbons; @@ -856,13 +869,22 @@ public class XmppConnection implements Runnable { tokenMechanism = null; } if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) { - if (ChannelBinding.priority(tokenMechanism.channelBinding) >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) { + if (ChannelBinding.priority(tokenMechanism.channelBinding) + >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) { this.account.setFastToken(tokenMechanism, token); Log.d( Config.LOGTAG, - account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism); + account.getJid().asBareJid() + + ": storing hashed token " + + tokenMechanism); } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not accepting hashed token "+ tokenMechanism.name()+" for log in mechanism "+currentSaslMechanism.getMechanism()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": not accepting hashed token " + + tokenMechanism.name() + + " for log in mechanism " + + currentSaslMechanism.getMechanism()); this.account.resetFastToken(); } } else if (this.hashTokenRequest != null) { @@ -1015,7 +1037,9 @@ public class XmppConnection implements Runnable { } else { Log.d( Config.LOGTAG, - account.getJid().asBareJid() + ": stream management enabled. resume at: " + streamId.location); + account.getJid().asBareJid() + + ": stream management enabled. resume at: " + + streamId.location); } this.streamId = streamId; this.stanzasReceived = 0; @@ -1061,8 +1085,7 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas"); for (final AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket) { - MessagePacket message = (MessagePacket) packet; + if (packet instanceof MessagePacket message) { mXmppConnectionService.markMessage( account, message.getTo().asBareJid(), @@ -1143,20 +1166,13 @@ public class XmppConnection implements Runnable { private @NonNull Element processPacket(final Tag currentTag, final int packetType) throws IOException { - final Element element; - switch (packetType) { - case PACKET_IQ: - element = new IqPacket(); - break; - case PACKET_MESSAGE: - element = new MessagePacket(); - break; - case PACKET_PRESENCE: - element = new PresencePacket(); - break; - default: - throw new AssertionError("Should never encounter invalid type"); - } + final Element element = + switch (packetType) { + case PACKET_IQ -> new IqPacket(); + case PACKET_MESSAGE -> new MessagePacket(); + case PACKET_PRESENCE -> new PresencePacket(); + default -> throw new AssertionError("Should never encounter invalid type"); + }; element.setAttributes(currentTag.getAttributes()); Tag nextTag = tagReader.readTag(); if (nextTag == null) { @@ -1476,10 +1492,12 @@ public class XmppConnection implements Runnable { this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); final Collection channelBindings = ChannelBinding.of(cbElement); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); - final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); + final SaslMechanism saslMechanism = + factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); this.validate(saslMechanism, mechanisms); final boolean quickStartAvailable; - final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); + final String firstMessage = + saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); final boolean usingFast = SaslMechanism.hashedToken(saslMechanism); final Element authenticate; if (version == SaslMechanism.Version.SASL) { @@ -1488,7 +1506,7 @@ public class XmppConnection implements Runnable { authenticate.setContent(firstMessage); } quickStartAvailable = false; - this.loginInfo = new LoginInfo(saslMechanism,version,Collections.emptyList()); + this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList()); } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); @@ -1496,7 +1514,8 @@ public class XmppConnection implements Runnable { if (usingFast) { hashTokenRequest = null; } else { - final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST); + final Element fast = + inline == null ? null : inline.findChild("fast", Namespace.FAST); final Collection fastMechanisms = SaslMechanism.mechanisms(fast); hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); @@ -1517,9 +1536,11 @@ public class XmppConnection implements Runnable { return; } } - this.loginInfo = new LoginInfo(saslMechanism,version,bindFeatures); + this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures); this.hashTokenRequest = hashTokenRequest; - authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm); + authenticate = + generateAuthenticationRequest( + firstMessage, usingFast, hashTokenRequest, bindFeatures, sm); } else { throw new AssertionError("Missing implementation for " + version); } @@ -1547,7 +1568,9 @@ public class XmppConnection implements Runnable { return inline != null && inline.hasChild("fast", Namespace.FAST); } - private void validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { + private void validate( + final @Nullable SaslMechanism saslMechanism, Collection mechanisms) + throws StateChangingException { if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1574,8 +1597,10 @@ public class XmppConnection implements Runnable { } } - private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { - return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); + private Element generateAuthenticationRequest( + final String firstMessage, final boolean usingFast) { + return generateAuthenticationRequest( + firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( @@ -2225,10 +2250,18 @@ public class XmppConnection implements Runnable { final String seeOtherHost = streamError.findChildContent("see-other-host"); final Resolver.Result currentResolverResult = this.currentResolverResult; if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": stream error " + streamError); throw new StateChangingException(Account.State.STREAM_ERROR); } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": see other host: " + + seeOtherHost + + " " + + currentResolverResult); final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost); if (seeOtherResult != null) { this.seeOtherHostResolverResult = seeOtherResult; @@ -2262,7 +2295,8 @@ public class XmppConnection implements Runnable { final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; final SaslMechanism quickStartMechanism; if (secureConnection) { - quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); + quickStartMechanism = + SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); } else { quickStartMechanism = null; } @@ -2271,10 +2305,16 @@ public class XmppConnection implements Runnable { && quickStartMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.loginInfo = new LoginInfo(quickStartMechanism, SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES); + this.loginInfo = + new LoginInfo( + quickStartMechanism, + SaslMechanism.Version.SASL_2, + Bind2.QUICKSTART_FEATURES); final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = - generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); + generateAuthenticationRequest( + quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), + usingFast); authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); sendStartStream(true, false); synchronized (this.mStanzaQueue) { @@ -2377,7 +2417,13 @@ public class XmppConnection implements Runnable { ++stanzasSent; if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": counting outbound " + + packet.getName() + + " as #" + + stanzasSent); } this.mStanzaQueue.append(stanzasSent, stanza); if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { @@ -2560,7 +2606,7 @@ public class XmppConnection implements Runnable { public int getTimeToNextAttempt(final boolean aggressive) { final int interval; if (aggressive) { - interval = Math.min((int) (3 * Math.pow(1.3,attempt)), 60); + interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60); } else { final int additionalTime = account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; @@ -2791,7 +2837,8 @@ public class XmppConnection implements Runnable { public boolean sm() { return streamId != null || (connection.streamFeatures != null - && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)); + && connection.streamFeatures.hasChild( + "sm", Namespace.STREAM_MANAGEMENT)); } public boolean csi() { @@ -2912,7 +2959,8 @@ public class XmppConnection implements Runnable { } public boolean bookmarks2() { - return pepPublishOptions() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); + return pepPublishOptions() + && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); } public boolean externalServiceDiscovery() { From c2592d141730694e95fda76db9c52f23aa8b1882 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 5 Feb 2024 15:59:06 +0100 Subject: [PATCH 024/556] fixup: simplify loginInfo null check --- .gitignore | 1 + .../conversations/xmpp/XmppConnection.java | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index c689a5f59..e2091fa3d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ src/quicksyPlaystore/res/values/push.xml build/ captures/ signing.properties +signing.managed.properties # Ignore Gradle GUI config gradle-app.setting diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6d77ca800..38ad748fe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -615,12 +615,29 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); processChallenge(challenge); + } else if (this.streamId != null + && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { + final Element resumed = tagReader.readElement(nextTag); + processResumed(resumed); + } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) { + final Element failed = tagReader.readElement(nextTag); + processFailed(failed, true); + } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { + processIq(nextTag); + } else if (!isBound) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server sent unexpected" + + nextTag.identifier()); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) { + processMessage(nextTag); + } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { + processPresence(nextTag); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); - } else if (nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { - final Element resumed = tagReader.readElement(nextTag); - processResumed(resumed); } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { @@ -675,17 +692,6 @@ public class XmppConnection implements Runnable { if (acknowledgedMessages) { mXmppConnectionService.updateConversationUi(); } - } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) { - final Element failed = tagReader.readElement(nextTag); - processFailed(failed, true); - } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { - processIq(nextTag); - } else if (!isBound) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) { - processMessage(nextTag); - } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { - processPresence(nextTag); } else { Log.e( Config.LOGTAG, From ff082ab607c14b7e0742c5b8951730afa766006a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 6 Feb 2024 14:27:32 +0100 Subject: [PATCH 025/556] code clean up in IQ callback handling --- .../conversations/xmpp/XmppConnection.java | 94 +++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 38ad748fe..41c7b4527 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1232,52 +1232,68 @@ public class XmppConnection implements Runnable { + "'"); return; } - if (packet instanceof JinglePacket) { + if (packet instanceof JinglePacket jinglePacket && isBound) { if (this.jingleListener != null) { - this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet); + this.jingleListener.onJinglePacketReceived(account, jinglePacket); } } else { - OnIqPacketReceived callback = null; - synchronized (this.packetCallbacks) { - final Pair packetCallbackDuple = - packetCallbacks.get(packet.getId()); - if (packetCallbackDuple != null) { - // Packets to the server should have responses from the server - if (packetCallbackDuple.first.toServer(account)) { - if (packet.fromServer(account)) { - callback = packetCallbackDuple.second; - packetCallbacks.remove(packet.getId()); - } else { - Log.e( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": ignoring spoofed iq packet"); - } - } else { - if (packet.getFrom() != null - && packet.getFrom().equals(packetCallbackDuple.first.getTo())) { - callback = packetCallbackDuple.second; - packetCallbacks.remove(packet.getId()); - } else { - Log.e( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": ignoring spoofed iq packet"); - } - } - } else if (packet.getType() == IqPacket.TYPE.GET - || packet.getType() == IqPacket.TYPE.SET) { - callback = this.unregisteredIqListener; - } + final OnIqPacketReceived callback = getIqPacketReceivedCallback(packet); + if (callback == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": no callback registered for IQ from " + + packet.getFrom()); + return; } - if (callback != null) { - try { - callback.onIqPacketReceived(account, packet); - } catch (StateChangingError error) { - throw new StateChangingException(error.state); + try { + callback.onIqPacketReceived(account, packet); + } catch (final StateChangingError error) { + throw new StateChangingException(error.state); + } + } + } + + private OnIqPacketReceived getIqPacketReceivedCallback(final IqPacket stanza) + throws StateChangingException { + final boolean isRequest = + stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET; + if (isRequest) { + if (isBound) { + return this.unregisteredIqListener; + } else { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } else { + synchronized (this.packetCallbacks) { + final var pair = packetCallbacks.get(stanza.getId()); + if (pair == null) { + return null; + } + if (pair.first.toServer(account)) { + if (stanza.fromServer(account)) { + packetCallbacks.remove(stanza.getId()); + return pair.second; + } else { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); + } + } else { + if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) { + packetCallbacks.remove(stanza.getId()); + return pair.second; + } else { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": ignoring spoofed iq packet"); + } } } } + return null; } private void processMessage(final Tag currentTag) throws IOException { From f80e11de926c4d93531da82222b709cbfff5c421 Mon Sep 17 00:00:00 2001 From: Application-Maker Date: Sun, 21 Jan 2024 08:35:25 +0000 Subject: [PATCH 026/556] Translated using Weblate (Russian) Currently translated at 99.8% (978 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 3667ef306..a880096ed 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -55,7 +55,7 @@ Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены. Создать новую учётную запись на сервере Изменить пароль на сервере - Поделиться с + Поделиться с… Начать беседу Пригласить контакт Пригласить @@ -88,7 +88,9 @@ Очистить историю Вы хотите удалить все сообщения в этой беседе?\n\nВнимание: Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах. Удалить файл - Вы уверены, что хотите удалить этот файл?\n\nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. + Вы уверены, что хотите удалить этот файл? +\n +\nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. Закрыть эту беседу Выберите устройство Нешифрованное сообщение @@ -97,7 +99,7 @@ OMEMO-зашифр. сообщение v\\OMEMO-зашифр. сообщение OpenPGP зашифр. сообщение - Имя уже используется + Используется новое имя Отправить в незашифрованном виде Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа. Установите OpenKeychain @@ -128,7 +130,7 @@ Время, на которое уведомления будут отключены, когда вы пользуетесь аккаунтом на другом устройстве. Дополнительно Не отправлять отчёты об ошибках - Отправляя отчеты об ошибках, вы помогаете разработке этого приложения + Отправляя отчёты об ошибках, вы помогаете разработке Quicksy Отчёты о получении Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения Запретить скриншоты @@ -523,7 +525,9 @@ Только большие изображения Оптимизации энергопотребления разрешены Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.\nРекомендуем её отключить. - Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение её отключить. + Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений. +\n +\nСейчас появится предложение её отключить. Запретить Выбранная область слишком большая (Нет активированных учётных записей) @@ -532,7 +536,7 @@ Отправить исправленное сообщение Вы уже пометили отпечаток этого человека как доверенный. Выбрав \"Готово\", вы только подтвердите, что %s является участником конференции. Вы отключили эту учётную запись - Ошибка безопасности: недействительный доступ к файлу + Ошибка безопасности: недействительный доступ к файлу! Не найдено приложения для передачи URI Отправить URI… Согласиться и продолжить @@ -543,7 +547,7 @@ Использовать свой провайдер Выберите имя пользователя Управлять доступностью вручную - Устанавливать свою доступность при редактировании статусного сообщения + Устанавливать свою доступность при редактировании статусного сообщения. Статусное собщение Свободен для общения В сети @@ -615,7 +619,7 @@ Некорректный 2D штрихкод Очистить кэш (используется камерой) Очистить кэш - Очистить приватное хранилище. + Очистить приватное хранилище Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера) Открывать ссылки из надёжного источника Вы подтверждаете OMEMO-ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$s мог разместить эту ссылку. @@ -623,7 +627,8 @@ Показывать неактивные Скрыть неактивные Прекратить доверять устройству - Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные. + Вы действительно хотите удалить устройство из доверенных? +\nЭто устройство и сообщения, полученные с него, будут помечаться как недоверенные. %d секунда %d секунды @@ -666,7 +671,7 @@ Не загружаем сообщения, в соответствии с локальным сроком хранения. Сжимание видео Соответствующие беседы закрыты. - Контакт заблокирован + Контакт заблокирован. Уведомления от неизвестных контактов Уведомлять о сообщениях и звонках от незнакомых контактов. Получено сообщение от неизвестного контакта @@ -827,7 +832,7 @@ Пожалуйста, попробуйте еще раз через %s У вас есть ограничение скорости Слишком много попыток - Вы используете устаревшую версию приложения + Вы используете устаревшую версию этого приложения. Обновить Этот номер телефона в данный момент авторизирован на другом устройстве. Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы. @@ -865,7 +870,7 @@ Этот канал уже существует Вы присоединились к существующему каналу Не удалось сохранить настройки канала - Разрешить всем редактировать тему. + Разрешить всем редактировать тему Разрешить всем приглашать других Кто угодно может редактировать тему. Владельцы могут редактировать тему. @@ -993,7 +998,7 @@ Переподключение Адрес XMPP не найден Учётная запись XMPP - Вы покинули эту беседу из-за технических причин + Вы покинули данный групповой чат по техническим причинам Добавить дополнительные треки\? Переподключение к звонку Переподключение к видеовызову @@ -1010,7 +1015,7 @@ Синхронизировать закладки Устанавливать флаг \"автоприсоединение\" при входе в- и выходе из MUC, и реагировать на изменения от других клиентов. Поиск по групповым беседам - Загрузка провалена: неверный файл + Загрузка неудачна: Неизвестный файл Перейти на видеовызов\? Исходящий вызов (%s) · %s Входящий вызов (%s) · %s From 37194c71a88bbf36abdf4924c0c24b20fb8c2a27 Mon Sep 17 00:00:00 2001 From: Application-Maker Date: Sun, 21 Jan 2024 08:43:04 +0000 Subject: [PATCH 027/556] Translated using Weblate (Russian) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/ru/ --- src/quicksy/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-ru/strings.xml b/src/quicksy/res/values-ru/strings.xml index c24ce3973..698c3bd29 100644 --- a/src/quicksy/res/values-ru/strings.xml +++ b/src/quicksy/res/values-ru/strings.xml @@ -4,7 +4,7 @@ Отправляя отчёты об ошибках, вы помогаете в разработке Quicksy Извещать собеседников, когда вы пользуетесь Quicksy Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Quicksy в список защищенных приложений. - Аватар профиля Quicksy + Аватар Quicksy Quicksy недоступен в Вашем регионе. Не удалось подтвердить сервер. Неизвестная ошибка безопасности. From e857948500970a144dcc93d859e08936a3e875c3 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Mon, 22 Jan 2024 14:09:47 +0000 Subject: [PATCH 028/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index fcb7ce14e..bffea05e1 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -554,7 +554,7 @@ 此字段是必需的 更正消息 发送更正后的消息 - 您已经验证了此用户的指纹。选择“完成”确认 %s 是此群聊的一员。 + 您已信任此人的指纹。选择“完成”即表示您确认 %s 是此群聊的一员。 您已禁用了此账号 安全错误:文件访问无效! 未找到可以分享 URI 的应用 From 371dfed57c26535a796a29b73c0f969fb8d0388d Mon Sep 17 00:00:00 2001 From: ghose Date: Mon, 22 Jan 2024 06:42:47 +0000 Subject: [PATCH 029/556] Translated using Weblate (Galician) Currently translated at 48.1% (26 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4208304.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4208304.txt b/fastlane/metadata/android/gl-ES/changelogs/4208304.txt new file mode 100644 index 000000000..03aa6966c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Soporte para a transferencia de ficheiros P2P a través de canles de datos WebRTC +* Arranxo dos problemas de interoperabilidade con Bind 2.0 en ejabberd +* Paquete de certificados raiz Let's Encrypt para Android <=7 From b18d0f2351e4ea01361cf1919cfe961f48d71554 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Sun, 21 Jan 2024 17:48:37 +0000 Subject: [PATCH 030/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (54 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4208304.txt diff --git a/fastlane/metadata/android/uk/changelogs/4208304.txt b/fastlane/metadata/android/uk/changelogs/4208304.txt new file mode 100644 index 000000000..6aa707982 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Підтримка передачі файлів P2P через канали даних WebRTC +* Виправлено проблеми сумісності з Bind 2.0 на ejabberd +* Пакет кореневих сертифікатів Let's Encrypt для версій Android до 7-ї включно From 20f096aab8dc97bceb01d736ee963488ae450497 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Mon, 22 Jan 2024 14:05:54 +0000 Subject: [PATCH 031/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/zh_Hans/ --- .../fastlane/metadata/android/zh-CN/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt index 085d21191..14b7bc66b 100644 --- a/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt +++ b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt @@ -28,7 +28,7 @@ Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展 到目前为止,这些 XEP 是: -* XEP-0065:SOCKS5 字节流(or mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。 +* XEP-0065:SOCKS5 字节流(或 mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。 * XEP-0163:个人事件协议(头像) * XEP-0191:屏蔽命令可让您将垃圾消息发送者列入黑名单或屏蔽的联系人中,而不会将其从花名册中删除。 * XEP-0198:流管理允许 XMPP 在小规模网络中断和底层 TCP 连接发生变化时继续运行。 From 887ec3b33c3cec8790307ff7ea77e0cc8ee1451f Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Mon, 22 Jan 2024 14:03:43 +0000 Subject: [PATCH 032/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (54 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4208304.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208304.txt b/fastlane/metadata/android/zh-CN/changelogs/4208304.txt new file mode 100644 index 000000000..b35be34f5 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* 支持通过 WebRTC 数据通道进行 P2P 文件传输 +* 修复 ejabberd 上 Bind 2.0 的互操作性问题 +* 捆绑适用于 Android <= 7 的 Let’s Encrypt 根证书 From d223b4c65bdf27d01ac051df7c4176c9bd68de75 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 24 Jan 2024 13:56:29 +0000 Subject: [PATCH 033/556] Translated using Weblate (Spanish) Currently translated at 100.0% (54 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4208304.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4208304.txt b/fastlane/metadata/android/es-ES/changelogs/4208304.txt new file mode 100644 index 000000000..0a3ef27b1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Admite transferencia de archivos P2P a través del canal de datos WebRTC +* Solucionar problemas de interoperabilidad con Bind 2.0 en ejabberd +* Paquetes de certificado raíz Let's Encrypt para Android <= 7 From 4fbfc2267060cde48d15d095583acd128988ae13 Mon Sep 17 00:00:00 2001 From: NLBRT Date: Sat, 27 Jan 2024 06:43:05 +0000 Subject: [PATCH 034/556] Translated using Weblate (Bengali (India)) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/bn_IN/ --- src/conversations/res/values-bn-rIN/strings.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/conversations/res/values-bn-rIN/strings.xml b/src/conversations/res/values-bn-rIN/strings.xml index 382343a37..b5fceb87a 100644 --- a/src/conversations/res/values-bn-rIN/strings.xml +++ b/src/conversations/res/values-bn-rIN/strings.xml @@ -1,10 +1,12 @@ - XMPP সার্ভার নির্বাচন করুন - conversations.im-ই ব্যবহার করা যাক - নতুন অ্যকাউন্ট তৈরী করা যাক - আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়। - XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী। + আপনার XMPP প্রোভাইডার নির্বাচন করুন + conversations.im ব্যবহার করুন + নতুন অ্যকাউন্ট তৈরী করুন + আপনার কি কোনও XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। যদি না করে থাকেন, তাহলে আপনি এখন একটি XMPP অ্যাকাউন্ট বানাতে পারেন। +\nসূত্র: মাঝে মধ্যে কিছু ইমেল প্রোভাইডাররা XMPP অ্যাকাউন্ট দেয়। + XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন। +\nআপনার সুবিধার্থে conversations.im-এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া সুবিধাজনক করা হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী। আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। আপনার নিমন্ত্রণপত্র, সার্ভার থেকে From 3ffa952d1f79e981bacf864bcb2b5cdba6df9965 Mon Sep 17 00:00:00 2001 From: random_r Date: Thu, 1 Feb 2024 10:15:28 +0000 Subject: [PATCH 035/556] Translated using Weblate (Italian) Currently translated at 100.0% (54 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/4208304.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4208304.txt b/fastlane/metadata/android/it-IT/changelogs/4208304.txt new file mode 100644 index 000000000..7139e8caa --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Supporto per trasferimenti P2P di file via canali di dati WebRTC +* Corretti problemi di interoperabilità con Bind 2.0 su ejabberd +* Integra certificati root di Let’s Encrypt su Android <= 7 From 38d00907823798bdb130b031343fcfe4029fa6ea Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 5 Feb 2024 08:44:55 +0000 Subject: [PATCH 036/556] Translated using Weblate (German) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index cfc29734b..42ef452c0 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -596,7 +596,7 @@ Erzeuge neue OMEMO-Schlüssel. Alle deine Kontakte müssen sie erneut verifizieren. Verwende dies nur als letztes Mittel. Ausgewählte Schlüssel löschen Du musst verbunden sein, um deinen Profilbild zu veröffentlichen. - Zeige Fehlermeldung + Fehlermeldung anzeigen Fehlermeldung Datensparmodus aktiv Dein Betriebssystem verhindert, dass %1$s im Hintergrund auf das Internet zugreift. Um Benachrichtigungen erhalten zu können, solltest du %1$s den Zugang erlauben, wenn der Datensparmodus aktiv ist.\n%1$s wird dennoch versuchen, so viele Daten wie möglich einzusparen. @@ -1021,5 +1021,5 @@ Abgemeldet Du verwendest nicht verifizierte Geräte. Scanne den 2D-Barcode auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. Spam melden - Spam melden und Spammer blockieren + Spam melden und Spammer sperren \ No newline at end of file From 600d6f6fc9b3460f1658ffa9e756e61bc109b03d Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 5 Feb 2024 08:40:46 +0000 Subject: [PATCH 037/556] Translated using Weblate (German) Currently translated at 100.0% (54 of 54 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4208304.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4208304.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4208304.txt b/fastlane/metadata/android/de-DE/changelogs/4208304.txt new file mode 100644 index 000000000..b3e0925f4 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4208304.txt @@ -0,0 +1,3 @@ +* Unterstützung von P2P-Dateiübertragung über WebRTC-Datenkanäle +* Behebung von Interoperabilitätsproblemen mit Bind 2.0 auf ejabberd +* Bündelung von Let's Encrypt Root-Zertifikaten für Android <= 7 From a63b419e58bac7c7b4c1ae1c71215af477a71a37 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 07:33:26 +0100 Subject: [PATCH 038/556] version bump to 2.13.1 (and rename changelogs) --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/en-US/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/es-ES/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/gl-ES/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/it-IT/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/uk/changelogs/{4208304.txt => 4208804.txt} | 0 .../android/zh-CN/changelogs/{4208304.txt => 4208804.txt} | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/es-ES/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/gl-ES/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/it-IT/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4208304.txt => 4208804.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4208304.txt => 4208804.txt} (100%) diff --git a/build.gradle b/build.gradle index b6acf28d6..ecb5321d6 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42084 - versionName "2.13.1-beta.3" + versionCode 42088 + versionName "2.13.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4208304.txt b/fastlane/metadata/android/de-DE/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4208304.txt rename to fastlane/metadata/android/de-DE/changelogs/4208804.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4208304.txt b/fastlane/metadata/android/en-US/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4208304.txt rename to fastlane/metadata/android/en-US/changelogs/4208804.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4208304.txt b/fastlane/metadata/android/es-ES/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/es-ES/changelogs/4208304.txt rename to fastlane/metadata/android/es-ES/changelogs/4208804.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4208304.txt b/fastlane/metadata/android/gl-ES/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/gl-ES/changelogs/4208304.txt rename to fastlane/metadata/android/gl-ES/changelogs/4208804.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4208304.txt b/fastlane/metadata/android/it-IT/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/it-IT/changelogs/4208304.txt rename to fastlane/metadata/android/it-IT/changelogs/4208804.txt diff --git a/fastlane/metadata/android/uk/changelogs/4208304.txt b/fastlane/metadata/android/uk/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4208304.txt rename to fastlane/metadata/android/uk/changelogs/4208804.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208304.txt b/fastlane/metadata/android/zh-CN/changelogs/4208804.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4208304.txt rename to fastlane/metadata/android/zh-CN/changelogs/4208804.txt From 961a024aa22d70a57c27bfd4acd958d96d7ecd22 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 23 Jan 2024 13:38:13 -0500 Subject: [PATCH 039/556] XEP says thread means not the topic Note: A message with a and a or a and a is a legitimate message, but it SHALL NOT be interpreted as a subject change. --- src/main/java/eu/siacs/conversations/parser/MessageParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 97910f3bb..bf3cba178 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -790,7 +790,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } if (isTypeGroupChat) { - if (packet.hasChild("subject")) { //TODO usually we would want to check for lack of body; however some servers do set a body :( + if (packet.hasChild("subject") && !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"); From 7455e99761bc4db96840a17eacb75a1e0210c607 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 09:15:36 +0100 Subject: [PATCH 040/556] remove unnecessary resume call --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 41c7b4527..25f210b12 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1453,6 +1453,8 @@ public class XmppConnection implements Runnable { && isSecure) { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) + && isSecure + && loginInfo != null && streamId != null && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { @@ -1467,7 +1469,9 @@ public class XmppConnection implements Runnable { this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { - if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) { + if (this.streamFeatures.hasChild("bind", Namespace.BIND) + && isSecure + && loginInfo != null) { sendBindRequest(); } else { Log.d( @@ -1478,7 +1482,6 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } } else { - Log.d( Config.LOGTAG, account.getJid().asBareJid() From 7fe8be1adccf66bed4c2ba5c3245927b6128d60c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 10:07:53 +0100 Subject: [PATCH 041/556] better work around for not processing race condition stanza --- .../conversations/xmpp/XmppConnection.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 25f210b12..9097a1256 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1232,6 +1232,12 @@ public class XmppConnection implements Runnable { + "'"); return; } + if (Thread.currentThread().isInterrupted()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + "Not processing iq. Thread was interrupted"); + return; + } if (packet instanceof JinglePacket jinglePacket && isBound) { if (this.jingleListener != null) { this.jingleListener.onJinglePacketReceived(account, jinglePacket); @@ -1308,11 +1314,18 @@ public class XmppConnection implements Runnable { + "'"); return; } + if (Thread.currentThread().isInterrupted()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + "Not processing message. Thread was interrupted"); + return; + } this.messageListener.onMessagePacketReceived(account, packet); } private void processPresence(final Tag currentTag) throws IOException { - PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); + final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); if (!packet.valid()) { Log.e( Config.LOGTAG, @@ -1323,6 +1336,13 @@ public class XmppConnection implements Runnable { + "'"); return; } + if (Thread.currentThread().isInterrupted()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + "Not processing presence. Thread was interrupted"); + return; + } this.presenceListener.onPresencePacketReceived(account, packet); } From 01ac2912f5ce46a78d2e8ebeda2dc64ca7e66b4b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 10:55:01 +0100 Subject: [PATCH 042/556] catch exception when hardware renderer can not be set up --- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index ddd5fbe8f..5b5c82bae 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -13,6 +13,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.opengl.GLException; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -836,8 +837,12 @@ public class RtpSessionActivity extends XmppActivity surfaceViewRenderer.setVisibility(View.VISIBLE); try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); - } catch (final IllegalStateException e) { - // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + } catch (final IllegalStateException ignored) { + // SurfaceViewRenderer was already initialized + } catch (final RuntimeException e) { + if (Throwables.getRootCause(e) instanceof GLException glException) { + Log.w(Config.LOGTAG, "could not set up hardware renderer", glException); + } } surfaceViewRenderer.setEnableHardwareScaler(true); } From 3882ea669d19a7fd8a6ad62bc80615368043b1d3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 10:55:33 +0100 Subject: [PATCH 043/556] check server from on stream open --- .../siacs/conversations/xmpp/XmppConnection.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 9097a1256..69fad55e8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -526,8 +526,7 @@ public class XmppConnection implements Runnable { tagReader.setInputStream(socket.getInputStream()); tagWriter.beginDocument(); final boolean quickStart; - if (socket instanceof SSLSocket) { - final SSLSocket sslSocket = (SSLSocket) socket; + if (socket instanceof SSLSocket sslSocket) { SSLSockets.log(account, sslSocket); quickStart = establishStream(SSLSockets.version(sslSocket)); } else { @@ -537,7 +536,16 @@ public class XmppConnection implements Runnable { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS); + if (tag == null) { + return false; + } + final boolean success = tag.isStart("stream", Namespace.STREAMS); + if (success) { + final var from = tag.getAttribute("from"); + if (from == null || !from.equals(account.getServer())) { + throw new StateChangingException(Account.State.HOST_UNKNOWN); + } + } if (success && quickStart) { this.quickStartInProgress = true; } From 5aff7d023cb9ccc4f829a48b9d8ccf48a8929bf1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 8 Feb 2024 11:06:07 +0100 Subject: [PATCH 044/556] slight modifications in quicksy onboard flow --- .../AbstractQuickConversationsService.java | 4 ++ src/main/res/values/strings.xml | 4 +- .../conversations/ui/EnterNameActivity.java | 46 +++++++++++-------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index 5d6f8eee5..ea883683a 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java @@ -30,6 +30,10 @@ public abstract class AbstractQuickConversationsService { return "playstore".equals(BuildConfig.FLAVOR_distribution); } + public static boolean isQuicksyPlayStore() { + return isQuicksy() && isPlayStoreFlavor(); + } + public abstract void signalAccountStateChange(); public abstract boolean isSynchronizing(); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 51e4c3c66..687b0d7c1 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -519,7 +519,7 @@ Grant %1$s access to the camera Synchronize with contacts %1$s wants permission to access your address book to match it with your XMPP contact list.\nThis will display your contacts’ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server. -
By continuing you agree to our Privacy Policy and our Terms & Conditions.

You will now be asked to grant permission to access your contacts.]]>
+
By continuing you agree to our Privacy Policy and our Terms & Conditions.

You will now be asked to grant permission to access your contacts.]]>
Notify on all messages Notify only when mentioned Notifications disabled @@ -545,7 +545,7 @@ Share URI with… Join the Conversation Have some Quick Conversations -
You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.
Quicksy stores your contacts’ phone numbers to make suggestions about possible contacts who are already on Quicksy.
By signing up you agree to our Privacy Policy and our Terms & Conditions.]]>
+
You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.
Quicksy uploads and processes your contact list to make suggestions about possible contacts who are already on Quicksy.
By signing up you agree to our Privacy Policy and our Terms & Conditions.]]>
Agree and continue A guide is set up for account creation on conversations.im.\nWhen picking conversations.im as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. Your full XMPP address will be: %s diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java index 30acdd2ed..d574f9010 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java @@ -1,20 +1,23 @@ package eu.siacs.conversations.ui; import android.content.Intent; -import androidx.databinding.DataBindingUtil; import android.os.Bundle; -import androidx.appcompat.widget.Toolbar; import android.view.View; -import java.util.concurrent.atomic.AtomicBoolean; +import androidx.appcompat.widget.Toolbar; +import androidx.databinding.DataBindingUtil; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityEnterNameBinding; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.AbstractQuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.AccountUtils; -public class EnterNameActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate { +import java.util.concurrent.atomic.AtomicBoolean; + +public class EnterNameActivity extends XmppActivity + implements XmppConnectionService.OnAccountUpdate { private ActivityEnterNameBinding binding; @@ -28,23 +31,28 @@ public class EnterNameActivity extends XmppActivity implements XmppConnectionSer this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_name); setSupportActionBar((Toolbar) this.binding.toolbar); this.binding.next.setOnClickListener(this::next); - this.setNick.set(savedInstanceState != null && savedInstanceState.getBoolean("set_nick",false)); + this.setNick.set( + savedInstanceState != null && savedInstanceState.getBoolean("set_nick", false)); } - private void next(View view) { - if (account != null) { - - String name = this.binding.name.getText().toString().trim(); - - account.setDisplayName(name); - - xmppConnectionService.publishDisplayName(account); - - Intent intent = new Intent(this, PublishProfilePictureActivity.class); - intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - startActivity(intent); + private void next(final View view) { + if (account == null) { + return; } + final String name = this.binding.name.getText().toString().trim(); + account.setDisplayName(name); + xmppConnectionService.publishDisplayName(account); + final Intent intent; + if (AbstractQuickConversationsService.isQuicksyPlayStore()) { + intent = new Intent(getApplicationContext(), StartConversationActivity.class); + intent.putExtra("init", true); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + } else { + intent = new Intent(this, PublishProfilePictureActivity.class); + intent.putExtra("setup", true); + } + intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + startActivity(intent); finish(); } @@ -66,7 +74,7 @@ public class EnterNameActivity extends XmppActivity implements XmppConnectionSer } private void checkSuggestPreviousNick() { - String displayName = this.account == null ? null : this.account.getDisplayName(); + final String displayName = this.account == null ? null : this.account.getDisplayName(); if (displayName != null) { if (setNick.compareAndSet(false, true) && this.binding.name.getText().length() == 0) { this.binding.name.getText().append(displayName); From b08466d6888cf8f3fd710f62fdda45ebe953b84e Mon Sep 17 00:00:00 2001 From: Licaon_Kter Date: Thu, 8 Feb 2024 10:36:44 +0000 Subject: [PATCH 045/556] No one knows what a "2D Barcode" is "stolen" from https://git.singpolyma.net/cheogram-android/commit/e9ce8ebbaf57067b71f11467312975b62c04cca7 --- src/main/res/values/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 687b0d7c1..bbdd23986 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -320,8 +320,8 @@ Copied XMPP address to clipboard Copied error message to clipboard web address - Scan 2D Barcode - Show 2D Barcode + Scan QR Code + Show QR Code Show block list Account details Confirm @@ -623,7 +623,7 @@ Trust new devices from non-verified contacts, but prompt manual confirmation of new devices for verified contacts. Blindly trusted OMEMO keys, meaning they could be someone else or someone could have tapped in. Untrusted - Invalid 2D barcode + Invalid QR Code Clean cache folder (used by camera app) Clean cache Clean private storage @@ -1018,8 +1018,8 @@ Hide notification Log out Log in - Your contact uses unverified devices. Scan their 2D barcode to perform verification and impede active MITM attacks. - You are using unverified devices. Scan the 2D barcode on your other devices to perform verification and impede active MITM attacks. + Your contact uses unverified devices. Scan their QR Code to perform verification and impede active MITM attacks. + You are using unverified devices. Scan the QR Code on your other devices to perform verification and impede active MITM attacks. Report spam Report spam and block spammer
From 0f34444b994b3e7096bc4bb2ddf736ba09deea2b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 8 Feb 2024 11:47:25 +0100 Subject: [PATCH 046/556] add next fastlane changelog (to be translated) --- fastlane/metadata/android/en-US/changelogs/4208904.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/4208904.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4208904.txt b/fastlane/metadata/android/en-US/changelogs/4208904.txt new file mode 100644 index 000000000..b062bedcb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4208904.txt @@ -0,0 +1,2 @@ +* minor bug fixes +* slight modifications in Quicksy onboard flow From 03dc15158b47ea0ed00da0964826ee80dba77412 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Feb 2024 11:23:05 +0100 Subject: [PATCH 047/556] use modern Java in XmppConnection --- .../conversations/xmpp/XmppConnection.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 69fad55e8..ef065c59d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1162,8 +1162,7 @@ public class XmppConnection implements Runnable { + mStanzaQueue.keyAt(i)); } final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket && acknowledgedListener != null) { - final MessagePacket packet = (MessagePacket) stanza; + if (stanza instanceof MessagePacket packet && acknowledgedListener != null) { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { @@ -1846,8 +1845,10 @@ public class XmppConnection implements Runnable { resetAttemptCount(true); resetStreamId(); clearIqCallbacks(); - this.stanzasSent = 0; - mStanzaQueue.clear(); + synchronized (this.mStanzaQueue) { + this.stanzasSent = 0; + this.mStanzaQueue.clear(); + } this.redirectionUrl = null; synchronized (this.disco) { disco.clear(); @@ -2332,8 +2333,7 @@ public class XmppConnection implements Runnable { synchronized (this.mStanzaQueue) { for (int i = 0; i < mStanzaQueue.size(); ++i) { final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket) { - final MessagePacket packet = (MessagePacket) stanza; + if (stanza instanceof MessagePacket packet) { final String id = packet.getId(); final Jid to = packet.getTo(); mXmppConnectionService.markMessage( @@ -2458,9 +2458,7 @@ public class XmppConnection implements Runnable { + " do not write stanza to unbound stream " + packet.toString()); } - if (packet instanceof AbstractAcknowledgeableStanza) { - AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet; - + if (packet instanceof AbstractAcknowledgeableStanza stanza) { if (this.mStanzaQueue.size() != 0) { int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1); if (currentHighestKey != stanzasSent) { @@ -2860,11 +2858,6 @@ public class XmppConnection implements Runnable { && pepPublishOptions(); } - public boolean avatarConversion() { - return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION) - && pepPublishOptions(); - } - public boolean blocking() { return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING); } From b93c5622dfa7ad7b438a5fbd46ead7e1702edb81 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Feb 2024 11:25:00 +0100 Subject: [PATCH 048/556] modify quicksy user consent wording once more --- .../siacs/conversations/ui/StartConversationActivity.java | 3 ++- src/main/res/values/strings.xml | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index bc9538d54..e3850e527 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -762,10 +762,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); final AtomicBoolean requestPermission = new AtomicBoolean(false); - builder.setTitle(R.string.sync_with_contacts); if (QuickConversationsService.isQuicksy()) { + builder.setTitle(R.string.quicksy_wants_your_consent); builder.setMessage(Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static))); } else { + builder.setTitle(R.string.sync_with_contacts); builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name))); } @StringRes int confirmButtonText; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index bbdd23986..18a29610a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -517,9 +517,10 @@ Text shared with %s Grant %1$s access to external storage Grant %1$s access to the camera + Quicksy asks for your consent to use your data Synchronize with contacts %1$s wants permission to access your address book to match it with your XMPP contact list.\nThis will display your contacts’ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server. -
By continuing you agree to our Privacy Policy and our Terms & Conditions.

You will now be asked to grant permission to access your contacts.]]>
+
Find more information in our Privacy Policy.]]>
Notify on all messages Notify only when mentioned Notifications disabled @@ -544,8 +545,8 @@ No app found to share URI Share URI with… Join the Conversation - Have some Quick Conversations -
You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.
Quicksy uploads and processes your contact list to make suggestions about possible contacts who are already on Quicksy.
By signing up you agree to our Privacy Policy and our Terms & Conditions.]]>
+ Welcome to Quicksy! + · Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days.

Find more information in our Privacy Policy.]]>
Agree and continue A guide is set up for account creation on conversations.im.\nWhen picking conversations.im as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. Your full XMPP address will be: %s From cd1821f2b6ff64d0b9373881dc30b378b3d0f6c8 Mon Sep 17 00:00:00 2001 From: p42ity Date: Fri, 9 Feb 2024 16:50:19 +0000 Subject: [PATCH 049/556] Avoid truncated voice messages for affected devices There were some devices which are known to produce truncated voice messages from time to time. I investigated this issue on a Fairphone 4 and found a configuration which did not show this behaviour in my experiments. These devices are very sensitive on AAC settings. That's why I added an extra section for them. The list of devices can be adjusted. Reviewed-on: https://codeberg.org/iNPUTmice/Conversations/pulls/192 Co-authored-by: p42ity Co-committed-by: p42ity --- .../conversations/ui/RecordingActivity.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index df3dabdce..5432ff335 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -17,6 +17,8 @@ import android.widget.Toast; import androidx.databinding.DataBindingUtil; +import com.google.common.collect.ImmutableSet; + import java.io.File; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; @@ -25,6 +27,7 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -95,6 +98,16 @@ public class RecordingActivity extends Activity implements View.OnClickListener } } + private static final Set AAC_SENSITIVE_DEVICES = + new ImmutableSet.Builder() + .add("FP4") // Fairphone 4 https://codeberg.org/monocles/monocles_chat/issues/133 + .add("ONEPLUS A6000") // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329 + .add("ONEPLUS A6003") // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329 + .add("ONEPLUS A6010") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133 + .add("ONEPLUS A6013") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133 + .add("Pixel 4a") // Pixel 4a https://github.com/iNPUTmice/Conversations/issues/4223 + .build(); + private boolean startRecording() { mRecorder = new MediaRecorder(); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); @@ -107,9 +120,16 @@ public class RecordingActivity extends Activity implements View.OnClickListener } else { outputFormat = MediaRecorder.OutputFormat.MPEG_4; mRecorder.setOutputFormat(outputFormat); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mRecorder.setAudioEncodingBitRate(96000); - mRecorder.setAudioSamplingRate(22050); + if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)) { + // Changing these three settings for AAC sensitive devices might lead to sporadically truncated (cut-off) voice messages. + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); + mRecorder.setAudioSamplingRate(24000); + mRecorder.setAudioEncodingBitRate(28000); + } else { + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + mRecorder.setAudioSamplingRate(22050); + mRecorder.setAudioEncodingBitRate(96000); + } } setupOutputFile(outputFormat); mRecorder.setOutputFile(mOutputFile.getAbsolutePath()); From 75f42d680d4088abc2fe6af0be37014a1ec7069c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Feb 2024 17:54:25 +0100 Subject: [PATCH 050/556] lower bitrate for voice recordings --- .../eu/siacs/conversations/ui/RecordingActivity.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 5432ff335..d3b3a96b2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -116,19 +116,19 @@ public class RecordingActivity extends Activity implements View.OnClickListener outputFormat = MediaRecorder.OutputFormat.OGG; mRecorder.setOutputFormat(outputFormat); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS); - mRecorder.setAudioEncodingBitRate(32000); + mRecorder.setAudioEncodingBitRate(32_000); } else { outputFormat = MediaRecorder.OutputFormat.MPEG_4; mRecorder.setOutputFormat(outputFormat); if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)) { // Changing these three settings for AAC sensitive devices might lead to sporadically truncated (cut-off) voice messages. mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); - mRecorder.setAudioSamplingRate(24000); - mRecorder.setAudioEncodingBitRate(28000); + mRecorder.setAudioSamplingRate(24_000); + mRecorder.setAudioEncodingBitRate(28_000); } else { mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mRecorder.setAudioSamplingRate(22050); - mRecorder.setAudioEncodingBitRate(96000); + mRecorder.setAudioSamplingRate(22_050); + mRecorder.setAudioEncodingBitRate(64_000); } } setupOutputFile(outputFormat); From 6ce052a33713365ff46a2b7ed1dc59789a25a2b8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Feb 2024 10:20:40 +0100 Subject: [PATCH 051/556] check sasl success message no response --- .../conversations/xmpp/XmppConnection.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index ef065c59d..541935712 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -623,6 +623,8 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("challenge")) { final Element challenge = tagReader.readElement(nextTag); processChallenge(challenge); + } else if (!LoginInfo.isSuccess(this.loginInfo)) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (this.streamId != null && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { final Element resumed = tagReader.readElement(nextTag); @@ -730,9 +732,13 @@ public class XmppConnection implements Runnable { } else { throw new AssertionError("Missing implementation for " + version); } + final LoginInfo currentLoginInfo = this.loginInfo; + if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } try { response.setContent( - this.loginInfo.saslMechanism.getResponse( + currentLoginInfo.saslMechanism.getResponse( challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. @@ -764,9 +770,9 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket)); + currentLoginInfo.success(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { - Log.e(Config.LOGTAG, String.valueOf(e)); + Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e); throw new StateChangingException(Account.State.UNAUTHORIZED); } Log.d( @@ -1481,7 +1487,7 @@ public class XmppConnection implements Runnable { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) && isSecure - && loginInfo != null + && LoginInfo.isSuccess(loginInfo) && streamId != null && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { @@ -1498,7 +1504,7 @@ public class XmppConnection implements Runnable { } else if (needsBinding) { if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure - && loginInfo != null) { + && LoginInfo.isSuccess(loginInfo)) { sendBindRequest(); } else { Log.d( @@ -2767,6 +2773,7 @@ public class XmppConnection implements Runnable { public final SaslMechanism saslMechanism; public final SaslMechanism.Version saslVersion; public final List inlineBindFeatures; + public final AtomicBoolean success = new AtomicBoolean(false); private LoginInfo( final SaslMechanism saslMechanism, @@ -2785,6 +2792,23 @@ public class XmppConnection implements Runnable { public static SaslMechanism mechanism(final LoginInfo loginInfo) { return loginInfo == null ? null : loginInfo.saslMechanism; } + + public void success(final String challenge, final SSLSocket sslSocket) + throws SaslMechanism.AuthenticationException { + final var response = this.saslMechanism.getResponse(challenge, sslSocket); + if (!Strings.isNullOrEmpty(response)) { + throw new SaslMechanism.AuthenticationException( + "processing success yielded another response"); + } + if (this.success.compareAndSet(false, true)) { + return; + } + throw new SaslMechanism.AuthenticationException("Process 'success' twice"); + } + + public static boolean isSuccess(final LoginInfo loginInfo) { + return loginInfo != null && loginInfo.success.get(); + } } private static class StreamId { From 9f1c4a42f8e854a497a0bd4089708951f2ccb13e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 Feb 2024 13:46:07 +0100 Subject: [PATCH 052/556] default to microphone foreground service type during call --- .../services/XmppConnectionService.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 23998ae53..723dea69a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1444,7 +1444,7 @@ public class XmppConnectionService extends Service { toggleForegroundService(false); } - private void toggleForegroundService(boolean force) { + private void toggleForegroundService(final boolean force) { final boolean status; final OngoingCall ongoing = ongoingCall.get(); if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || ongoing != null || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { @@ -1453,12 +1453,12 @@ public class XmppConnectionService extends Service { if (ongoing != null) { notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; - startForegroundOrCatch(id, notification); + startForegroundOrCatch(id, notification, true); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); } else { notification = this.mNotificationService.createForegroundNotification(); id = NotificationService.FOREGROUND_NOTIFICATION_ID; - startForegroundOrCatch(id, notification); + startForegroundOrCatch(id, notification, false); } if (!mForceForegroundService.get()) { @@ -1478,11 +1478,17 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); } - private void startForegroundOrCatch(final int id, final Notification notification) { + private void startForegroundOrCatch( + final int id, final Notification notification, final boolean requireMicrophone) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { final int foregroundServiceType; - if (getSystemService(PowerManager.class) + if (requireMicrophone + && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + Log.d(Config.LOGTAG, "defaulting to microphone foreground service type"); + } else if (getSystemService(PowerManager.class) .isIgnoringBatteryOptimizations(getPackageName())) { foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) @@ -1493,7 +1499,7 @@ public class XmppConnectionService extends Service { foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; } else { foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; - Log.w(Config.LOGTAG,"falling back to special use foreground service type"); + Log.w(Config.LOGTAG, "falling back to special use foreground service type"); } startForeground(id, notification, foregroundServiceType); } else { From 513ec579193c230f25c4f98f6c2cc2e6492e2f68 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 8 Feb 2024 15:59:29 +0000 Subject: [PATCH 053/556] Translated using Weblate (German) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 42ef452c0..f1e8137c8 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -320,8 +320,8 @@ XMPP-Adresse in Zwischenablage kopiert Fehlermeldung in Zwischenablage kopiert Internetadresse - Barcode scannen - Barcode anzeigen + QR-Code scannen + QR-Code anzeigen Sperrliste anzeigen Kontodetails Bestätigen @@ -616,7 +616,7 @@ Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen. Blind vertraute OMEMO-Schlüssel bedeutet, dass es sich um eine andere Person handeln könnte oder dass jemand sie abgehört haben könnte. Nicht vertraut - Ungültiger Barcode + Ungültiger QR-Code Cache-Ordner löschen (von der Kamera-App genutzt) Lösche Cache Lösche privaten Speicher @@ -1016,10 +1016,10 @@ Du hast dich von diesem Konto abgemeldet Anmelden Benachrichtigung ausblenden - Dein Kontakt verwendet nicht verifizierte Geräte. Scanne deren 2D-Barcode, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. + Dein Kontakt verwendet nicht verifizierte Geräte. Scanne deren QR-Code, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. Abmelden Abgemeldet - Du verwendest nicht verifizierte Geräte. Scanne den 2D-Barcode auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. + Du verwendest nicht verifizierte Geräte. Scanne den QR-Code auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. Spam melden Spam melden und Spammer sperren
\ No newline at end of file From ce997eb223e6a504946f5b7461532a8cb1aeffe8 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 8 Feb 2024 20:27:19 +0000 Subject: [PATCH 054/556] Translated using Weblate (Spanish) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index c8f12e22a..0c6e36b1a 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -321,7 +321,7 @@ Dirección XMPP copiada al portapapeles Mensaje de error copiado al portapapeles dirección web - Escanear código QR + Escanear el código QR Mostrar código QR Mostrar contactos bloqueados Detalles de la cuenta @@ -1030,10 +1030,10 @@ Has salido de esta cuenta Iniciar sesión Ocultar la notificación - Su contacto utiliza dispositivos no verificados. Escanea su código de barras en 2D para realizar la verificación e impedir ataques MITM activos. + Tu contacto utiliza dispositivos no verificados. Escanea su código QR para realizar la verificación e impedir ataques MITM activos. Desconectarse Desconectado - Está utilizando dispositivos no verificados. Escanea el código de barras 2D en tus otros dispositivos para realizar la verificación e impedir los ataques MITM activos. + Está utilizando dispositivos no verificados. Escanea el código QR en tus otros dispositivos para realizar la verificación e impedir ataques MITM activos. Informar de spam y bloquear al spammer Informar sobre spam \ No newline at end of file From c1052c5811b6f92cda50c5ca1958cebbb4cda573 Mon Sep 17 00:00:00 2001 From: ghose Date: Thu, 8 Feb 2024 13:59:44 +0000 Subject: [PATCH 055/556] Translated using Weblate (Galician) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 1ed177ab0..ecd82650c 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -322,8 +322,8 @@ Copiouse o enderezo XMPP ao portapapeis Mensaxe do fallo copiado ao portapapeis Dirección Web - Escanear código de barras 2D - Mostar código de barras 2D + Escanear código QR + Mostar código QR Mostrar lista de bloqueo Detalles da conta Confirmar @@ -618,7 +618,7 @@ Confiar en dispositivos novos de contactos non verificados, pero solicitar confirmación manual de novos dispositivos para contactos verificados. Chaves OMEMO de confianza cega, significa que podería ser calquera outra persoa ou algunha impostora. Non confiables - Código de barras 2D non válido + Código QR non válido Baleirar o cartafol da caché (utilizado pola cámara) Baleirar caché Baleirar almacenaxe privada @@ -1019,10 +1019,10 @@ Pechaches a sesión desta conta Acceder Agochar notificación - O teu contacto usa dispositivos non verificados. Escanea o seu código de barras 2D para verficalo e impedir ataques MITM. + O teu contacto usa dispositivos non verificados. Escanea o seu código QR para verficalo e impedir ataques MITM. Saír da sesión Sesión pechada - Estás a usar dispositivos non verificados. Escanea os códigos de barras 2D nos teus outros dispositivos para verificalos e impedir ataques MITM. + Estás a usar dispositivos non verificados. Escanea os códigos QR nos teus outros dispositivos para verificalos e impedir ataques MITM. Informar de spam e bloquear conta Informar de spam \ No newline at end of file From 1118184ff0b71223bf815049f1cc9529b620bd1e Mon Sep 17 00:00:00 2001 From: SomeTr Date: Thu, 8 Feb 2024 20:40:57 +0000 Subject: [PATCH 056/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 8a2d7510a..983a0a078 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -602,7 +602,7 @@ Автоматично довіряти всім новим пристроям співрозмовників, які ще не пройшли перевірки, запитувати підтвердження вручну щоразу, як перевірений контакт додає свій новий пристрій. Ключі OMEMO, яким Ви довіряєте наосліп, тобто співрозмовник може бути не тим, кому Ви довіряєте. Недовірений - Недійсний QR-код + Недійсний QR-код Очистити теку з кешем (використовується застосунком Камера) Очистити кеш Очистити приватне сховище From 7fc79edef7c4b2c8106251e38e82b33b782af23a Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Thu, 8 Feb 2024 11:05:50 +0000 Subject: [PATCH 057/556] Translated using Weblate (Romanian) Currently translated at 100.0% (979 of 979 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index a852da73f..cff52668c 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -323,8 +323,8 @@ Adresă XMPP copiată în memorie Mesaj de eroare copiat în memorie adresă web - Scanează cod de bare 2D - Arată cod de bare 2D + Scanează cod QR + Arată cod QR Listă contacte blocate Detalii cont Confirmă @@ -627,7 +627,7 @@ Ai încredere în toate dispozitivele noi ale contactelor care nu au fost verificate anterior, dar cere confirmarea manuală pentru dispozitivele noi ale contactelor verificate. Încredere oarbă în aceste chei OMEMO, aceasta înseamnă că ar putea fi altcineva sau cineva și-a strecurat propriile chei. De neîncredere - Cod de bare 2D invalid + Cod QR invalid Curățare dosar temporar (folosit de aplicația cameră foto) Curăța memoria temporară Curăță stocarea privată @@ -1038,10 +1038,10 @@ V-ați deconectat de la acest cont Conectați-vă Ascunde notificare - Persoana de contact utilizează dispozitive neverificate. Scanați codul de bare 2D al acestora pentru a efectua verificarea și a împiedica atacurile MITM active. + Persoana de contact utilizează dispozitive neverificate. Scanați codul QR al acestora pentru a efectua verificarea și a împiedica atacurile MITM active. Deconectare Deconectat - Folosiți dispozitive neverificate. Scanați codul de bare 2D pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active. + Folosiți dispozitive neverificate. Scanați codul QR pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active. Raportează spam și blochează spamerul Raportează spam \ No newline at end of file From 431fe50fd6ecaf37e72213b9f302a01fef3a2ab3 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 9 Feb 2024 13:30:40 +0000 Subject: [PATCH 058/556] Translated using Weblate (Spanish) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 0c6e36b1a..5e2bf83d6 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1036,4 +1036,6 @@ Está utilizando dispositivos no verificados. Escanea el código QR en tus otros dispositivos para realizar la verificación e impedir ataques MITM activos. Informar de spam y bloquear al spammer Informar sobre spam + ¡Bienvenido a Quicksy! + Quicksy pide tu consentimiento para utilizar tus datos \ No newline at end of file From d4cdd3f39fb68b04863f6cb62d3e34892e5c47fb Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Fri, 9 Feb 2024 11:40:47 +0000 Subject: [PATCH 059/556] Translated using Weblate (Romanian) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index cff52668c..89a4ef24e 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1044,4 +1044,6 @@ Folosiți dispozitive neverificate. Scanați codul QR pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active. Raportează spam și blochează spamerul Raportează spam + Bine ați venit la Quicksy! + Quicksy vă solicită consimțământul pentru a utiliza datele dumneavoastră \ No newline at end of file From 899432c0e259e1dd79b0ad223977fc7aab11fe3e Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 9 Feb 2024 13:32:16 +0000 Subject: [PATCH 060/556] Translated using Weblate (Spanish) Currently translated at 100.0% (55 of 55 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/4208904.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4208904.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4208904.txt b/fastlane/metadata/android/es-ES/changelogs/4208904.txt new file mode 100644 index 000000000..700284fd2 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4208904.txt @@ -0,0 +1,2 @@ +* Correcciones de errores menores +* ligeras modificaciones en el flujo interno de Quicksy From 1dcf2a473210fd5cbaa9deb56afd37610753256d Mon Sep 17 00:00:00 2001 From: random_r Date: Sat, 10 Feb 2024 11:04:02 +0000 Subject: [PATCH 061/556] Translated using Weblate (Italian) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 7a59158c8..69510ee5e 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -321,8 +321,8 @@ Indirizzo XMPP copiato negli appunti Messaggio di errore copiato negli appunti indirizzo web - Scansiona codice a barre 2D - Mostra codice a barre 2D + Scansiona codice QR + Mostra codice QR Mostra la lista nera Dettagli del profilo Conferma @@ -619,7 +619,7 @@ Fidati di nuovi dispositivi da contatti non verificati, ma chiedi una conferma manuale per nuovi dispositivi da contatti verificati. Chiavi OMEMO accettate ciecamente, perciò potrebbero essere di qualcun altro o qualcuno potrebbe essersi intromesso. Non fidato - Codice a barre 2D non valido + Codice QR non valido Svuota la cartella della cache (usata dall\'app fotocamera) Svuota cache Svuota archivio privato @@ -1030,10 +1030,12 @@ Ti sei disconnesso da questo profilo Accedi Nascondi notifica - Il tuo contatto usa dispositivi non verificati. Scansiona il suo codice a barre 2D per effettuare la verifica e impedire attacchi MITM attivi. + Il tuo contatto usa dispositivi non verificati. Scansiona il suo codice QR per effettuare la verifica e impedire attacchi MITM attivi. Disconnetti Disconnesso - Stai usando dispositivi non verificati. Scansiona il codice a barre 2D nei tuoi altri dispositivi per effettuare la verifica e impedire attacchi MITM attivi. + Stai usando dispositivi non verificati. Scansiona il codice QR nei tuoi altri dispositivi per effettuare la verifica e impedire attacchi MITM attivi. Segnala spam e blocca l\'utente Segnala spam + Benvenuti su Quicksy! + Quicksy ti chiede il consenso per usare i tuoi dati \ No newline at end of file From 35cfc1028beeb47fbe3abd1a157984c0e55ff136 Mon Sep 17 00:00:00 2001 From: acioustick Date: Sat, 10 Feb 2024 12:46:16 +0000 Subject: [PATCH 062/556] Translated using Weblate (Japanese) Currently translated at 98.6% (967 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 9e68283d9..2a7b3bb59 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -181,7 +181,7 @@ 出席情報告知から OpenPGP 公開鍵を削除してもよろしいですか?\n連絡先はあなたに OpenPGP 暗号化メッセージを送信できなくなります。 OpenPGP 公開鍵を公開しました。 アカウントを有効化 - アカウントを削除すると会話履歴がすべて消去されます + アカウントを削除してよろしいですか?アカウントを削除すると会話履歴がすべて消去されます 音声を録音 XMPP アドレス XMPP アドレスをブロック @@ -291,10 +291,10 @@ その他 ブックマーク同期 OMEMO フィンガープリントをクリップボードにコピーしました - このグループチャットから出禁にされています + このグループチャットへの参加はブロックされています このグループチャットはメンバー制です リソース制限 - このグループチャットから蹴り出されています + このグループチャットから退出させられました このグループチャットは閉鎖されました あなたはもうこのグループチャットに参加していません 技術的理由の為、あなたはこのグループチャットを離れました @@ -896,15 +896,15 @@ 通話終了 応答 拒否 - デバイス発見 - 鳴動 + デバイスを探索中 + 呼び出し中 取込中 通話に接続できません 接続切断 撤回された通話 アプリの失敗 検証に問題 - 電話を切る + 通話を切る 継続中の通話 継続中の映像通話 通話再接続中 @@ -913,7 +913,7 @@ 着信通話 不在着信通話・%s 発信通話 - 不在着信通話 + 不在着信 %2$sから%1$d件の不在着信 @@ -932,7 +932,7 @@ 継続中の通話に戻る カメラを切り替えできません 最上に留める - 最上から留めるのをやめる + 最上部へのピン止めを外す GPX 追跡 メッセージを修正できません すべての会話 @@ -944,9 +944,9 @@ 非暗号化 終了 音声メールを録音 - 音声再生 - 音声一時中断 - 連絡先を追加、作成またはグループチャットに参加、または談話室を発見する + 音声を再生 + 音声を一時停止 + 連絡先を追加、作成またはグループチャットに参加、またはチャンネルを発見する %1$d人の参加者を表示 @@ -956,7 +956,7 @@ 配信に失敗 更なるオプション アプリケーションが見つかりません - 会話に招待 + Conversationsに招待 招待を解析できません サーバーは招待の作成をサポートしていません この機能をサポートするアクティブなアカウントがありません @@ -968,19 +968,33 @@ 一時的な認証失敗 アバターを削除 Tor使用中のため通話できません - ビデオ通話切替 + ビデオ通話へ切替 このアカウントをログアウトしました ルート - ビデオ通話を却下する - 着信通話 (%s) · %s + ビデオ通話を拒否 + 着信中の通話 (%s) · %s XMPPアカウント グループ 選択したファイルは、旧式のファイル形式ので復元できません グループを検索 - 発信通話 (%s) · %s + 発信中の通話 (%s) · %s ログアウトしました 発信通話 · %s オーディオブック 談話室の発見は<a href=https://search.jabber.network>search.jabber.network</a>というサービスを利用します.<br><br>利用するとIPアドレスと検索語はそのサービスに送信されます。詳細についてはそのサービスの<a href=https://search.jabber.network/privacy>個人情報保護方針</a>を参照してください。 自分で保存したバックアップしか復元しないでください! + XMPP経由でPushメッセージを端末に転送するユーザー指定のPushサーバー。 + ログイン + 通知を表示しない + アカウントをサーバーから削除 + ログアウト + なし(無効) + 拒否 + Pushサーバー + グループチャットとして保存 + UnifiedPushディストリビューター + スパムを報告 + Pushメッセージを受信する際に経由するアカウント。 + サーバーからアカウントを削除できませんでした + 概要 \ No newline at end of file From ad86d3317e8691aa9bb1b229bd8f5c455f639733 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Sat, 10 Feb 2024 05:28:32 +0000 Subject: [PATCH 063/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index bffea05e1..c14afe1f1 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1034,4 +1034,6 @@ 您正在使用未经验证的设备。扫描您其他设备的二维码进行验证并阻止主动式中间人攻击。 报告垃圾消息并屏蔽垃圾消息发送者 报告垃圾消息 + 欢迎使用 Quicksy! + Quicksy 请求您同意使用您的数据 \ No newline at end of file From f9e49b0fa62df34b25ad92ddc0cc674f7e089e99 Mon Sep 17 00:00:00 2001 From: random_r Date: Sat, 10 Feb 2024 11:05:49 +0000 Subject: [PATCH 064/556] Translated using Weblate (Italian) Currently translated at 100.0% (55 of 55 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/4208904.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/4208904.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4208904.txt b/fastlane/metadata/android/it-IT/changelogs/4208904.txt new file mode 100644 index 000000000..6186e8210 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4208904.txt @@ -0,0 +1,2 @@ +* correzioni minori +* piccole modifiche nel flusso di configurazione di Quicksy From 0ef071b65b05eb71ab08e856781dccc6133af210 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Sat, 10 Feb 2024 05:31:25 +0000 Subject: [PATCH 065/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (55 of 55 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4208904.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4208904.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208904.txt b/fastlane/metadata/android/zh-CN/changelogs/4208904.txt new file mode 100644 index 000000000..da11d968f --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4208904.txt @@ -0,0 +1,2 @@ +* 修正了一些小错误 +* Quicksy 板载流程略有修改 From 8181eb18bc38f3d4658cfce8185dfec4ef4183d2 Mon Sep 17 00:00:00 2001 From: acioustick Date: Sat, 10 Feb 2024 12:53:55 +0000 Subject: [PATCH 066/556] Translated using Weblate (Japanese) Currently translated at 98.6% (967 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 2a7b3bb59..af8fa11cd 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -906,12 +906,12 @@ 検証に問題 通話を切る 継続中の通話 - 継続中の映像通話 + 継続中のビデオ通話 通話再接続中 ビデオ通話再接続中 通話するのに Tor を無効化 着信通話 - 不在着信通話・%s + 不在着信・%s 発信通話 不在着信 From 9ad95ba40274e298951288b99261d3cf2e8d6fd8 Mon Sep 17 00:00:00 2001 From: acioustick Date: Sat, 10 Feb 2024 13:04:32 +0000 Subject: [PATCH 067/556] Translated using Weblate (Japanese) Currently translated at 98.6% (967 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index af8fa11cd..4298a2338 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -7,7 +7,7 @@ 会話を閉じる 連絡先の詳細 グループチャットの詳細 - 談話室の詳細 + チャンネルの詳細 アカウントを追加 名前を編集 アドレス帳に追加 @@ -34,7 +34,7 @@ 送信中… メッセージを復号しています。しばらくお待ちください… - OpenPGP 暗号化メッセージ + OpenPGPで暗号化されたメッセージ ニックネームは既に使用されています このニックネームは使えません 管理者 @@ -42,11 +42,11 @@ 調停者 参加者 訪問者 - 連絡先名簿から %s を削除しますか? この連絡先との会話は削除されません。 - %s からあなたに送信されるメッセージをブロックしますか? + 連絡先リストから%sを削除しますか? この連絡先との会話は削除されません。 + %sからあなたに送信されるメッセージをブロックしますか? %s のブロックを解除し、あなたにメッセージを送信できるようにしますか? - %s からの連絡をすべてブロックしますか? - %s からすべての連絡先のブロックを解除しますか? + %sの連絡先をすべてブロックしますか? + %sのすべての連絡先のブロックを解除しますか? 連絡先をブロックしました ブロックしました %s のブックマークを削除しますか? このブックマークとの会話は削除されません。 @@ -68,14 +68,14 @@ 保存 OK %1$s がクラッシュしました - あなたの XMPP アカウントを使用してスタックトレースの送信をすることで、 %1$s の継続的な開発を支援します。 + あなたのXMPPアカウントを使用してスタックトレースを送信すると、 %1$sの実施中の開発の支援となります。 今すぐ送信 今後は表示しない アカウントに接続できません 複数のアカウントに接続できません タップしてアカウントを管理 ファイルを添付 - 連絡先が連絡先名簿にありません。名簿に追加しますか? + 連絡先が連絡先リストにありません。リストに追加しますか? 連絡先を追加 配信に失敗しました 送信用画像の準備中 @@ -105,8 +105,10 @@ 依頼中… 待機中… OpenPGP 鍵が見つかりません - 連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n連絡先に OpenPGP をセットアップするように依頼してください。 - OpenPGP 鍵が見つかりません + 連絡先が公開鍵を通知していないため、あなたのメッセージを暗号化することができません。 +\n +\n連絡先にOpenPGPを設定するように依頼してください。 + OpenPGPの鍵が見つかりません 連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n連絡先に OpenPGP をセットアップするように依頼してください。 全般 ファイルを受取 @@ -122,10 +124,10 @@ 新着メッセージの通知音 着信通話の呼出音 猶予期間 - 別のデバイスでの操作を検知した際に、通知を止める時間の長さ + 別のデバイスでの操作を検知した際に、通知を止める時間の長さ。 詳細 クラッシュレポートを送信しない - スタックトレースを送信すると、 Conversations の開発を支援します + スタックトレースを送信すると、 開発の助けとなります メッセージを確認 あなたがメッセージを受信して読んだときに、連絡先に知らせる スクリーンショットを防ぐ @@ -907,9 +909,9 @@ 通話を切る 継続中の通話 継続中のビデオ通話 - 通話再接続中 - ビデオ通話再接続中 - 通話するのに Tor を無効化 + 通話に再接続中 + ビデオ通話に再接続中 + 通話を行う際にはTorを無効にしてください 着信通話 不在着信・%s 発信通話 From 6182ba9f930b35acdb7a3119f50c94af4be41c31 Mon Sep 17 00:00:00 2001 From: acioustick Date: Sat, 10 Feb 2024 14:32:40 +0000 Subject: [PATCH 068/556] Translated using Weblate (Japanese) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/ja/ --- src/quicksy/res/values-ja/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quicksy/res/values-ja/strings.xml b/src/quicksy/res/values-ja/strings.xml index d846cd3df..fb77f0f19 100644 --- a/src/quicksy/res/values-ja/strings.xml +++ b/src/quicksy/res/values-ja/strings.xml @@ -1,7 +1,7 @@ 別のデバイスで活動を見た後、Quicksy を静かにする時間の長さ - スタックトレースを送信することで、あなたは Quicksy の継続的な開発を支援しています + スタックトレースを送信すると、 Quicksyの開発の助けとなります Quicksy を使用するときに、すべての連絡先に知らせましょう 画面がオフになっている場合でも通知を受信し続けるには、保護されたアプリのリストに Quicksy を追加する必要があります。 Quicksy プロフィール写真 @@ -9,4 +9,4 @@ サーバーの同一性を確認できません。 未知のセキュリティエラー。 サーバーへの接続中にタイムアウトが発生しました。 - + \ No newline at end of file From 93b5a099b0aa146cfa8eebd960b7ede32cbcb394 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 11 Feb 2024 08:45:46 +0100 Subject: [PATCH 069/556] add json mime type according to RFC4627 --- src/main/java/eu/siacs/conversations/utils/MimeUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index c90872028..adf3841d3 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -68,6 +68,7 @@ public final class MimeUtils { // by guessExtensionFromMimeType. add("application/andrew-inset", "ez"); add("application/dsptype", "tsp"); + add("application/json", "json"); add("application/epub+zip", "epub"); add("application/gpx+xml", "gpx"); add("application/hta", "hta"); From 2833fc833c54ecdcfd4e0295e39f60cfe3d33712 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 11 Feb 2024 10:40:50 +0100 Subject: [PATCH 070/556] fix JET spec compliance --- .../jingle/JingleFileTransferConnection.java | 13 +++++-- .../xmpp/jingle/stanzas/Content.java | 34 +++++++++++++++++ .../xmpp/jingle/stanzas/JinglePacket.java | 38 ------------------- 3 files changed, 43 insertions(+), 42 deletions(-) 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 0f7247fdc..632a8f034 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -209,8 +209,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection this.transportSecurity = new TransportSecurity( xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV()); - jinglePacket.setSecurity( - Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage); + final var contents = jinglePacket.getJingleContents(); + final var rawContent = + contents.get(Iterables.getOnlyElement(contentMap.contents.keySet())); + if (rawContent != null) { + rawContent.setSecurity(xmppAxolotlMessage); + } } jinglePacket.setTo(id.with); xmppConnectionService.sendIqPacket( @@ -327,8 +331,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection return; } final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + final var contents = jinglePacket.getJingleContents(); + final var rawContent = contents.get(Iterables.getOnlyElement(contentMap.contents.keySet())); final var security = - jinglePacket.getSecurity(Iterables.getOnlyElement(contentMap.contents.keySet())); + rawContent == null ? null : rawContent.getSecurity(jinglePacket.getFrom()); if (security != null) { Log.d(Config.LOGTAG, "found security element!"); keyTransportMessage = @@ -349,7 +355,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (transition(State.SESSION_INITIALIZED, () -> setRemoteContentMap(contentMap))) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, jinglePacket.toString()); Log.d( Config.LOGTAG, "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index fbde212a3..a4cb13e07 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -9,8 +9,11 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.SessionDescription; import java.util.Locale; @@ -102,6 +105,37 @@ public class Content extends Element { } } + public void setSecurity(final XmppAxolotlMessage xmppAxolotlMessage) { + final String contentName = this.getContentName(); + final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); + security.setAttribute("name", contentName); + security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding"); + security.setAttribute("type", AxolotlService.PEP_PREFIX); + security.addChild(xmppAxolotlMessage.toElement()); + this.addChild(security); + } + + public XmppAxolotlMessage getSecurity(final Jid from) { + final String contentName = this.getContentName(); + for (final Element child : this.children) { + if ("security".equals(child.getName()) + && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) { + final String name = child.getAttribute("name"); + final String type = child.getAttribute("type"); + final String cipher = child.getAttribute("cipher"); + if (contentName.equals(name) + && AxolotlService.PEP_PREFIX.equals(type) + && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) { + final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX); + if (encrypted != null) { + return XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); + } + } + } + } + return null; + } + public void setTransport(GenericTransportInfo transportInfo) { this.addChild(transportInfo); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 552046fb8..82c5b155c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; -import android.util.Log; - import androidx.annotation.NonNull; import com.google.common.base.CaseFormat; @@ -9,9 +7,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; @@ -121,39 +116,6 @@ public class JinglePacket extends IqPacket { jingle.addChild(child); } - public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) { - final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); - security.setAttribute("name", name); - security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding"); - security.setAttribute("type", AxolotlService.PEP_PREFIX); - security.addChild(xmppAxolotlMessage.toElement()); - addJingleChild(security); - } - - public XmppAxolotlMessage getSecurity(final String nameNeedle) { - final Element jingle = findChild("jingle", Namespace.JINGLE); - if (jingle == null) { - return null; - } - for (final Element child : jingle.getChildren()) { - if ("security".equals(child.getName()) - && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) { - final String name = child.getAttribute("name"); - final String type = child.getAttribute("type"); - final String cipher = child.getAttribute("cipher"); - if (nameNeedle.equals(name) - && AxolotlService.PEP_PREFIX.equals(type) - && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) { - final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX); - if (encrypted != null) { - return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid()); - } - } - } - } - return null; - } - public String getSessionId() { return findChild("jingle", Namespace.JINGLE).getAttribute("sid"); } From 5cad2dccb219e151120b1760c83784348bdd072d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 11 Feb 2024 16:40:17 +0100 Subject: [PATCH 071/556] version bump to 2.13.2 (and rename changelogs) --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- .../android/en-US/changelogs/{4208904.txt => 4209004.txt} | 0 .../android/es-ES/changelogs/{4208904.txt => 4209004.txt} | 0 .../android/it-IT/changelogs/{4208904.txt => 4209004.txt} | 0 .../android/zh-CN/changelogs/{4208904.txt => 4209004.txt} | 0 6 files changed, 7 insertions(+), 2 deletions(-) rename fastlane/metadata/android/en-US/changelogs/{4208904.txt => 4209004.txt} (100%) rename fastlane/metadata/android/es-ES/changelogs/{4208904.txt => 4209004.txt} (100%) rename fastlane/metadata/android/it-IT/changelogs/{4208904.txt => 4209004.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4208904.txt => 4209004.txt} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47cf9350f..ea87a3906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.13.2 + +* minor bug fixes +* slight modifications in Quicksy onboard flow + ### Version 2.13.1 * Support P2P file transfer via WebRTC data channels diff --git a/build.gradle b/build.gradle index ecb5321d6..51e51fc85 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42088 - versionName "2.13.1" + versionCode 42090 + versionName "2.13.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4208904.txt b/fastlane/metadata/android/en-US/changelogs/4209004.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4208904.txt rename to fastlane/metadata/android/en-US/changelogs/4209004.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4208904.txt b/fastlane/metadata/android/es-ES/changelogs/4209004.txt similarity index 100% rename from fastlane/metadata/android/es-ES/changelogs/4208904.txt rename to fastlane/metadata/android/es-ES/changelogs/4209004.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4208904.txt b/fastlane/metadata/android/it-IT/changelogs/4209004.txt similarity index 100% rename from fastlane/metadata/android/it-IT/changelogs/4208904.txt rename to fastlane/metadata/android/it-IT/changelogs/4209004.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208904.txt b/fastlane/metadata/android/zh-CN/changelogs/4209004.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4208904.txt rename to fastlane/metadata/android/zh-CN/changelogs/4209004.txt From ce8ccd0c793f49274c4cb588eb0585d6c48baf00 Mon Sep 17 00:00:00 2001 From: ghose Date: Mon, 12 Feb 2024 08:27:40 +0000 Subject: [PATCH 072/556] Translated using Weblate (Galician) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index ecd82650c..88d143770 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1025,4 +1025,6 @@ Estás a usar dispositivos non verificados. Escanea os códigos QR nos teus outros dispositivos para verificalos e impedir ataques MITM. Informar de spam e bloquear conta Informar de spam + Benvida a Quicksy! + Quicksy solicita permiso para usar os teus datos \ No newline at end of file From 65149c9f2da2803dc618aba821a43e8fe8e9dfa1 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Mon, 12 Feb 2024 07:44:36 +0000 Subject: [PATCH 073/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 983a0a078..aba3ad109 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1076,4 +1076,6 @@ Ви вийшли Повідомити про спам і заблокувати спамера Повідомити про спам + Вітаємо у Quicksy! + Quicksy просить згоду на використання Ваших даних \ No newline at end of file From 0fb3fabd5fd8431463bc19b5b0c83f9c5d96c3a4 Mon Sep 17 00:00:00 2001 From: ghose Date: Mon, 12 Feb 2024 08:29:59 +0000 Subject: [PATCH 074/556] Translated using Weblate (Galician) Currently translated at 49.0% (27 of 55 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4208104.txt | 4 ++-- fastlane/metadata/android/gl-ES/changelogs/4209004.txt | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4209004.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4208104.txt b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt index f4532b679..ff912c254 100644 --- a/fastlane/metadata/android/gl-ES/changelogs/4208104.txt +++ b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt @@ -1,4 +1,4 @@ -* Acceso mais rápido á 'Mostrar código QR' -* Soporte para a PEP Marcadores Nativos +* Acceso mais rápido a 'Mostrar código QR' +* Soporte para PEP Marcadores Nativos * Engadido soporte para SDP Offer / Answer Model (usado por pasarelas SIP) * Establecida a API de Android 14 como obxectivo diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209004.txt b/fastlane/metadata/android/gl-ES/changelogs/4209004.txt new file mode 100644 index 000000000..d1f8b3e26 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* arranxos menores +* pequenos cambios no primeiro incio de Quicksy From 5fa1caf7ee8ff39948c96c69f3ad90dbcca33d07 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Mon, 12 Feb 2024 07:48:22 +0000 Subject: [PATCH 075/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (55 of 55 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4209004.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4209004.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209004.txt b/fastlane/metadata/android/uk/changelogs/4209004.txt new file mode 100644 index 000000000..2863414a6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* Незначні виправлення помилок +* Деякі зміни у процесі підключення до Quicksy From 1cfc5d426eb1d6b77a9f41ef5dfb500393371737 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 13 Feb 2024 11:04:38 +0100 Subject: [PATCH 076/556] access Android_id only on push --- .../services/XmppConnectionService.java | 44 +++++++++++++------ .../conversations/utils/PhoneHelper.java | 8 ++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 723dea69a..0a3e9059b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -913,27 +913,45 @@ public class XmppConnectionService extends Service { manageAccountConnectionStates(ACTION_INTERNAL_PING, null); } - private synchronized void manageAccountConnectionStates(final String action, final Bundle extras) { + private synchronized void manageAccountConnectionStates( + final String action, final Bundle extras) { final String pushedAccountHash = extras == null ? null : extras.getString("account"); - final boolean interactive = Arrays.asList(ACTION_TRY_AGAIN).contains(action); + final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action); WakeLockHelper.acquire(wakeLock); - boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); + boolean pingNow = + ConnectivityManager.CONNECTIVITY_ACTION.equals(action) + || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 + && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); final HashSet pingCandidates = new HashSet<>(); - final String androidId = PhoneHelper.getAndroidId(this); + final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this); for (final Account account : accounts) { - final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash); - pingNow |= processAccountState(account, - interactive, - "ui".equals(action), - pushWasMeantForThisAccount, - pingCandidates); + final boolean pushWasMeantForThisAccount = + androidId != null + && CryptoHelper.getAccountFingerprint(account, androidId) + .equals(pushedAccountHash); + pingNow |= + processAccountState( + account, + interactive, + "ui".equals(action), + pushWasMeantForThisAccount, + pingCandidates); } if (pingNow) { - for (Account account : pingCandidates) { + for (final Account account : pingCandidates) { final boolean lowTimeout = isInLowPingTimeoutMode(account); account.getXmppConnection().sendPing(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")"); - scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " send ping (action=" + + action + + ",lowTimeout=" + + lowTimeout + + ")"); + scheduleWakeUpCall( + lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, + account.getUuid().hashCode()); } } WakeLockHelper.release(wakeLock); diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index 9ff492578..edb181f4f 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -13,14 +13,12 @@ import android.provider.Settings; public class PhoneHelper { @SuppressLint("HardwareIds") - public static String getAndroidId(Context context) { + public static String getAndroidId(final Context context) { return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } - public static Uri getProfilePictureUri(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) - != PackageManager.PERMISSION_GRANTED) { + public static Uri getProfilePictureUri(final Context context) { + if (context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { return null; } final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; From 617dd76d2f5654bca263502c4325a6cd6f78e99a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 13 Feb 2024 18:29:08 +0100 Subject: [PATCH 077/556] add shortcut info only for 'messages' channel --- .../services/NotificationService.java | 274 +++++++++--------- 1 file changed, 138 insertions(+), 136 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 641e2e109..3ad182867 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -115,6 +115,7 @@ public class NotificationService { private long mLastNotification; private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel"; + private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages"; private Ringtone currentlyPlayingRingtone = null; private ScheduledFuture vibrationFuture; @@ -244,7 +245,7 @@ public class NotificationService { final NotificationChannel messagesChannel = new NotificationChannel( - "messages", + MESSAGES_NOTIFICATION_CHANNEL, c.getString(R.string.messages_channel_name), NotificationManager.IMPORTANCE_HIGH); messagesChannel.setShowBadge(true); @@ -1112,7 +1113,7 @@ public class NotificationService { final Builder mBuilder = new NotificationCompat.Builder( mXmppConnectionService, - quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); + quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages")); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); style.setBigContentTitle( mXmppConnectionService @@ -1178,160 +1179,161 @@ public class NotificationService { private Builder buildSingleConversations( final ArrayList messages, final boolean notify, final boolean quietHours) { - final Builder mBuilder = - new NotificationCompat.Builder( - mXmppConnectionService, - quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); - if (messages.size() >= 1) { - final Conversation conversation = (Conversation) messages.get(0).getConversation(); - mBuilder.setLargeIcon( + final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"); + final Builder notificationBuilder = + new NotificationCompat.Builder(mXmppConnectionService, channel); + if (messages.isEmpty()) { + return notificationBuilder; + } + final Conversation conversation = (Conversation) messages.get(0).getConversation(); + notificationBuilder.setLargeIcon( + mXmppConnectionService + .getAvatarService() + .get( + conversation, + AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + notificationBuilder.setContentTitle(conversation.getName()); + if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { + int count = messages.size(); + notificationBuilder.setContentText( mXmppConnectionService - .getAvatarService() - .get( - conversation, - AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); - mBuilder.setContentTitle(conversation.getName()); - if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { - int count = messages.size(); - mBuilder.setContentText( - mXmppConnectionService - .getResources() - .getQuantityString(R.plurals.x_messages, count, count)); + .getResources() + .getQuantityString(R.plurals.x_messages, count, count)); + } else { + final Message message; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && (message = getImage(messages)) != null) { + modifyForImage(notificationBuilder, message, messages); } else { - Message message; - // TODO starting with Android 9 we might want to put images in MessageStyle - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && (message = getImage(messages)) != null) { - modifyForImage(mBuilder, message, messages); - } else { - modifyForTextOnly(mBuilder, messages); - } - RemoteInput remoteInput = - new RemoteInput.Builder("text_reply") - .setLabel( - UIHelper.getMessageHint( - mXmppConnectionService, conversation)) - .build(); - PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation); - NotificationCompat.Action markReadAction = - new NotificationCompat.Action.Builder( - R.drawable.ic_drafts_white_24dp, - mXmppConnectionService.getString(R.string.mark_as_read), - markAsReadPendingIntent) - .setSemanticAction( - NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) - .setShowsUserInterface(false) - .build(); - final String replyLabel = mXmppConnectionService.getString(R.string.reply); - final String lastMessageUuid = Iterables.getLast(messages).getUuid(); - final NotificationCompat.Action replyAction = - new NotificationCompat.Action.Builder( - R.drawable.ic_send_text_offline, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, false)) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) - .setShowsUserInterface(false) - .addRemoteInput(remoteInput) - .build(); - final NotificationCompat.Action wearReplyAction = - new NotificationCompat.Action.Builder( - R.drawable.ic_wear_reply, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, true)) - .addRemoteInput(remoteInput) - .build(); - mBuilder.extend( - new NotificationCompat.WearableExtender().addAction(wearReplyAction)); - int addedActionsCount = 1; - mBuilder.addAction(markReadAction); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mBuilder.addAction(replyAction); - ++addedActionsCount; - } + modifyForTextOnly(notificationBuilder, messages); + } + RemoteInput remoteInput = + new RemoteInput.Builder("text_reply") + .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)) + .build(); + PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation); + NotificationCompat.Action markReadAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_drafts_white_24dp, + mXmppConnectionService.getString(R.string.mark_as_read), + markAsReadPendingIntent) + .setSemanticAction( + NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build(); + final String replyLabel = mXmppConnectionService.getString(R.string.reply); + final String lastMessageUuid = Iterables.getLast(messages).getUuid(); + final NotificationCompat.Action replyAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_send_text_offline, + replyLabel, + createReplyIntent(conversation, lastMessageUuid, false)) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + .addRemoteInput(remoteInput) + .build(); + final NotificationCompat.Action wearReplyAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_wear_reply, + replyLabel, + createReplyIntent(conversation, lastMessageUuid, true)) + .addRemoteInput(remoteInput) + .build(); + notificationBuilder.extend( + new NotificationCompat.WearableExtender().addAction(wearReplyAction)); + int addedActionsCount = 1; + notificationBuilder.addAction(markReadAction); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + notificationBuilder.addAction(replyAction); + ++addedActionsCount; + } - if (displaySnoozeAction(messages)) { - String label = mXmppConnectionService.getString(R.string.snooze); - PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation); - NotificationCompat.Action snoozeAction = - new NotificationCompat.Action.Builder( - R.drawable.ic_notifications_paused_white_24dp, - label, - pendingSnoozeIntent) - .build(); - mBuilder.addAction(snoozeAction); - ++addedActionsCount; - } - if (addedActionsCount < 3) { - final Message firstLocationMessage = getFirstLocationMessage(messages); - if (firstLocationMessage != null) { - final PendingIntent pendingShowLocationIntent = - createShowLocationIntent(firstLocationMessage); - if (pendingShowLocationIntent != null) { - final String label = - mXmppConnectionService - .getResources() - .getString(R.string.show_location); - NotificationCompat.Action locationAction = - new NotificationCompat.Action.Builder( - R.drawable.ic_room_white_24dp, - label, - pendingShowLocationIntent) - .build(); - mBuilder.addAction(locationAction); - ++addedActionsCount; - } - } - } - if (addedActionsCount < 3) { - Message firstDownloadableMessage = getFirstDownloadableMessage(messages); - if (firstDownloadableMessage != null) { - String label = + if (displaySnoozeAction(messages)) { + String label = mXmppConnectionService.getString(R.string.snooze); + PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation); + NotificationCompat.Action snoozeAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_notifications_paused_white_24dp, + label, + pendingSnoozeIntent) + .build(); + notificationBuilder.addAction(snoozeAction); + ++addedActionsCount; + } + if (addedActionsCount < 3) { + final Message firstLocationMessage = getFirstLocationMessage(messages); + if (firstLocationMessage != null) { + final PendingIntent pendingShowLocationIntent = + createShowLocationIntent(firstLocationMessage); + if (pendingShowLocationIntent != null) { + final String label = mXmppConnectionService .getResources() - .getString( - R.string.download_x_file, - UIHelper.getFileDescriptionString( - mXmppConnectionService, - firstDownloadableMessage)); - PendingIntent pendingDownloadIntent = - createDownloadIntent(firstDownloadableMessage); - NotificationCompat.Action downloadAction = + .getString(R.string.show_location); + NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder( - R.drawable.ic_file_download_white_24dp, + R.drawable.ic_room_white_24dp, label, - pendingDownloadIntent) + pendingShowLocationIntent) .build(); - mBuilder.addAction(downloadAction); + notificationBuilder.addAction(locationAction); ++addedActionsCount; } } } - final ShortcutInfoCompat info; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - final Contact contact = conversation.getContact(); - final Uri systemAccount = contact.getSystemAccount(); - if (systemAccount != null) { - mBuilder.addPerson(systemAccount.toString()); + if (addedActionsCount < 3) { + Message firstDownloadableMessage = getFirstDownloadableMessage(messages); + if (firstDownloadableMessage != null) { + String label = + mXmppConnectionService + .getResources() + .getString( + R.string.download_x_file, + UIHelper.getFileDescriptionString( + mXmppConnectionService, + firstDownloadableMessage)); + PendingIntent pendingDownloadIntent = + createDownloadIntent(firstDownloadableMessage); + NotificationCompat.Action downloadAction = + new NotificationCompat.Action.Builder( + R.drawable.ic_file_download_white_24dp, + label, + pendingDownloadIntent) + .build(); + notificationBuilder.addAction(downloadAction); + ++addedActionsCount; } - info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact); - } else { - info = - mXmppConnectionService - .getShortcutService() - .getShortcutInfoCompat(conversation.getMucOptions()); } - mBuilder.setWhen(conversation.getLatestMessage().getTimeSent()); - mBuilder.setSmallIcon(R.drawable.ic_notification); - mBuilder.setDeleteIntent(createDeleteIntent(conversation)); - mBuilder.setContentIntent(createContentIntent(conversation)); - mBuilder.setShortcutInfo(info); + } + final ShortcutInfoCompat info; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + final Contact contact = conversation.getContact(); + final Uri systemAccount = contact.getSystemAccount(); + if (systemAccount != null) { + notificationBuilder.addPerson(systemAccount.toString()); + } + info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact); + } else { + info = + mXmppConnectionService + .getShortcutService() + .getShortcutInfoCompat(conversation.getMucOptions()); + } + notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent()); + notificationBuilder.setSmallIcon(R.drawable.ic_notification); + notificationBuilder.setDeleteIntent(createDeleteIntent(conversation)); + notificationBuilder.setContentIntent(createContentIntent(conversation)); + if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) { + // when do not want 'customized' notifications for silent notifications in their + // respective channels + notificationBuilder.setShortcutInfo(info); if (Build.VERSION.SDK_INT >= 30) { mXmppConnectionService .getSystemService(ShortcutManager.class) .pushDynamicShortcut(info.toShortcutInfo()); } } - return mBuilder; + return notificationBuilder; } private void modifyForImage( From 59c23f5558cbd2c7240a669f5b1f7ae2638c2496 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Feb 2024 09:11:49 +0100 Subject: [PATCH 078/556] add ability to display privacy policy link in menu --- build.gradle | 4 +++- .../ui/ConversationsOverviewFragment.java | 22 +++++++++++++------ .../ui/StartConversationActivity.java | 11 +++++++--- .../siacs/conversations/ui/XmppActivity.java | 18 +++++++++++++++ .../conversations/ui/text/FixedURLSpan.java | 4 +--- .../menu/fragment_conversations_overview.xml | 5 +++++ src/main/res/menu/start_conversation.xml | 22 ++++++++++++------- src/main/res/values/strings.xml | 1 + 8 files changed, 65 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 51e51fc85..60ba30519 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42090 + versionCode 42091 versionName "2.13.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" @@ -105,6 +105,7 @@ android { def appName = "Conversations" resValue "string", "app_name", appName buildConfigField "String", "APP_NAME", "\"$appName\"" + buildConfigField "String", "PRIVACY_POLICY", "null" } splits { @@ -144,6 +145,7 @@ android { def appName = "Quicksy" resValue "string", "app_name", appName buildConfigField "String", "APP_NAME", "\"$appName\"" + buildConfigField "String", "PRIVACY_POLICY", "\"https://quicksy.im/privacy.htm\"" } conversations { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index eebd94df5..b673260ad 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -57,12 +57,14 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.ui.interfaces.OnConversationArchived; import eu.siacs.conversations.ui.interfaces.OnConversationSelected; @@ -304,13 +306,19 @@ public class ConversationsOverviewFragment extends XmppFragment { return binding.getRoot(); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - menuInflater.inflate(R.menu.fragment_conversations_overview, menu); - AccountUtils.showHideMenuItems(menu); - final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite); - easyOnboardInvite.setVisible(EasyOnboardingInvite.anyHasSupport(activity == null ? null : activity.xmppConnectionService)); - } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + menuInflater.inflate(R.menu.fragment_conversations_overview, menu); + AccountUtils.showHideMenuItems(menu); + final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite); + easyOnboardInvite.setVisible( + EasyOnboardingInvite.anyHasSupport( + activity == null ? null : activity.xmppConnectionService)); + final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy); + privacyPolicyMenuItem.setVisible( + BuildConfig.PRIVACY_POLICY != null + && QuickConversationsService.isPlayStoreFlavor()); + } @Override public void onBackendConnected() { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index e3850e527..dfdc8be19 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -62,6 +62,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityStartConversationBinding; @@ -658,11 +659,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.start_conversation, menu); AccountUtils.showHideMenuItems(menu); - MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline); - MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code); + final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline); + final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code); + final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy); + privacyPolicyMenuItem.setVisible( + BuildConfig.PRIVACY_POLICY != null + && QuickConversationsService.isPlayStoreFlavor()); qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable()); if (QuickConversationsService.isQuicksy()) { menuHideOffline.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 8b555f1b9..9190697a8 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -56,6 +56,7 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Strings; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -422,6 +423,9 @@ public abstract class XmppActivity extends ActionBarActivity { case R.id.action_settings: startActivity(new Intent(this, SettingsActivity.class)); break; + case R.id.action_privacy_policy: + openPrivacyPolicy(); + break; case R.id.action_accounts: AccountUtils.launchManageAccounts(this); break; @@ -438,6 +442,20 @@ public abstract class XmppActivity extends ActionBarActivity { return super.onOptionsItemSelected(item); } + private void openPrivacyPolicy() { + if (BuildConfig.PRIVACY_POLICY == null) { + return; + } + final var viewPolicyIntent = new Intent(Intent.ACTION_VIEW); + viewPolicyIntent.setData(Uri.parse(BuildConfig.PRIVACY_POLICY)); + try { + startActivity(viewPolicyIntent); + } catch (final ActivityNotFoundException e) { + Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT) + .show(); + } + } + public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { final Contact contact = conversation.getContact(); if (contact.showInRoster() || contact.isSelf()) { 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 684fd8094..a5fba62a7 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -75,9 +75,7 @@ public class FixedURLSpan extends URLSpan { } } final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); //intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); try { context.startActivity(intent); diff --git a/src/main/res/menu/fragment_conversations_overview.xml b/src/main/res/menu/fragment_conversations_overview.xml index 38c83df8f..431e25bbd 100644 --- a/src/main/res/menu/fragment_conversations_overview.xml +++ b/src/main/res/menu/fragment_conversations_overview.xml @@ -49,6 +49,11 @@ android:orderInCategory="90" android:title="@string/action_account" app:showAsAction="never" /> +

+ xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="collapseActionView|always" /> + android:title="@string/scan_qr_code" + app:showAsAction="always" /> + app:showAsAction="never" /> + app:showAsAction="never" /> + app:showAsAction="never" /> + + app:showAsAction="never" /> \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 18a29610a..b644fbdd4 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1023,4 +1023,5 @@ You are using unverified devices. Scan the QR Code on your other devices to perform verification and impede active MITM attacks. Report spam Report spam and block spammer + Privacy policy From 33c63fb5627487b8a6b234ddff0b0475b5b1a8c1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Feb 2024 12:09:08 +0100 Subject: [PATCH 079/556] fixup: access Android_id only on push --- src/main/java/eu/siacs/conversations/utils/PhoneHelper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index edb181f4f..d76be537b 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -18,7 +18,8 @@ public class PhoneHelper { } public static Uri getProfilePictureUri(final Context context) { - if (context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { return null; } final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; From 85984627370e48af55d77159fe7f4b332ab28482 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 14 Feb 2024 13:53:46 +0100 Subject: [PATCH 080/556] remove address book integration from Conversations on PlayStore --- build.gradle | 2 +- src/free/AndroidManifest.xml | 6 ++ src/main/AndroidManifest.xml | 2 - .../android/JabberIdContact.java | 61 +++++++++++++------ .../AbstractQuickConversationsService.java | 4 ++ .../services/XmppConnectionService.java | 6 +- .../ui/ContactDetailsActivity.java | 39 ++++++++---- .../ui/StartConversationActivity.java | 8 ++- .../conversations/utils/PhoneHelper.java | 8 ++- src/main/res/values/strings.xml | 1 + src/quicksy/AndroidManifest.xml | 3 + 11 files changed, 100 insertions(+), 40 deletions(-) create mode 100644 src/free/AndroidManifest.xml diff --git a/build.gradle b/build.gradle index 60ba30519..20956240a 100644 --- a/build.gradle +++ b/build.gradle @@ -105,7 +105,7 @@ android { def appName = "Conversations" resValue "string", "app_name", appName buildConfigField "String", "APP_NAME", "\"$appName\"" - buildConfigField "String", "PRIVACY_POLICY", "null" + buildConfigField "String", "PRIVACY_POLICY", "\"https://conversations.im/privacy.html\"" } splits { diff --git a/src/free/AndroidManifest.xml b/src/free/AndroidManifest.xml new file mode 100644 index 000000000..b127401a9 --- /dev/null +++ b/src/free/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f9c3da16c..c01009862 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -5,8 +5,6 @@ - - diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 0b701d27a..1cb4dfa71 100644 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java @@ -8,28 +8,39 @@ import android.os.Build; import android.provider.ContactsContract; import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.xmpp.Jid; + import java.util.Collections; import java.util.HashMap; import java.util.Map; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.Jid; - public class JabberIdContact extends AbstractPhoneContact { - private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.Data.PHOTO_URI, - ContactsContract.Data.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Im.DATA - }; - private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))"; + private static final String[] PROJECTION = + new String[] { + ContactsContract.Data._ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.PHOTO_URI, + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Im.DATA + }; + private static final String SELECTION = + ContactsContract.Data.MIMETYPE + + "=? AND (" + + ContactsContract.CommonDataKinds.Im.PROTOCOL + + "=? or (" + + ContactsContract.CommonDataKinds.Im.PROTOCOL + + "=? and lower(" + + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + + ")=?))"; private static final String[] SELECTION_ARGS = { - ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE, - String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER), - String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM), - "xmpp" + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE, + String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER), + String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM), + "xmpp" }; private final Jid jid; @@ -37,8 +48,12 @@ public class JabberIdContact extends AbstractPhoneContact { private JabberIdContact(Cursor cursor) throws IllegalArgumentException { super(cursor); try { - this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); - } catch (IllegalArgumentException | NullPointerException e) { + this.jid = + Jid.of( + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Im.DATA))); + } catch (final IllegalArgumentException | NullPointerException e) { throw new IllegalArgumentException(e); } } @@ -48,10 +63,20 @@ public class JabberIdContact extends AbstractPhoneContact { } public static Map load(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + if (!QuickConversationsService.isFreeOrQuicksyFlavor() + || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED)) { return Collections.emptyMap(); } - try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) { + try (final Cursor cursor = + context.getContentResolver() + .query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + SELECTION, + SELECTION_ARGS, + null)) { if (cursor == null) { return Collections.emptyMap(); } diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index ea883683a..47cb567d7 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java @@ -30,6 +30,10 @@ public abstract class AbstractQuickConversationsService { return "playstore".equals(BuildConfig.FLAVOR_distribution); } + public static boolean isFreeOrQuicksyFlavor() { + return "free".equals(BuildConfig.FLAVOR_distribution) || "quicksy".equals(BuildConfig.FLAVOR_mode); + } + public static boolean isQuicksyPlayStore() { return isQuicksy() && isPlayStoreFlavor(); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0a3e9059b..ec6d93cec 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1290,7 +1290,11 @@ public class XmppConnectionService extends Service { restoreFromDatabase(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + if (QuickConversationsService.isFreeOrQuicksyFlavor() + && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || ContextCompat.checkSelfPermission( + this, Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED)) { startContactObserver(); } FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index f1556df6e..6c4134b9f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -46,6 +46,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.services.AbstractQuickConversationsService; +import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.MediaAdapter; @@ -119,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp private void checkContactPermissionAndShowAddDialog() { if (hasContactsPermission()) { showAddToPhoneBookDialog(); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + } else if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); } } private boolean hasContactsPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; } else { return true; @@ -523,18 +524,30 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } } - private void onBadgeClick(View view) { - final Uri systemAccount = contact.getSystemAccount(); - if (systemAccount == null) { - checkContactPermissionAndShowAddDialog(); - } else { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(systemAccount); - try { - startActivity(intent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show(); + private void onBadgeClick(final View view) { + if (QuickConversationsService.isFreeOrQuicksyFlavor()) { + final Uri systemAccount = contact.getSystemAccount(); + if (systemAccount == null) { + checkContactPermissionAndShowAddDialog(); + } else { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(systemAccount); + try { + startActivity(intent); + } catch (final ActivityNotFoundException e) { + Toast.makeText( + this, + R.string.no_application_found_to_view_contact, + Toast.LENGTH_SHORT) + .show(); + } } + } else { + Toast.makeText( + this, + R.string.contact_list_integration_not_available, + Toast.LENGTH_SHORT) + .show(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index dfdc8be19..18c1fb892 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -761,7 +761,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private void askForContactsPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { if (mRequestedContactsPermission.compareAndSet(false, true)) { if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { @@ -840,8 +840,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override protected void onBackendConnected() { - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + if (QuickConversationsService.isFreeOrQuicksyFlavor() + && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || checkSelfPermission(Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED)) { xmppConnectionService.getQuickConversationsService().considerSyncBackground(false); } if (mPostponedActivityResult != null) { diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index d76be537b..f6849e6d1 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -10,6 +10,8 @@ import android.os.Build; import android.provider.ContactsContract.Profile; import android.provider.Settings; +import eu.siacs.conversations.services.QuickConversationsService; + public class PhoneHelper { @SuppressLint("HardwareIds") @@ -18,8 +20,10 @@ public class PhoneHelper { } public static Uri getProfilePictureUri(final Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + if (!QuickConversationsService.isFreeOrQuicksyFlavor() + || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED)) { return null; } final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index b644fbdd4..0f8a16b46 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1024,4 +1024,5 @@ Report spam Report spam and block spammer Privacy policy + Address book integration is not available diff --git a/src/quicksy/AndroidManifest.xml b/src/quicksy/AndroidManifest.xml index 7b03ed1b7..78f60cebe 100644 --- a/src/quicksy/AndroidManifest.xml +++ b/src/quicksy/AndroidManifest.xml @@ -6,6 +6,9 @@ android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" /> + + + Date: Thu, 15 Feb 2024 08:39:00 +0100 Subject: [PATCH 081/556] version bump to 2.13.3 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4209204.txt | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4209204.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ea87a3906..45e50d4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.13.3 + +* Provide easier access to 'Privacy Policy' on Play Store version (Quicksy and Conversations) +* Remove address book integration on Play Store version of Conversations + ### Version 2.13.2 * minor bug fixes diff --git a/build.gradle b/build.gradle index 20956240a..58b46cd94 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42091 - versionName "2.13.2" + versionCode 42092 + versionName "2.13.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4209204.txt b/fastlane/metadata/android/en-US/changelogs/4209204.txt new file mode 100644 index 000000000..83a947d54 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Provide easier access to 'Privacy Policy' on Play Store version (Quicksy and Conversations) +* Remove address book integration on Play Store version of Conversations From dc88c3a5d6ad17aebb1f69c3b74b7fe8f837467d Mon Sep 17 00:00:00 2001 From: ghose Date: Tue, 13 Feb 2024 05:59:02 +0000 Subject: [PATCH 082/556] Translated using Weblate (Galician) Currently translated at 100.0% (980 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 88d143770..4167acbc3 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -39,7 +39,7 @@ O alcume xa está en uso Alcume non válido Admin - Dono + Dona Moderador Participante Visitante @@ -300,7 +300,7 @@ Poñer marca de \"autojoin\" ao entrar ou deixar unha MUC e reaccionar ás modificacións feitas desde outros clientes. Copiouse a impresión dixital OMEMO ao portapapeis Non podes acceder a esta conversa en grupo - Esta conversa en grupo é so para membros + Este chat en grupo é so para membros Restrición do recurso Xa foi expulsado de esta conversa en grupo A conversa en grupo foi apagada @@ -750,7 +750,7 @@ Na pantalla \'Iniciar Conversa\' abrir teclado e pór o cursor no campo de busca Avatar da conversa de grupo O servidor non soporta o avatar na conversa de grupo - Só o dono pode cambiar o avatar da conversa de grupo + Só a propietaria pode cambiar o avatar da conversa Nome do contacto Alcume Nome From cf8a075529d3b4df7d3a7f45a50f4b857a14a9ea Mon Sep 17 00:00:00 2001 From: 0que <0que@users.noreply.translate.codeberg.org> Date: Wed, 14 Feb 2024 10:16:52 +0000 Subject: [PATCH 083/556] Translated using Weblate (Russian) Currently translated at 99.6% (977 of 980 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index a880096ed..8d60c8a6b 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -316,8 +316,8 @@ XMPP-адрес скопирован в буфер обмена Сообщение об ошибке скопировано в буфер обмена веб-адрес - Сканировать 2D штрихкод - Показать 2D штрихкод + Сканировать QR-код + Показать QR-код Показать чёрный список Сведения об учётной записи Подтвердить @@ -616,7 +616,7 @@ Автоматически доверять всем новым устройствам контактов, которые не были подтверждены ранее, но запрашивать ручное подтверждение каждый раз, когда подтвержденный контакт добавляет новое устройство. Принятие OMEMO-ключей вслепую. Это означает, что собеседник может оказаться недоверенным лицом. Недоверенный - Некорректный 2D штрихкод + Некорректный QR-код Очистить кэш (используется камерой) Очистить кэш Очистить приватное хранилище @@ -1045,10 +1045,10 @@ Вы вышли из этой учётной записи Войти Скрыть уведомление - Ваш контакт использует неподтверждённые устройства. Отсканируйте его штрих-код для проверки и предотвращения атаки посредника. + Ваш контакт использует неподтверждённые устройства. Отсканируйте его QR-код для проверки и предотвращения атаки посредника. Выйти Деавторизован - Вы используете неподтверждённые устройства. Отсканируйте штрих-код на подтверждённом устройстве для проверки и предотвращения атаки посредника. + Вы используете неподтверждённые устройства. Отсканируйте QR-код на подтверждённом устройстве для проверки и предотвращения атаки посредника. Пожаловаться на спам и заблокировать Пожаловаться на спам \ No newline at end of file From 6cc2f639df6c84740522c16949c795da046dfee9 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Thu, 15 Feb 2024 12:18:29 +0000 Subject: [PATCH 084/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index aba3ad109..75313cb41 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1078,4 +1078,6 @@ Повідомити про спам Вітаємо у Quicksy! Quicksy просить згоду на використання Ваших даних + Політика конфіденційності + Інтеграція адресної книги недоступна \ No newline at end of file From 06adbf18c8dc32f044b7364341431906d84db463 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Thu, 15 Feb 2024 12:58:50 +0000 Subject: [PATCH 085/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index c14afe1f1..216c355d8 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1036,4 +1036,6 @@ 报告垃圾消息 欢迎使用 Quicksy! Quicksy 请求您同意使用您的数据 + 隐私政策 + 通讯录集成不可用 \ No newline at end of file From d66eb147135df7184d4ee9eb7313fcd410dae0bf Mon Sep 17 00:00:00 2001 From: SomeTr Date: Thu, 15 Feb 2024 12:26:20 +0000 Subject: [PATCH 086/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (56 of 56 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4209204.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4209204.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209204.txt b/fastlane/metadata/android/uk/changelogs/4209204.txt new file mode 100644 index 000000000..652ea89d8 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Простіший доступ до «Політики конфіденційності» у версії Play Store (Quicksy та Conversations) +* Видалено інтеграцію адресної книги у версії Conversations для Play Store From 5692cccf70299ed050e14289aedb28a1fcfedaad Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Thu, 15 Feb 2024 13:14:07 +0000 Subject: [PATCH 087/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (56 of 56 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4209204.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4209204.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4209204.txt b/fastlane/metadata/android/zh-CN/changelogs/4209204.txt new file mode 100644 index 000000000..37c544ff2 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* 在 Play 商店版本(Quicksy 和 Conversations)上提供对“隐私政策”的更轻松访问 +* 移除 Play 商店版本的 Conversations 上的通讯录集成 From d175843cbda3f62a03e2cb0a3898dce2097c4c6f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 16 Feb 2024 16:59:21 +0100 Subject: [PATCH 088/556] ignore 'subscribe' presence for blocked contacts --- .../conversations/parser/PresenceParser.java | 738 ++++++++++-------- 1 file changed, 395 insertions(+), 343 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 8ad582b17..584b8e704 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -2,11 +2,6 @@ package eu.siacs.conversations.parser; import android.util.Log; -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.ArrayList; -import java.util.List; - import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -28,123 +23,159 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -public class PresenceParser extends AbstractParser implements - OnPresencePacketReceived { +import org.openintents.openpgp.util.OpenPgpUtils; - public PresenceParser(XmppConnectionService service) { - super(service); - } +import java.util.ArrayList; +import java.util.List; - public void parseConferencePresence(PresencePacket packet, Account account) { - final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().asBareJid()); - if (conversation != null) { - final MucOptions mucOptions = conversation.getMucOptions(); - boolean before = mucOptions.online(); - int count = mucOptions.getUserCount(); - final List tileUserBefore = mucOptions.getUsers(5); - processConferencePresence(packet, conversation); - final List tileUserAfter = mucOptions.getUsers(5); - if (!tileUserAfter.equals(tileUserBefore)) { - mXmppConnectionService.getAvatarService().clear(mucOptions); - } - if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) { - mXmppConnectionService.updateConversationUi(); - } else if (mucOptions.online()) { - mXmppConnectionService.updateMucRosterUi(); - } - } - } +public class PresenceParser extends AbstractParser implements OnPresencePacketReceived { - private void processConferencePresence(PresencePacket packet, Conversation conversation) { - final Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); - final Jid jid = conversation.getAccount().getJid(); - final Jid from = packet.getFrom(); - if (!from.isBareJid()) { - final String type = packet.getAttribute("type"); - final Element x = packet.findChild("x", Namespace.MUC_USER); - Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - final List codes = getStatusCodes(x); - if (type == null) { - if (x != null) { - Element item = x.findChild("item"); - if (item != null && !from.isBareJid()) { - mucOptions.setError(MucOptions.Error.NONE); - MucOptions.User user = parseItem(conversation, item, from); - if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) { - if (mucOptions.setOnline()) { - mXmppConnectionService.getAvatarService().clear(mucOptions); - } - if (mucOptions.setSelf(user)) { - Log.d(Config.LOGTAG,"role or affiliation changed"); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - } + public PresenceParser(XmppConnectionService service) { + super(service); + } - mXmppConnectionService.persistSelfNick(user); - invokeRenameListener(mucOptions, true); - } - boolean isNew = mucOptions.updateUser(user); - final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - Contact contact = user.getContact(); - if (isNew - && user.getRealJid() != null - && mucOptions.isPrivateAndNonAnonymous() - && (contact == null || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList(user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); - } - if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid() - +": room '" - +mucOptions.getConversation().getJid().asBareJid() - +"' created. pushing default configuration"); - mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(), - IqGenerator.defaultChannelConfiguration(), - null); - } - if (mXmppConnectionService.getPgpEngine() != null) { - Element signed = packet.findChild("x", "jabber:x:signed"); - if (signed != null) { - Element status = packet.findChild("status"); - String msg = status == null ? "" : status.getContent(); - long keyId = mXmppConnectionService.getPgpEngine().fetchKeyId(mucOptions.getAccount(), msg, signed.getContent()); - if (keyId != 0) { - user.setPgpKeyId(keyId); - } - } - } - if (avatar != null) { - avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (user.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(user); - } - if (user.getRealJid() != null) { - final Contact c = conversation.getAccount().getRoster().getContact(user.getRealJid()); - c.setAvatar(avatar); - mXmppConnectionService.syncRoster(conversation.getAccount()); - mXmppConnectionService.getAvatarService().clear(c); - mXmppConnectionService.updateRosterUi(); - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar); - } - } - } - } - } else if (type.equals("unavailable")) { - final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid()); - if (x.hasChild("destroy") && fullJidMatches) { - Element destroy = x.findChild("destroy"); - final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid")); - mucOptions.setError(MucOptions.Error.DESTROYED); - if (alternate != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); - } - } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { - mucOptions.setError(MucOptions.Error.SHUTDOWN); - } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { - if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { + public void parseConferencePresence(PresencePacket packet, Account account) { + final Conversation conversation = + packet.getFrom() == null + ? null + : mXmppConnectionService.find(account, packet.getFrom().asBareJid()); + if (conversation != null) { + final MucOptions mucOptions = conversation.getMucOptions(); + boolean before = mucOptions.online(); + int count = mucOptions.getUserCount(); + final List tileUserBefore = mucOptions.getUsers(5); + processConferencePresence(packet, conversation); + final List tileUserAfter = mucOptions.getUsers(5); + if (!tileUserAfter.equals(tileUserBefore)) { + mXmppConnectionService.getAvatarService().clear(mucOptions); + } + if (before != mucOptions.online() + || (mucOptions.online() && count != mucOptions.getUserCount())) { + mXmppConnectionService.updateConversationUi(); + } else if (mucOptions.online()) { + mXmppConnectionService.updateMucRosterUi(); + } + } + } + + private void processConferencePresence(PresencePacket packet, Conversation conversation) { + final Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); + final Jid jid = conversation.getAccount().getJid(); + final Jid from = packet.getFrom(); + if (!from.isBareJid()) { + final String type = packet.getAttribute("type"); + final Element x = packet.findChild("x", Namespace.MUC_USER); + Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); + final List codes = getStatusCodes(x); + if (type == null) { + if (x != null) { + Element item = x.findChild("item"); + if (item != null && !from.isBareJid()) { + mucOptions.setError(MucOptions.Error.NONE); + MucOptions.User user = parseItem(conversation, item, from); + if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) + || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) + && jid.equals( + InvalidJid.getNullForInvalid( + item.getAttributeAsJid("jid"))))) { + if (mucOptions.setOnline()) { + mXmppConnectionService.getAvatarService().clear(mucOptions); + } + if (mucOptions.setSelf(user)) { + Log.d(Config.LOGTAG, "role or affiliation changed"); + mXmppConnectionService.databaseBackend.updateConversation( + conversation); + } + + mXmppConnectionService.persistSelfNick(user); + invokeRenameListener(mucOptions, true); + } + boolean isNew = mucOptions.updateUser(user); + final AxolotlService axolotlService = + conversation.getAccount().getAxolotlService(); + Contact contact = user.getContact(); + if (isNew + && user.getRealJid() != null + && mucOptions.isPrivateAndNonAnonymous() + && (contact == null || !contact.mutualPresenceSubscription()) + && axolotlService.hasEmptyDeviceList(user.getRealJid())) { + axolotlService.fetchDeviceIds(user.getRealJid()); + } + if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) + && mucOptions.autoPushConfiguration()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": room '" + + mucOptions.getConversation().getJid().asBareJid() + + "' created. pushing default configuration"); + mXmppConnectionService.pushConferenceConfiguration( + mucOptions.getConversation(), + IqGenerator.defaultChannelConfiguration(), + null); + } + if (mXmppConnectionService.getPgpEngine() != null) { + Element signed = packet.findChild("x", "jabber:x:signed"); + if (signed != null) { + Element status = packet.findChild("status"); + String msg = status == null ? "" : status.getContent(); + long keyId = + mXmppConnectionService + .getPgpEngine() + .fetchKeyId( + mucOptions.getAccount(), + msg, + signed.getContent()); + if (keyId != 0) { + user.setPgpKeyId(keyId); + } + } + } + if (avatar != null) { + avatar.owner = from; + if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (user.setAvatar(avatar)) { + mXmppConnectionService.getAvatarService().clear(user); + } + if (user.getRealJid() != null) { + final Contact c = + conversation + .getAccount() + .getRoster() + .getContact(user.getRealJid()); + c.setAvatar(avatar); + mXmppConnectionService.syncRoster(conversation.getAccount()); + mXmppConnectionService.getAvatarService().clear(c); + mXmppConnectionService.updateRosterUi(); + } + } else if (mXmppConnectionService.isDataSaverDisabled()) { + mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar); + } + } + } + } + } else if (type.equals("unavailable")) { + final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid()); + if (x.hasChild("destroy") && fullJidMatches) { + Element destroy = x.findChild("destroy"); + final Jid alternate = + destroy == null + ? null + : InvalidJid.getNullForInvalid( + destroy.getAttributeAsJid("jid")); + mucOptions.setError(MucOptions.Error.DESTROYED); + if (alternate != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": muc destroyed. alternate location " + + alternate); + } + } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { + mucOptions.setError(MucOptions.Error.SHUTDOWN); + } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { + if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { final boolean wasOnline = mucOptions.online(); mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS); Log.d( @@ -157,238 +188,259 @@ public class PresenceParser extends AbstractParser implements if (wasOnline) { mXmppConnectionService.mucSelfPingAndRejoin(conversation); } - } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { - mucOptions.setError(MucOptions.Error.KICKED); - } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { - mucOptions.setError(MucOptions.Error.BANNED); - } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) { - mucOptions.setError(MucOptions.Error.SHUTDOWN); - } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) { - mucOptions.setError(MucOptions.Error.UNKNOWN); - Log.d(Config.LOGTAG, "unknown error in conference: " + packet); - } - } else if (!from.isBareJid()){ - Element item = x.findChild("item"); - if (item != null) { - mucOptions.updateUser(parseItem(conversation, item, from)); - } - MucOptions.User user = mucOptions.deleteUser(from); - if (user != null) { - mXmppConnectionService.getAvatarService().clear(user); - } - } - } else if (type.equals("error")) { - final Element error = packet.findChild("error"); - if (error == null) { - return; - } - if (error.hasChild("conflict")) { - if (mucOptions.online()) { - invokeRenameListener(mucOptions, false); - } else { - mucOptions.setError(MucOptions.Error.NICK_IN_USE); - } - } else if (error.hasChild("not-authorized")) { - mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED); - } else if (error.hasChild("forbidden")) { - mucOptions.setError(MucOptions.Error.BANNED); - } else if (error.hasChild("registration-required")) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (error.hasChild("resource-constraint")) { - mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT); - } else if (error.hasChild("remote-server-timeout")) { - mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT); - } else if (error.hasChild("gone")) { - final String gone = error.findChildContent("gone"); - final Jid alternate; - if (gone != null) { - final XmppUri xmppUri = new XmppUri(gone); - if (xmppUri.isValidJid()) { - alternate = xmppUri.getJid(); - } else { - alternate = null; - } - } else { - alternate = null; - } - mucOptions.setError(MucOptions.Error.DESTROYED); - if (alternate != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); - } - } else { - final String text = error.findChildContent("text"); - if (text != null && text.contains("attribute 'to'")) { - if (mucOptions.online()) { - invokeRenameListener(mucOptions, false); - } else { - mucOptions.setError(MucOptions.Error.INVALID_NICK); - } - } else { - mucOptions.setError(MucOptions.Error.UNKNOWN); - Log.d(Config.LOGTAG, "unknown error in conference: " + packet); - } - } - } - } - } + } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { + mucOptions.setError(MucOptions.Error.KICKED); + } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { + mucOptions.setError(MucOptions.Error.BANNED); + } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) { + mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); + } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) { + mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); + } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) { + mucOptions.setError(MucOptions.Error.SHUTDOWN); + } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) { + mucOptions.setError(MucOptions.Error.UNKNOWN); + Log.d(Config.LOGTAG, "unknown error in conference: " + packet); + } + } else if (!from.isBareJid()) { + Element item = x.findChild("item"); + if (item != null) { + mucOptions.updateUser(parseItem(conversation, item, from)); + } + MucOptions.User user = mucOptions.deleteUser(from); + if (user != null) { + mXmppConnectionService.getAvatarService().clear(user); + } + } + } else if (type.equals("error")) { + final Element error = packet.findChild("error"); + if (error == null) { + return; + } + if (error.hasChild("conflict")) { + if (mucOptions.online()) { + invokeRenameListener(mucOptions, false); + } else { + mucOptions.setError(MucOptions.Error.NICK_IN_USE); + } + } else if (error.hasChild("not-authorized")) { + mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED); + } else if (error.hasChild("forbidden")) { + mucOptions.setError(MucOptions.Error.BANNED); + } else if (error.hasChild("registration-required")) { + mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); + } else if (error.hasChild("resource-constraint")) { + mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT); + } else if (error.hasChild("remote-server-timeout")) { + mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT); + } else if (error.hasChild("gone")) { + final String gone = error.findChildContent("gone"); + final Jid alternate; + if (gone != null) { + final XmppUri xmppUri = new XmppUri(gone); + if (xmppUri.isValidJid()) { + alternate = xmppUri.getJid(); + } else { + alternate = null; + } + } else { + alternate = null; + } + mucOptions.setError(MucOptions.Error.DESTROYED); + if (alternate != null) { + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": muc destroyed. alternate location " + + alternate); + } + } else { + final String text = error.findChildContent("text"); + if (text != null && text.contains("attribute 'to'")) { + if (mucOptions.online()) { + invokeRenameListener(mucOptions, false); + } else { + mucOptions.setError(MucOptions.Error.INVALID_NICK); + } + } else { + mucOptions.setError(MucOptions.Error.UNKNOWN); + Log.d(Config.LOGTAG, "unknown error in conference: " + packet); + } + } + } + } + } - private static void invokeRenameListener(final MucOptions options, boolean success) { - if (options.onRenameListener != null) { - if (success) { - options.onRenameListener.onSuccess(); - } else { - options.onRenameListener.onFailure(); - } - options.onRenameListener = null; - } - } + private static void invokeRenameListener(final MucOptions options, boolean success) { + if (options.onRenameListener != null) { + if (success) { + options.onRenameListener.onSuccess(); + } else { + options.onRenameListener.onFailure(); + } + options.onRenameListener = null; + } + } - private static List getStatusCodes(Element x) { - List codes = new ArrayList<>(); - if (x != null) { - for (Element child : x.getChildren()) { - if (child.getName().equals("status")) { - String code = child.getAttribute("code"); - if (code != null) { - codes.add(code); - } - } - } - } - return codes; - } + private static List getStatusCodes(Element x) { + List codes = new ArrayList<>(); + if (x != null) { + for (Element child : x.getChildren()) { + if (child.getName().equals("status")) { + String code = child.getAttribute("code"); + if (code != null) { + codes.add(code); + } + } + } + } + return codes; + } - private void parseContactPresence(final PresencePacket packet, final Account account) { - final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); - final Jid from = packet.getFrom(); - if (from == null || from.equals(account.getJid())) { - return; - } - final String type = packet.getAttribute("type"); - final Contact contact = account.getRoster().getContact(from); - if (type == null) { - final String resource = from.isBareJid() ? "" : from.getResource(); - Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) { - avatar.owner = from.asBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (avatar.owner.equals(account.getJid().asBareJid())) { - account.setAvatar(avatar.getFilename()); - mXmppConnectionService.databaseBackend.updateAccount(account); - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } else { - contact.setAvatar(avatar); - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - } else if (mXmppConnectionService.isDataSaverDisabled()){ - mXmppConnectionService.fetchAvatar(account, avatar); - } - } + private void parseContactPresence(final PresencePacket packet, final Account account) { + final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); + final Jid from = packet.getFrom(); + if (from == null || from.equals(account.getJid())) { + return; + } + final String type = packet.getAttribute("type"); + final Contact contact = account.getRoster().getContact(from); + if (type == null) { + final String resource = from.isBareJid() ? "" : from.getResource(); + final Avatar avatar = + Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); + if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) { + avatar.owner = from.asBareJid(); + if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (avatar.owner.equals(account.getJid().asBareJid())) { + account.setAvatar(avatar.getFilename()); + mXmppConnectionService.databaseBackend.updateAccount(account); + mXmppConnectionService.getAvatarService().clear(account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); + } else { + contact.setAvatar(avatar); + mXmppConnectionService.syncRoster(account); + mXmppConnectionService.getAvatarService().clear(contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else if (mXmppConnectionService.isDataSaverDisabled()) { + mXmppConnectionService.fetchAvatar(account, avatar); + } + } - if (mXmppConnectionService.isMuc(account, from)) { - return; - } + if (mXmppConnectionService.isMuc(account, from)) { + return; + } - int sizeBefore = contact.getPresences().size(); + final int sizeBefore = contact.getPresences().size(); - final String show = packet.findChildContent("show"); - final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); - final String message = packet.findChildContent("status"); - final Presence presence = Presence.parse(show, caps, message); - contact.updatePresence(resource, presence); - if (presence.hasCaps()) { - mXmppConnectionService.fetchCaps(account, from, presence); - } + final String show = packet.findChildContent("show"); + final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); + final String message = packet.findChildContent("status"); + final Presence presence = Presence.parse(show, caps, message); + contact.updatePresence(resource, presence); + if (presence.hasCaps()) { + mXmppConnectionService.fetchCaps(account, from, presence); + } - final Element idle = packet.findChild("idle", Namespace.IDLE); - if (idle != null) { - try { - final String since = idle.getAttribute("since"); - contact.setLastseen(AbstractParser.parseTimestamp(since)); - contact.flagInactive(); - } catch (Throwable throwable) { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { - contact.flagActive(); - } - } - } else { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { - contact.flagActive(); - } - } + final Element idle = packet.findChild("idle", Namespace.IDLE); + if (idle != null) { + try { + final String since = idle.getAttribute("since"); + contact.setLastseen(AbstractParser.parseTimestamp(since)); + contact.flagInactive(); + } catch (Throwable throwable) { + if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { + contact.flagActive(); + } + } + } else { + if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { + contact.flagActive(); + } + } - PgpEngine pgp = mXmppConnectionService.getPgpEngine(); - Element x = packet.findChild("x", "jabber:x:signed"); - if (pgp != null && x != null) { - final String status = packet.findChildContent("status"); - final long keyId = pgp.fetchKeyId(account, status, x.getContent()); - if (keyId != 0 && contact.setPgpKeyId(keyId)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId)); - mXmppConnectionService.syncRoster(account); - } - } - boolean online = sizeBefore < contact.getPresences().size(); - mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); - } else if (type.equals("unavailable")) { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet,0L,true))) { - contact.flagInactive(); - } - if (from.isBareJid()) { - contact.clearPresences(); - } else { - contact.removePresence(from.getResource()); - } - if (contact.getShownStatus() == Presence.Status.OFFLINE) { - contact.flagInactive(); - } - mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false); - } else if (type.equals("subscribe")) { - if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) { - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - } - if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - mXmppConnectionService.sendPresencePacket(account, - mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } else { - contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, contact.getJid().asBareJid(), false, false); - final String statusMessage = packet.findChildContent("status"); - if (statusMessage != null - && !statusMessage.isEmpty() - && conversation.countMessages() == 0) { - conversation.add(new Message( - conversation, - statusMessage, - Message.ENCRYPTION_NONE, - Message.STATUS_RECEIVED - )); - } - } - } - mXmppConnectionService.updateRosterUi(); - } + final PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + final Element x = packet.findChild("x", "jabber:x:signed"); + if (pgp != null && x != null) { + final String status = packet.findChildContent("status"); + final long keyId = pgp.fetchKeyId(account, status, x.getContent()); + if (keyId != 0 && contact.setPgpKeyId(keyId)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": found OpenPGP key id for " + + contact.getJid() + + " " + + OpenPgpUtils.convertKeyIdToHex(keyId)); + mXmppConnectionService.syncRoster(account); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) { + contact.flagInactive(); + } + if (from.isBareJid()) { + contact.clearPresences(); + } else { + contact.removePresence(from.getResource()); + } + if (contact.getShownStatus() == Presence.Status.OFFLINE) { + contact.flagInactive(); + } + mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false); + } else if (type.equals("subscribe")) { + if (contact.isBlocked()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ignoring 'subscribe' presence from blocked " + + from); + return; + } + if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) { + mXmppConnectionService.syncRoster(account); + mXmppConnectionService.getAvatarService().clear(contact); + } + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresencePacket( + account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, contact.getJid().asBareJid(), false, false); + final String statusMessage = packet.findChildContent("status"); + if (statusMessage != null + && !statusMessage.isEmpty() + && conversation.countMessages() == 0) { + conversation.add( + new Message( + conversation, + statusMessage, + Message.ENCRYPTION_NONE, + Message.STATUS_RECEIVED)); + } + } + } + mXmppConnectionService.updateRosterUi(); + } - @Override - public void onPresencePacketReceived(Account account, PresencePacket packet) { - if (packet.hasChild("x", Namespace.MUC_USER)) { - this.parseConferencePresence(packet, account); - } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - this.parseConferencePresence(packet, account); - } else if ("error".equals(packet.getAttribute("type")) && mXmppConnectionService.isMuc(account, packet.getFrom())) { - this.parseConferencePresence(packet, account); - } else { - this.parseContactPresence(packet, account); - } - } + @Override + public void onPresencePacketReceived(Account account, PresencePacket packet) { + if (packet.hasChild("x", Namespace.MUC_USER)) { + this.parseConferencePresence(packet, account); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + this.parseConferencePresence(packet, account); + } else if ("error".equals(packet.getAttribute("type")) + && mXmppConnectionService.isMuc(account, packet.getFrom())) { + this.parseConferencePresence(packet, account); + } else { + this.parseContactPresence(packet, account); + } + } } From a62a7a4a8455371fde87f7cc785e66791281e223 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 18 Feb 2024 10:20:54 +0100 Subject: [PATCH 089/556] resolver results need to be editable if we inject see-other-host --- .../java/eu/siacs/conversations/utils/Resolver.java | 10 +++++----- .../eu/siacs/conversations/xmpp/XmppConnection.java | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 915209413..6746f3ea9 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -166,15 +166,15 @@ public class Resolver { threads[2].interrupt(); synchronized (results) { Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString()); - return new ArrayList<>(results); + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results); + return results; } } else { threads[2].join(); synchronized (fallbackResults) { Collections.sort(fallbackResults); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString()); - return new ArrayList<>(fallbackResults); + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults); + return fallbackResults; } } } catch (InterruptedException e) { @@ -257,7 +257,7 @@ public class Resolver { } private static List resolveNoSrvRecords(DNSName dnsName, boolean withCnames) { - List results = new ArrayList<>(); + final List results = new ArrayList<>(); try { for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) { results.add(Result.createDefault(dnsName, a.getInetAddress())); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 541935712..9141e1c70 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -338,12 +338,13 @@ public class XmppConnection implements Runnable { } } else { final String domain = account.getServer(); - final List results; + final List results = new ArrayList<>(); final boolean hardcoded = extended && !account.getHostname().isEmpty(); if (hardcoded) { - results = Resolver.fromHardCoded(account.getHostname(), account.getPort()); + results.addAll( + Resolver.fromHardCoded(account.getHostname(), account.getPort())); } else { - results = Resolver.resolve(domain); + results.addAll(Resolver.resolve(domain)); } if (Thread.currentThread().isInterrupted()) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); From 9b832e128565c996e79f5076dc1be754f1d9ea97 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 18 Feb 2024 17:38:32 +0100 Subject: [PATCH 090/556] null check PushTargetMessenger --- .../conversations/services/UnifiedPushBroker.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 2562e13a6..bfa1785f1 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -12,6 +12,7 @@ import android.preference.PreferenceManager; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Strings; @@ -85,24 +86,24 @@ public class UnifiedPushBroker { service.sendPresencePacket(account, presence); } - public Optional renewUnifiedPushEndpoints() { - return renewUnifiedPushEndpoints(null); + public void renewUnifiedPushEndpoints() { + renewUnifiedPushEndpoints(null); } - public Optional renewUnifiedPushEndpoints(final PushTargetMessenger pushTargetMessenger) { + public Optional renewUnifiedPushEndpoints(@Nullable final PushTargetMessenger pushTargetMessenger) { final Optional transportOptional = getTransport(); if (transportOptional.isPresent()) { final Transport transport = transportOptional.get(); if (transport.account.isEnabled()) { renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger); } else { - if (pushTargetMessenger.messenger != null) { + if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) { sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled"); } Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled"); } } else { - if (pushTargetMessenger.messenger != null) { + if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) { sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected"); } Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected"); From 8e73b7f47756ff4191b101ded543e0eb4ea114ad Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 19 Feb 2024 10:12:52 +0100 Subject: [PATCH 091/556] make contact integration depend on manifest, not flavor --- .../android/JabberIdContact.java | 4 +- .../AbstractQuickConversationsService.java | 39 ++++++- .../services/XmppConnectionService.java | 2 +- .../ui/ContactDetailsActivity.java | 6 +- .../ui/StartConversationActivity.java | 110 +++++++++++++----- .../conversations/utils/PhoneHelper.java | 28 ++--- src/main/res/values/strings.xml | 6 +- 7 files changed, 138 insertions(+), 57 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java index 1cb4dfa71..32d5a53e7 100644 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java @@ -62,8 +62,8 @@ public class JabberIdContact extends AbstractPhoneContact { return jid; } - public static Map load(Context context) { - if (!QuickConversationsService.isFreeOrQuicksyFlavor() + public static Map load(final Context context) { + if (!QuickConversationsService.isContactListIntegration(context) || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED)) { diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index 47cb567d7..8f6c3c1f2 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java @@ -1,14 +1,22 @@ package eu.siacs.conversations.services; +import android.Manifest; +import android.content.Context; import android.content.Intent; -import android.os.Build; +import android.content.pm.PackageManager; + +import com.google.common.collect.Iterables; import eu.siacs.conversations.BuildConfig; +import java.util.Arrays; + public abstract class AbstractQuickConversationsService { + public static final String SMS_RETRIEVED_ACTION = + "com.google.android.gms.auth.api.phone.SMS_RETRIEVED"; - public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED"; + private static Boolean declaredReadContacts = null; protected final XmppConnectionService service; @@ -30,8 +38,31 @@ public abstract class AbstractQuickConversationsService { return "playstore".equals(BuildConfig.FLAVOR_distribution); } - public static boolean isFreeOrQuicksyFlavor() { - return "free".equals(BuildConfig.FLAVOR_distribution) || "quicksy".equals(BuildConfig.FLAVOR_mode); + public static boolean isContactListIntegration(final Context context) { + if ("quicksy".equals(BuildConfig.FLAVOR_mode)) { + return true; + } + final var readContacts = AbstractQuickConversationsService.declaredReadContacts; + if (readContacts != null) { + return Boolean.TRUE.equals(readContacts); + } + AbstractQuickConversationsService.declaredReadContacts = hasDeclaredReadContacts(context); + return AbstractQuickConversationsService.declaredReadContacts; + } + + private static boolean hasDeclaredReadContacts(final Context context) { + final String[] permissions; + try { + permissions = + context.getPackageManager() + .getPackageInfo( + context.getPackageName(), PackageManager.GET_PERMISSIONS) + .requestedPermissions; + } catch (final PackageManager.NameNotFoundException e) { + return false; + } + return Iterables.any( + Arrays.asList(permissions), p -> p.equals(Manifest.permission.READ_CONTACTS)); } public static boolean isQuicksyPlayStore() { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ec6d93cec..584237156 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1290,7 +1290,7 @@ public class XmppConnectionService extends Service { restoreFromDatabase(); - if (QuickConversationsService.isFreeOrQuicksyFlavor() + if (QuickConversationsService.isContactListIntegration(this) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission( this, Manifest.permission.READ_CONTACTS) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 6c4134b9f..394331452 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -120,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp private void checkContactPermissionAndShowAddDialog() { if (hasContactsPermission()) { showAddToPhoneBookDialog(); - } else if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); } } private boolean hasContactsPermission() { - if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; } else { return true; @@ -525,7 +525,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } private void onBadgeClick(final View view) { - if (QuickConversationsService.isFreeOrQuicksyFlavor()) { + if (QuickConversationsService.isContactListIntegration(this)) { final Uri systemAccount = contact.getSystemAccount(); if (systemAccount == null) { checkContactPermissionAndShowAddDialog(); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 18c1fb892..8b94dd440 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -6,12 +6,14 @@ import android.app.Dialog; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; @@ -91,6 +93,8 @@ import eu.siacs.conversations.xmpp.XmppConnection; public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { + private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent"; + public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri"; private final int REQUEST_SYNC_CONTACTS = 0x28cf; @@ -761,50 +765,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private void askForContactsPermissions() { - if (QuickConversationsService.isFreeOrQuicksyFlavor() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + if (QuickConversationsService.isContactListIntegration(this) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (checkSelfPermission(Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { if (mRequestedContactsPermission.compareAndSet(false, true)) { - if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { + final String consent = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) + .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null); + final boolean requiresConsent = + (QuickConversationsService.isQuicksy() + || QuickConversationsService.isPlayStoreFlavor()) + && !"agreed".equals(consent); + if (requiresConsent && "declined".equals(consent)) { + Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined"); + return; + } + if (requiresConsent + || shouldShowRequestPermissionRationale( + Manifest.permission.READ_CONTACTS)) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); final AtomicBoolean requestPermission = new AtomicBoolean(false); if (QuickConversationsService.isQuicksy()) { builder.setTitle(R.string.quicksy_wants_your_consent); - builder.setMessage(Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static))); + builder.setMessage( + Html.fromHtml( + getString(R.string.sync_with_contacts_quicksy_static))); } else { builder.setTitle(R.string.sync_with_contacts); - builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name))); + builder.setMessage( + getString( + R.string.sync_with_contacts_long, + getString(R.string.app_name))); } @StringRes int confirmButtonText; - if (QuickConversationsService.isConversations()) { - confirmButtonText = R.string.next; - } else { + if (requiresConsent) { confirmButtonText = R.string.agree_and_continue; + } else { + confirmButtonText = R.string.next; } - builder.setPositiveButton(confirmButtonText, (dialog, which) -> { - if (requestPermission.compareAndSet(false, true)) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - }); - builder.setOnDismissListener(dialog -> { - if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - }); - if (QuickConversationsService.isQuicksy()) { - builder.setNegativeButton(R.string.decline, null); + builder.setPositiveButton( + confirmButtonText, + (dialog, which) -> { + if (requiresConsent) { + PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString( + PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed") + .apply(); + } + if (requestPermission.compareAndSet(false, true)) { + requestPermissions( + new String[] {Manifest.permission.READ_CONTACTS}, + REQUEST_SYNC_CONTACTS); + } + }); + if (requiresConsent) { + builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString( + PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined") + .apply()); + } else { + builder.setOnDismissListener( + dialog -> { + if (requestPermission.compareAndSet(false, true)) { + requestPermissions( + new String[] { + Manifest.permission.READ_CONTACTS + }, + REQUEST_SYNC_CONTACTS); + } + }); } - builder.setCancelable(QuickConversationsService.isQuicksy()); + builder.setCancelable(requiresConsent); final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy()); - dialog.setOnShowListener(dialogInterface -> { - final TextView tv = dialog.findViewById(android.R.id.message); - if (tv != null) { - tv.setMovementMethod(LinkMovementMethod.getInstance()); - } - }); + dialog.setCanceledOnTouchOutside(requiresConsent); + dialog.setOnShowListener( + dialogInterface -> { + final TextView tv = dialog.findViewById(android.R.id.message); + if (tv != null) { + tv.setMovementMethod(LinkMovementMethod.getInstance()); + } + }); dialog.show(); } else { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); + requestPermissions( + new String[] {Manifest.permission.READ_CONTACTS}, + REQUEST_SYNC_CONTACTS); } } } @@ -840,7 +890,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override protected void onBackendConnected() { - if (QuickConversationsService.isFreeOrQuicksyFlavor() + if (QuickConversationsService.isContactListIntegration(this) && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED)) { diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index f6849e6d1..98924a262 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -10,6 +10,8 @@ import android.os.Build; import android.provider.ContactsContract.Profile; import android.provider.Settings; +import com.google.common.base.Strings; + import eu.siacs.conversations.services.QuickConversationsService; public class PhoneHelper { @@ -20,27 +22,25 @@ public class PhoneHelper { } public static Uri getProfilePictureUri(final Context context) { - if (!QuickConversationsService.isFreeOrQuicksyFlavor() + if (!QuickConversationsService.isContactListIntegration(context) || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED)) { return null; } final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI}; - final Cursor cursor; - try { - cursor = - context.getContentResolver() - .query(Profile.CONTENT_URI, projection, null, null, null); - } catch (Throwable e) { - return null; + try (final Cursor cursor = + context.getContentResolver() + .query(Profile.CONTENT_URI, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + final var photoUri = cursor.getString(1); + if (Strings.isNullOrEmpty(photoUri)) { + return null; + } + return Uri.parse(photoUri); + } } - if (cursor == null) { - return null; - } - final String uri = cursor.moveToFirst() ? cursor.getString(1) : null; - cursor.close(); - return uri == null ? null : Uri.parse(uri); + return null; } public static boolean isEmulator() { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0f8a16b46..3ee875fcf 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -518,8 +518,8 @@ Grant %1$s access to external storage Grant %1$s access to the camera Quicksy asks for your consent to use your data - Synchronize with contacts - %1$s wants permission to access your address book to match it with your XMPP contact list.\nThis will display your contacts’ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server. + Contact list integration + %1$s processes your contact list locally, on your device, to show you the names and profile pictures for matching contacts on XMPP.\n\nNo contact list data ever leaves your device!
Find more information in our Privacy Policy.]]>
Notify on all messages Notify only when mentioned @@ -1024,5 +1024,5 @@ Report spam Report spam and block spammer Privacy policy - Address book integration is not available + Contact list integration is not available From 2025d9773c2c0ad602c48ce97dfbf02005839ca3 Mon Sep 17 00:00:00 2001 From: ghose Date: Fri, 16 Feb 2024 05:33:13 +0000 Subject: [PATCH 092/556] Translated using Weblate (Galician) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 4167acbc3..2f9d07166 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1027,4 +1027,6 @@ Informar de spam Benvida a Quicksy! Quicksy solicita permiso para usar os teus datos + Política de privacidade + Non está dispoñible a integración coa libreta de enderezos \ No newline at end of file From 80d1e67e20b841165a47e10180d2b06e0a95adf1 Mon Sep 17 00:00:00 2001 From: ghose Date: Fri, 16 Feb 2024 05:35:10 +0000 Subject: [PATCH 093/556] Translated using Weblate (Galician) Currently translated at 50.0% (28 of 56 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4209204.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4209204.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209204.txt b/fastlane/metadata/android/gl-ES/changelogs/4209204.txt new file mode 100644 index 000000000..4b7430123 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Acceso máis doado á 'Política de Privacidade' na versión da Play Store (Quicksy e Conversations) +* Retirada a integración coa libreta de enderezo na versión de Conversations da Play Store From a1013a6aadbb718cd6a14f491010e577b2c45d64 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 17 Feb 2024 16:41:28 +0000 Subject: [PATCH 094/556] Translated using Weblate (Spanish) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 5e2bf83d6..617fb9ce1 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1038,4 +1038,6 @@ Informar sobre spam ¡Bienvenido a Quicksy! Quicksy pide tu consentimiento para utilizar tus datos + Política de privacidad + La integración de la agenda no está disponible \ No newline at end of file From 3fe418b807ecea44af8375aaaf8e58f2e4a97ad0 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 17 Feb 2024 16:43:46 +0000 Subject: [PATCH 095/556] Translated using Weblate (Spanish) Currently translated at 100.0% (56 of 56 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/4209204.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4209204.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4209204.txt b/fastlane/metadata/android/es-ES/changelogs/4209204.txt new file mode 100644 index 000000000..097d42728 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Facilitar el acceso a la "Política de privacidad" en la versión de Play Store (Quicksy y Conversations). +* Eliminar la integración de la libreta de direcciones en la versión Play Store de Conversations From 293a2f54fe11ddf2c960d5f1fc2e4e6ac550cf76 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sun, 18 Feb 2024 10:37:14 +0000 Subject: [PATCH 096/556] Translated using Weblate (German) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index f1e8137c8..f2f8e03b0 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1022,4 +1022,8 @@ Du verwendest nicht verifizierte Geräte. Scanne den QR-Code auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern. Spam melden Spam melden und Spammer sperren + Willkommen bei Quicksy! + Datenschutzbestimmungen + Quicksy bittet dich um deine Zustimmung zur Verwendung deiner Daten + Adressbuchintegration ist nicht verfügbar \ No newline at end of file From 86486c54e703afbbec774d4ca966081c50ad6cf5 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sun, 18 Feb 2024 10:41:59 +0000 Subject: [PATCH 097/556] Translated using Weblate (German) Currently translated at 100.0% (56 of 56 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4209004.txt | 2 ++ fastlane/metadata/android/de-DE/changelogs/4209204.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4209004.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/4209204.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4209004.txt b/fastlane/metadata/android/de-DE/changelogs/4209004.txt new file mode 100644 index 000000000..018d6e1eb --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* kleinere Fehlerbehebungen +* Geringfügige Änderungen beim Quicksy-Onboarding diff --git a/fastlane/metadata/android/de-DE/changelogs/4209204.txt b/fastlane/metadata/android/de-DE/changelogs/4209204.txt new file mode 100644 index 000000000..7658709d2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Einfacherer Zugang zu den Datenschutzbestimmungen in der Play Store-Version (Quicksy und Conversations) +* Entfernen der Adressbuchintegration in der Play Store-Version von Conversations From d50cd3ae39d3785abe2df061b2653217a74cb8c4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 19 Feb 2024 14:34:25 +0100 Subject: [PATCH 098/556] push changelog (to be translated before release) --- CHANGELOG.md | 4 ++++ fastlane/metadata/android/en-US/changelogs/4209404.txt | 1 + 2 files changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/4209404.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e50d4e9..e20272a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.13.4 + +* Fix minor regressions introduced with 2.13.1 + ### Version 2.13.3 * Provide easier access to 'Privacy Policy' on Play Store version (Quicksy and Conversations) diff --git a/fastlane/metadata/android/en-US/changelogs/4209404.txt b/fastlane/metadata/android/en-US/changelogs/4209404.txt new file mode 100644 index 000000000..764f13c52 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4209404.txt @@ -0,0 +1 @@ +* Fix minor regressions introduced with 2.13.1 From 38a77455a22293038ef7ea80f1b59f50d0fa7d79 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 20 Feb 2024 10:25:18 +0100 Subject: [PATCH 099/556] add Contacts permission to play store flavor --- src/playstore/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml index 402d957f4..07ac01234 100644 --- a/src/playstore/AndroidManifest.xml +++ b/src/playstore/AndroidManifest.xml @@ -1,6 +1,10 @@ + + + + Date: Tue, 20 Feb 2024 10:27:35 +0100 Subject: [PATCH 100/556] version bump to 2.13.4 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 58b46cd94..b0e622754 100644 --- a/build.gradle +++ b/build.gradle @@ -97,15 +97,14 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42092 - versionName "2.13.3" + versionCode 42094 + versionName "2.13.4" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId def appName = "Conversations" resValue "string", "app_name", appName buildConfigField "String", "APP_NAME", "\"$appName\"" - buildConfigField "String", "PRIVACY_POLICY", "\"https://conversations.im/privacy.html\"" } splits { @@ -150,6 +149,7 @@ android { conversations { dimension "mode" + buildConfigField "String", "PRIVACY_POLICY", "\"https://conversations.im/privacy.html\"" } playstore { From 710cf21fb0b4de1d88a3ac523d596b7452ad5e7f Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Mon, 19 Feb 2024 10:12:28 +0000 Subject: [PATCH 101/556] Translated using Weblate (Romanian) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 89a4ef24e..8d06a15bc 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1046,4 +1046,6 @@ Raportează spam Bine ați venit la Quicksy! Quicksy vă solicită consimțământul pentru a utiliza datele dumneavoastră + Integrarea agendei nu este disponibilă + Politica de confidențialitate \ No newline at end of file From f199999c5fdfd8b00dbb387ef89ba498f9296c0f Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:41 +0000 Subject: [PATCH 102/556] Translated using Weblate (Bulgarian) Currently translated at 93.2% (916 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/bg/ --- src/main/res/values-bg/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 4dd0a24cc..910b10fc2 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -505,7 +505,10 @@ Дайте на %1$s разрешение за достъп до външната памет Дайте на %1$s разрешение за достъп до камерата Синхронизиране с контактите - %1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP.\nТова ще покаже пълните имена и аватари на контактите Ви.\n\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви. + %1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP. +\nТова ще покаже пълните имена и аватари на контактите Ви. +\n +\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви. Известяване за всички съобщения Известяване само при споменаване Известията са изключени From 2cc0ea93aec5c17b15730a20e1acb0248b6ab5ac Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:42 +0000 Subject: [PATCH 103/556] Translated using Weblate (Czech) Currently translated at 96.2% (945 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/cs/ --- src/main/res/values-cs/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index fc01b569f..18e2941cd 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -511,7 +511,10 @@ Povolit %1$s přístup k externímu úložišti Povolit %1$s přístup ke kameře Synchronizovat s kontakty - %1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.\nU kontaktů se pak zobrazí celé jméno a avatar.\n\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server. + %1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty. +\nU kontaktů se pak zobrazí celé jméno a avatar. +\n +\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server. Upozorňovat na všechny zprávy Upozornit pouze, když mě někdo zmíní Upozornění vypnuta From e6ec72aaf779f20eee3d4b86bc06919f76c46a83 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:43 +0000 Subject: [PATCH 104/556] Translated using Weblate (German) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index f2f8e03b0..7e24a9286 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -516,7 +516,10 @@ %1$s den Zugriff auf den externen Speicher gewähren %1$s den Zugriff auf die Kamera gewähren Mit Kontakten synchronisieren - %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen.\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt.\n\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird. + %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen. +\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt. +\n +\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird. Bei allen Nachrichten benachrichtigen Nur benachrichtigen, wenn ich erwähnt werde Benachrichtigungen deaktiviert From 149ea5f6a99a64e95f0a4c502005488ab332feb3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:44 +0000 Subject: [PATCH 105/556] Translated using Weblate (Greek) Currently translated at 94.2% (926 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/el/ --- src/main/res/values-el/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index dccb53bf0..aebf7e03a 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -31,10 +31,7 @@ πριν από %d λεπτά %d μη αναγνωσμένη συζήτηση - - %d μη αναγνωσμένες συζητήσεις - αποστολή... Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε... @@ -509,7 +506,10 @@ Απόδοση δικαιώματος στο %1$s για πρόσβαση στον εξωτερικό αποθηκευτικό χώρο Απόδοση δικαιώματος στο %1$s για πρόσβαση στην φωτογραφική μηχανή Συγχρονισμός με επαφές - Το %1$s ζητάει το δικαίωμα να έχει πρόσβαση στο βιβλίο διευθύνσεων για να το ταιριάξει με την λίστα επαφών XMPP σας.\nΑυτή η ενέργεια θα εμφανίσει τα πλήρη ονόματα και τις εικόνες προφίλ των επαφών σας.\n\nΤο %1$s θα διαβάσει μόνο το βιβλίο διευθύνσεών σας και θα το ταιριάξει τοπικά χωρίς να μεταφορτώσει κανένα στοιχείο στον διακομιστή σας. + Το %1$s ζητάει το δικαίωμα να έχει πρόσβαση στο βιβλίο διευθύνσεων για να το ταιριάξει με την λίστα επαφών XMPP σας. +\nΑυτή η ενέργεια θα εμφανίσει τα πλήρη ονόματα και τις εικόνες προφίλ των επαφών σας. +\n +\nΤο %1$s θα διαβάσει μόνο το βιβλίο διευθύνσεών σας και θα το ταιριάξει τοπικά χωρίς να μεταφορτώσει κανένα στοιχείο στον διακομιστή σας. Ειδοποίηση για όλα τα μηνύματα Ειδοποίηση μόνο όταν αναφέρεται το όνομά μου Οι ειδοποιήσεις απενεργοποιήθηκαν @@ -965,4 +965,4 @@ Έγγραφο απλού κειμένου Δεν υποστηρίζονται εγγραφές λογαριασμών Δεν βρέθηκε διεύθυνση XMPP - + \ No newline at end of file From 4ad410e79545ec43b5e4e03abf99bceffd3e2592 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:43 +0000 Subject: [PATCH 106/556] Translated using Weblate (Danish) Currently translated at 97.5% (958 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/da/ --- src/main/res/values-da-rDK/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 099369df1..a7a534e1c 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -514,7 +514,10 @@ Giv %1$s adgang til ekstern lagerplads Giv %1$s adgang til kameraet Synkroniser med kontakter - %1$s ønsker tilladelse til at få adgang til din adressebog for at matche den med din XMPP kontaktliste.\nDette vil vise dine kontakters fulde navne og avatarer.\n\n%1$s læser kun din adressebog og matcher den lokalt uden at uploade noget til din server. + %1$s ønsker tilladelse til at få adgang til din adressebog for at matche den med din XMPP kontaktliste. +\nDette vil vise dine kontakters fulde navne og avatarer. +\n +\n%1$s læser kun din adressebog og matcher den lokalt uden at uploade noget til din server. Underret ved alle beskeder Underret kun når nævnt Notifikationer deaktiveret From 117c4e310b620a575ddeb87add5ac039eb1d873b Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:45 +0000 Subject: [PATCH 107/556] Translated using Weblate (Spanish) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 617fb9ce1..fcefc2946 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -519,7 +519,10 @@ Permitir a %1$s acceder al almacenamiento externo Permitir a %1$s acceder a la cámara Sincronizar contactos - %1$s quiere permiso para acceder a tu agenda de contactos y cruzarla con tu lista de contactos de XMPP.\nEsto permitirá mostrar el nombre completo y los avatares de tus contactos.\n\n%1$s solo leerá tu agenda de contactos y la cruzará localmente sin subir nada a tu servidor. + %1$s quiere permiso para acceder a tu agenda de contactos y cruzarla con tu lista de contactos de XMPP. +\nEsto permitirá mostrar el nombre completo y los avatares de tus contactos. +\n +\n%1$s solo leerá tu agenda de contactos y la cruzará localmente sin subir nada a tu servidor. Notificar para todos los mensajes Notificar solo cuando eres mencionado Notificaciones deshabilitadas From a964bc171d0a38a847198cedad5641c8522fa8d0 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:46 +0000 Subject: [PATCH 108/556] Translated using Weblate (Finnish) Currently translated at 91.7% (901 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fi/ --- src/main/res/values-fi/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 40cf4eaf8..c5f7f4172 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -493,7 +493,10 @@ Salli %1$s:n käyttää ulkoista tallennustilaa Salli %1$s:n käyttää kameraa Synkronoi yhteystietojen kanssa - %1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa.\nTämä näyttää yhteystietojesi koko nimen ja kuvan.\n\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle. + %1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa. +\nTämä näyttää yhteystietojesi koko nimen ja kuvan. +\n +\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle. Ilmoita kaikista uusista viesteistä Ilmoita vain kun minut mainitaan Ilmoitukset pois käytöstä From ab2d36387346018096aa4b7b261fc219a065a670 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:47 +0000 Subject: [PATCH 109/556] Translated using Weblate (Galician) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 2f9d07166..fac37134b 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -518,7 +518,10 @@ Permitir que %1$s acceda ao almacenaxe externo Permitir que %1$s acceda á cámara Sincronice con todos os contactos - %1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP.\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos.\n\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor. + %1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP. +\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos. +\n +\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor. Notificar todas as mensaxes Notificar só cando é mencionada Notificacións desactivadas From 6785fe049dab885b491b0d9ff39790a7ebec6dbf Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:49 +0000 Subject: [PATCH 110/556] Translated using Weblate (Hungarian) Currently translated at 87.1% (856 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/hu/ --- src/main/res/values-hu/strings.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index c4c01573a..87b1e000f 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -31,10 +31,7 @@ %d perce %d olvasatlan beszélgetés - - %d olvasatlan beszélgetés - küldés… Üzenet visszafejtése. Kérem várjon… @@ -890,4 +887,4 @@ %1$d résztvevő megtekintése %1$d résztvevő megtekintése - + \ No newline at end of file From 5cdd0c0609ab9f9bcb2cd2773e729229d9bbf451 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:49 +0000 Subject: [PATCH 111/556] Translated using Weblate (Indonesian) Currently translated at 47.7% (469 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/id/ --- src/main/res/values-id/strings.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index aa357c713..21022e927 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -31,7 +31,6 @@ %d min lalu %d percakapan belum dibaca - mengirim... Mendekripsi pesan. Mohon tunggu… @@ -488,4 +487,4 @@ alamat XMPP Buat channel publik... Sibuk - + \ No newline at end of file From d668819c015e64be7f58863fff5f3a334c3c0565 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:50 +0000 Subject: [PATCH 112/556] Translated using Weblate (Italian) Currently translated at 99.7% (980 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 69510ee5e..de49a1aff 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -519,7 +519,10 @@ Dai a %1$s l\'accesso all\'archiviazione esterna Dai a %1$s l\'accesso alla fotocamera Sincronizza con i contatti - %1$s vuole l\'autorizzazione ad accedere alla tua rubrica per confrontarla con la lista di contatti in XMPP.\nCiò mostrerà i nomi ed avatar dei contatti.\n\n%1$s leggerà solamente la rubrica e la confronterà localmente senza inviare nulla al tuo server. + %1$s vuole l\'autorizzazione ad accedere alla tua rubrica per confrontarla con la lista di contatti in XMPP. +\nCiò mostrerà i nomi ed avatar dei contatti. +\n +\n%1$s leggerà solamente la rubrica e la confronterà localmente senza inviare nulla al tuo server. Notifica per tutti i messaggi Notifica solo quando menzionato Notifiche disattivate From 75707507ba99ddd18115a0c9f6c2ca7d8822fe4c Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:50 +0000 Subject: [PATCH 113/556] Translated using Weblate (Hebrew) Currently translated at 27.9% (274 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 9b43fcbbc..5d36f5872 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -281,4 +281,4 @@ מקוון ההודעה הועתקה הראה מיקום - + \ No newline at end of file From 007731f757532dfac2a6e815dc332c0536d7f108 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:51 +0000 Subject: [PATCH 114/556] Translated using Weblate (Japanese) Currently translated at 98.4% (967 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 4298a2338..80bb88581 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -510,7 +510,10 @@ %1$s に外部ストレージへのアクセス権を付与してください %1$s にカメラへのアクセス権を付与 連絡先と同期 - %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。\nこれにより、連絡先のフルネームとアバターが表示されます。\n\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。 + %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。 +\nこれにより、連絡先のフルネームとアバターが表示されます。 +\n +\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。 すべてのメッセージで通知 メンションされたときにのみ通知 通知は無効 From c98d14757f76a80e3f27a52ba75c648c45e3188f Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:51 +0000 Subject: [PATCH 115/556] Translated using Weblate (Korean) Currently translated at 40.1% (394 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ko/ --- src/main/res/values-ko/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index 82afd51ba..cce479592 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -398,4 +398,4 @@ 중간 위치 표시 바쁨 - + \ No newline at end of file From 5c13f30058c9c25ecf43dbe99fc9d8f36f6f0d23 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:53 +0000 Subject: [PATCH 116/556] Translated using Weblate (Polish) Currently translated at 99.0% (973 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 068274583..c567574f3 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -524,7 +524,10 @@ Pozwól %1$s na dostęp do zewnętrznego magazynu Pozwól %1$s na dostępu do aparatu Synchronizuj z kontaktami - %1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP.\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów.\n\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer. + %1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP. +\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów. +\n +\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer. Powiadom o wszystkich wiadomościach Powiadamiaj tylko w przypadku wzmianki o mnie Powiadomienia wyłączone From d32fa8ae1b3e5d614fded5226ec446047151a7bd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:54 +0000 Subject: [PATCH 117/556] Translated using Weblate (Portuguese) Currently translated at 41.2% (405 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt/ --- src/main/res/values-pt/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index 8afde7af1..34c36151a 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -412,4 +412,4 @@ Médio Exibir localização Ocupado - + \ No newline at end of file From c846ac87235156f189c096194d45f2acf51e9dec Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:55 +0000 Subject: [PATCH 118/556] Translated using Weblate (Russian) Currently translated at 99.4% (977 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 8d60c8a6b..eb43f1142 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -514,7 +514,10 @@ Предоставить %1$s разрешение на использование внешнего накопителя Предоставить %1$s разрешение на использование камеры Синхронизировать с контактами - %1$s нужно разрешение на доступ к контактам, чтобы соотнести их с вашими XMPP-контактами.\nЭто позволит отобразить полные имена и аватары контактов.\n\n%1$s сделает это локально, без отправки чего-либо на ваш сервер. + %1$s нужно разрешение на доступ к контактам, чтобы соотнести их с вашими XMPP-контактами. +\nЭто позволит отобразить полные имена и аватары контактов. +\n +\n%1$s сделает это локально, без отправки чего-либо на ваш сервер. Все сообщения Уведомлять только при упоминании Уведомления выключены From 8531bae43dc7f53b3297fbb4629284d76cd07899 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:52 +0000 Subject: [PATCH 119/556] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 46.9% (461 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/nb_NO/ --- src/main/res/values-nb-rNO/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-nb-rNO/strings.xml b/src/main/res/values-nb-rNO/strings.xml index 73b5b7311..75c1ee1de 100644 --- a/src/main/res/values-nb-rNO/strings.xml +++ b/src/main/res/values-nb-rNO/strings.xml @@ -467,4 +467,4 @@ Gruppesludringsnavn Opprett gruppesludring Opptatt - + \ No newline at end of file From b3aa78c50ae488be11a62938208019b3d898d130 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:53 +0000 Subject: [PATCH 120/556] Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.0% (943 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 8c04b5a6c..7b823803c 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -31,13 +31,8 @@ %d minutos atrás %d conversa não lida - - %d conversas não lidas - - %d conversas não lidas - enviando... Descriptografando a mensagem. Por favor, aguarde... @@ -520,7 +515,9 @@ Permita o acesso do %1$s ao armazenamento externo Permita o acesso do %1$s à câmera Sincronizar com os contatos - %1$s gostaria de obter a permissão para acessar seu livro de endereços e fazer a correspondência entre ele e a sua lista de contatos do XMPP. Isso permitirá exibir os nomes completos e avatares dos seus contatos.\n\n%1$s fará a leitura e a correspondência do seu livro de endereços localmente, sem enviar os seus contatos para o servidor em uso. + %1$s gostaria de obter a permissão para acessar seu livro de endereços e fazer a correspondência entre ele e a sua lista de contatos do XMPP. Isso permitirá exibir os nomes completos e avatares dos seus contatos. +\n +\n%1$s fará a leitura e a correspondência do seu livro de endereços localmente, sem enviar os seus contatos para o servidor em uso. Notificar em todas as mensagens Notificar somente quando for mencionado Notificações desabilitadas @@ -1007,5 +1004,4 @@ As chamadas estão desabilitadas ao usar Tor Mudar para vídeo Recusar requisição de mudança para vídeo - - + \ No newline at end of file From a6ef77db927865ef6fa68859365d9bc3354ec8e6 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:56 +0000 Subject: [PATCH 121/556] Translated using Weblate (Serbian) Currently translated at 65.2% (641 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sr/ --- src/main/res/values-sr/strings.xml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index de9f6d1df..925c98ffe 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -31,13 +31,8 @@ пре %d минута %d непрочитана порука - - %d непрочитане поруке - - %d непрочитаних порука - шаљем… Дешифрујем поруку, сачекајте… @@ -684,4 +679,4 @@ Тихе поруке Видео компресија Заузет - + \ No newline at end of file From 6b7bc1027e2b78092f18b4028fd980a50b77eb2a Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:59 +0000 Subject: [PATCH 122/556] Translated using Weblate (Vietnamese) Currently translated at 97.5% (958 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/vi/ --- src/main/res/values-vi/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index fdac1c310..dad02f28d 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -507,7 +507,10 @@ Cấp quyền truy cập bộ nhớ cho %1$s Cấp quyền truy cập máy ảnh cho %1$s Đồng bộ với danh bạ - %1$s muốn quyền truy cập sổ địa chỉ của bạn để nối nó với danh sách liên hệ XMPP của bạn.\nViệc này sẽ hiển thị họ tên và ảnh đại diện của các liên hệ của bạn.\n\n%1$s sẽ chỉ đọc sổ địa chỉ của bạn và nối nó một cách cục bộ mà không tải gì cả lên máy chủ của bạn. + %1$s muốn quyền truy cập sổ địa chỉ của bạn để nối nó với danh sách liên hệ XMPP của bạn. +\nViệc này sẽ hiển thị họ tên và ảnh đại diện của các liên hệ của bạn. +\n +\n%1$s sẽ chỉ đọc sổ địa chỉ của bạn và nối nó một cách cục bộ mà không tải gì cả lên máy chủ của bạn. Thông báo tất cả tin nhắn Chỉ thông báo khi được nhắc đến Đã tắt thông báo From 24a387f705fb63fdd79a0fb2f1b92f32ee8c62ed Mon Sep 17 00:00:00 2001 From: Anonymous Date: Tue, 20 Feb 2024 09:59:57 +0000 Subject: [PATCH 123/556] Translated using Weblate (Silesian) Currently translated at 94.1% (925 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/szl/ --- src/main/res/values-szl/strings.xml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index 62806a6f4..266a41688 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -31,13 +31,8 @@ %d minut tymu %d niyprzeczytano kōnwersacyjo - - %d niyprzeczytane kōnwersacyje - - %d niyprzeczytanych kōnwersacyji - wysyłanie… Ôdszyfrowowanie wiadōmości. To weźnie ino chwila… @@ -1010,4 +1005,4 @@ Dokumynt ze samym tekstym Registracyjo kōnt niy je spiyrano Żodno adresa XMPP niyznojdziōno - + \ No newline at end of file From 34cf6d758a04f0320b8e2794eb1c51664d304329 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Tue, 20 Feb 2024 11:35:42 +0000 Subject: [PATCH 124/556] Translated using Weblate (German) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 7e24a9286..77c02e0d2 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -515,11 +515,10 @@ Text mit %s geteilt %1$s den Zugriff auf den externen Speicher gewähren %1$s den Zugriff auf die Kamera gewähren - Mit Kontakten synchronisieren - %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen. -\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt. + Kontaktlistenintegration + %1$s verarbeitet deine Kontaktliste lokal auf deinem Gerät, um dir die Namen und Profilbilder von passenden Kontakten auf XMPP zu zeigen. \n -\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird. +\nKeine Daten der Kontaktliste verlassen jemals dein Gerät! Bei allen Nachrichten benachrichtigen Nur benachrichtigen, wenn ich erwähnt werde Benachrichtigungen deaktiviert @@ -1028,5 +1027,5 @@ Willkommen bei Quicksy! Datenschutzbestimmungen Quicksy bittet dich um deine Zustimmung zur Verwendung deiner Daten - Adressbuchintegration ist nicht verfügbar + Kontaktlistenintegration ist nicht verfügbar \ No newline at end of file From 38910d4406a356b6c1a1fff7f3ec65a120eaef33 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 20 Feb 2024 13:20:31 +0000 Subject: [PATCH 125/556] Translated using Weblate (Spanish) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index fcefc2946..b7b87ee79 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -518,11 +518,10 @@ Texto compartido con %s Permitir a %1$s acceder al almacenamiento externo Permitir a %1$s acceder a la cámara - Sincronizar contactos - %1$s quiere permiso para acceder a tu agenda de contactos y cruzarla con tu lista de contactos de XMPP. -\nEsto permitirá mostrar el nombre completo y los avatares de tus contactos. + Integración de la lista de contactos + %1$s procesa tu lista de contactos localmente, en tu dispositivo, para mostrarte los nombres y fotos de perfil de los contactos coincidentes en XMPP. \n -\n%1$s solo leerá tu agenda de contactos y la cruzará localmente sin subir nada a tu servidor. +\n¡Ningún dato de la lista de contactos sale de tu dispositivo! Notificar para todos los mensajes Notificar solo cuando eres mencionado Notificaciones deshabilitadas @@ -1042,5 +1041,5 @@ ¡Bienvenido a Quicksy! Quicksy pide tu consentimiento para utilizar tus datos Política de privacidad - La integración de la agenda no está disponible + La lista de contactos no está disponible \ No newline at end of file From 268c09bda0485b2c7e59fed00725d13bf7fd9c8d Mon Sep 17 00:00:00 2001 From: SomeTr Date: Tue, 20 Feb 2024 10:24:51 +0000 Subject: [PATCH 126/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 75313cb41..13019dcae 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -507,7 +507,7 @@ Зображення надіслано %s Зображення надіслано %s Текст надіслано %s - Синхронізувати контакти + Інтеграція зі списком контактів Сповіщати про всі повідомлення Сповіщати, лише якщо згадують Сповіщення вимкнено @@ -991,10 +991,9 @@ Оберіть сервер для доставки push-повідомлень через XMPP на Ваш пристрій. Застосунок для передачі зображення не надав достатніх дозволів. Виклики вимкнені при використанні Tor - %1$s потребує дозволу на доступ до контактів, щоб порівняти їх з Вашими XMPP-контактами. -\nТаким чином можна буде показувати піктограми і повні імена користувачів. + %1$s обробляє Ваш список контактів локально, на Вашому пристрої, щоб показати імена та зображення профілю для відповідних контактів у XMPP. \n -\n%1$s лише прочитає Вашу адресну книгу і зіставлятиме інформацію про контакти локально, нічого не завантажуючи на сервер. +\nУсі дані списку контактів залишаються на Вашому пристрої! Пропущені виклики Ваша операційна система обмежує для %1$s доступ до Інтернету у фоновому режимі. Щоб отримувати сповіщення про нові повідомлення, Вам потрібно дозволити %1$s необмежений доступ, коли заощадження трафіку увімкнено. \n%1$s намагатиметься по можливості економити трафік. @@ -1079,5 +1078,5 @@ Вітаємо у Quicksy! Quicksy просить згоду на використання Ваших даних Політика конфіденційності - Інтеграція адресної книги недоступна + Інтеграція зі списком контактів недоступна \ No newline at end of file From 16644810cc641c86cd3b6ef31b7287468261ad8e Mon Sep 17 00:00:00 2001 From: nautilusx Date: Tue, 20 Feb 2024 11:36:48 +0000 Subject: [PATCH 127/556] Translated using Weblate (German) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/de/ --- src/quicksy/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/res/values-de/strings.xml b/src/quicksy/res/values-de/strings.xml index 8dff5deae..da4e1721f 100644 --- a/src/quicksy/res/values-de/strings.xml +++ b/src/quicksy/res/values-de/strings.xml @@ -1,7 +1,7 @@ Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat - Mit dem Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy + Durch das Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy Informiere deine Kontakte, wann du Quicksy nutzt Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Quicksy zur Liste der geschützten Apps hinzufügen. Quicksy Profilbild From e5098d2556f4d34dc209e1a8d3ffd89c10365170 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Tue, 20 Feb 2024 11:28:11 +0000 Subject: [PATCH 128/556] Translated using Weblate (German) Currently translated at 100.0% (57 of 57 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4209404.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4209404.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4209404.txt b/fastlane/metadata/android/de-DE/changelogs/4209404.txt new file mode 100644 index 000000000..bb5feb585 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4209404.txt @@ -0,0 +1 @@ +* Behebung kleinerer Schwierigkeiten, die mit 2.13.1 eingeführt wurden From 1c2db7bcc5e81a7fd3ff410a781e1765e87fc20e Mon Sep 17 00:00:00 2001 From: SomeTr Date: Tue, 20 Feb 2024 10:21:17 +0000 Subject: [PATCH 129/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (57 of 57 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4209404.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4209404.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209404.txt b/fastlane/metadata/android/uk/changelogs/4209404.txt new file mode 100644 index 000000000..90e81392d --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4209404.txt @@ -0,0 +1 @@ +* Виправлено незначні регресії, які з'явилися у версії 2.13.1 From aefcce430d6540d51343abe8fde138a51bf5a6a3 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 20 Feb 2024 13:21:12 +0000 Subject: [PATCH 130/556] Translated using Weblate (Spanish) Currently translated at 100.0% (57 of 57 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/4209404.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4209404.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4209404.txt b/fastlane/metadata/android/es-ES/changelogs/4209404.txt new file mode 100644 index 000000000..e6999f067 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4209404.txt @@ -0,0 +1 @@ +* Se corrigieron problemas menores introducidos en 2.13.1 From 19c634f3d2d089b4ed5eefcfda36cc0dd4d11c93 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 10:58:00 +0100 Subject: [PATCH 131/556] use call integration via MANAGE_OWN_CALLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit better integrate calls into the system via 'Build a calling app'¹ a few hooks like onAnswer/onReject and automatic PhoneAccount creation are still missing ¹: https://developer.android.com/develop/connectivity/telecom/selfManaged --- build.gradle | 2 +- src/main/AndroidManifest.xml | 13 +- .../services/AppRTCAudioManager.java | 100 ++--- .../services/CallIntegration.java | 408 ++++++++++++++++++ .../CallIntegrationConnectionService.java | 255 +++++++++++ .../services/XmppConnectionService.java | 30 +- .../ui/ConversationFragment.java | 6 +- .../conversations/ui/RtpSessionActivity.java | 57 ++- .../xmpp/jingle/JingleConnectionManager.java | 87 ++-- .../xmpp/jingle/JingleRtpConnection.java | 116 ++++- .../xmpp/jingle/RtpEndUserState.java | 2 +- .../xmpp/jingle/ToneManager.java | 3 +- .../xmpp/jingle/WebRTCWrapper.java | 42 +- 13 files changed, 909 insertions(+), 212 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/services/CallIntegration.java create mode 100644 src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java diff --git a/build.gradle b/build.gradle index b0e622754..e3fb55a76 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,7 @@ android { compileSdk 34 defaultConfig { - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 34 versionCode 42094 versionName "2.13.4" diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c01009862..6233770df 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -5,9 +5,6 @@ - @@ -50,6 +47,8 @@ + + @@ -133,6 +132,14 @@ + + + + + + diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index 3bed4eaba..a472445d3 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -58,18 +58,18 @@ public class AppRTCAudioManager { private boolean hasWiredHeadset; // Default audio device; speaker phone for video calls or earpiece for audio // only calls. - private AudioDevice defaultAudioDevice; + private CallIntegration.AudioDevice defaultAudioDevice; // Contains the currently selected audio device. // This device is changed automatically using a certain scheme where e.g. // a wired headset "wins" over speaker phone. It is also possible for a // user to explicitly select a device (and overrid any predefined scheme). // See |userSelectedAudioDevice| for details. - private AudioDevice selectedAudioDevice; + private CallIntegration.AudioDevice selectedAudioDevice; // Contains the user-selected audio device which overrides the predefined // selection scheme. // TODO(henrika): always set to AudioDevice.NONE today. Add support for // explicit selection based on choice by userSelectedAudioDevice. - private AudioDevice userSelectedAudioDevice; + private CallIntegration.AudioDevice userSelectedAudioDevice; // Proximity sensor object. It measures the proximity of an object in cm // relative to the view screen of a device and can therefore be used to // assist device switching (close to ear <=> use headset earpiece if @@ -78,26 +78,25 @@ public class AppRTCAudioManager { private AppRTCProximitySensor proximitySensor; // Contains a list of available audio devices. A Set collection is used to // avoid duplicate elements. - private Set audioDevices = new HashSet<>(); + private Set audioDevices = new HashSet<>(); // Broadcast receiver for wired headset intent broadcasts. private final BroadcastReceiver wiredHeadsetReceiver; // Callback method for changes in audio focus. @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; - private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) { - Log.d(Config.LOGTAG, "ctor"); + public AppRTCAudioManager(final Context context) { ThreadUtils.checkIsOnMainThread(); apprtcContext = context; audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); bluetoothManager = AppRTCBluetoothManager.create(context, this); wiredHeadsetReceiver = new WiredHeadsetReceiver(); amState = AudioManagerState.UNINITIALIZED; - this.speakerPhonePreference = speakerPhonePreference; - if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { - defaultAudioDevice = AudioDevice.EARPIECE; + // CallIntegration / Connection uses Earpiece as default too + if (hasEarpiece()) { + defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE; } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } // Create and initialize the proximity sensor. // Tablet devices (e.g. Nexus 7) does not support proximity sensors. @@ -114,20 +113,13 @@ public class AppRTCAudioManager { public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { this.speakerPhonePreference = speakerPhonePreference; if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { - defaultAudioDevice = AudioDevice.EARPIECE; + defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE; } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } updateAudioDeviceState(); } - /** - * Construction. - */ - public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) { - return new AppRTCAudioManager(context, speakerPhonePreference); - } - public static boolean isMicrophoneAvailable() { microphoneLatch = new CountDownLatch(1); AudioRecord audioRecord = null; @@ -174,16 +166,16 @@ public class AppRTCAudioManager { } // The proximity sensor should only be activated when there are exactly two // available audio devices. - if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) - && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { + if (audioDevices.size() == 2 && audioDevices.contains(CallIntegration.AudioDevice.EARPIECE) + && audioDevices.contains(CallIntegration.AudioDevice.SPEAKER_PHONE)) { if (proximitySensor.sensorReportsNearState()) { // Sensor reports that a "handset is being held up to a person's ear", // or "something is covering the light sensor". - setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE); + setAudioDeviceInternal(CallIntegration.AudioDevice.EARPIECE); } else { // Sensor reports that a "handset is removed from a person's ear", or // "the light sensor is no longer covered". - setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE); } } } @@ -258,8 +250,8 @@ public class AppRTCAudioManager { // Always disable microphone mute during a WebRTC call. setMicrophoneMute(false); // Set initial device states. - userSelectedAudioDevice = AudioDevice.NONE; - selectedAudioDevice = AudioDevice.NONE; + userSelectedAudioDevice = CallIntegration.AudioDevice.NONE; + selectedAudioDevice = CallIntegration.AudioDevice.NONE; audioDevices.clear(); // Initialize and start Bluetooth if a BT device is available or initiate // detection of new (enabled) BT devices. @@ -315,7 +307,7 @@ public class AppRTCAudioManager { /** * Changes selection of the currently active audio device. */ - private void setAudioDeviceInternal(AudioDevice device) { + private void setAudioDeviceInternal(CallIntegration.AudioDevice device) { Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); AppRTCUtils.assertIsTrue(audioDevices.contains(device)); switch (device) { @@ -338,7 +330,7 @@ public class AppRTCAudioManager { * Changes default audio device. * TODO(henrika): add usage of this method in the AppRTCMobile client. */ - public void setDefaultAudioDevice(AudioDevice defaultDevice) { + public void setDefaultAudioDevice(CallIntegration.AudioDevice defaultDevice) { ThreadUtils.checkIsOnMainThread(); switch (defaultDevice) { case SPEAKER_PHONE: @@ -348,7 +340,7 @@ public class AppRTCAudioManager { if (hasEarpiece()) { defaultAudioDevice = defaultDevice; } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } break; default: @@ -362,7 +354,7 @@ public class AppRTCAudioManager { /** * Changes selection of the currently active audio device. */ - public void selectAudioDevice(AudioDevice device) { + public void selectAudioDevice(CallIntegration.AudioDevice device) { ThreadUtils.checkIsOnMainThread(); if (!audioDevices.contains(device)) { Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices); @@ -374,7 +366,7 @@ public class AppRTCAudioManager { /** * Returns current set of available/selectable audio devices. */ - public Set getAudioDevices() { + public Set getAudioDevices() { ThreadUtils.checkIsOnMainThread(); return Collections.unmodifiableSet(new HashSet<>(audioDevices)); } @@ -382,7 +374,7 @@ public class AppRTCAudioManager { /** * Returns the currently selected audio device. */ - public AudioDevice getSelectedAudioDevice() { + public CallIntegration.AudioDevice getSelectedAudioDevice() { ThreadUtils.checkIsOnMainThread(); return selectedAudioDevice; } @@ -479,21 +471,21 @@ public class AppRTCAudioManager { bluetoothManager.updateDevice(); } // Update the set of available audio devices. - Set newAudioDevices = new HashSet<>(); + Set newAudioDevices = new HashSet<>(); if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { - newAudioDevices.add(AudioDevice.BLUETOOTH); + newAudioDevices.add(CallIntegration.AudioDevice.BLUETOOTH); } if (hasWiredHeadset) { // If a wired headset is connected, then it is the only possible option. - newAudioDevices.add(AudioDevice.WIRED_HEADSET); + newAudioDevices.add(CallIntegration.AudioDevice.WIRED_HEADSET); } else { // No wired headset, hence the audio-device list can contain speaker // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - newAudioDevices.add(AudioDevice.SPEAKER_PHONE); + newAudioDevices.add(CallIntegration.AudioDevice.SPEAKER_PHONE); if (hasEarpiece()) { - newAudioDevices.add(AudioDevice.EARPIECE); + newAudioDevices.add(CallIntegration.AudioDevice.EARPIECE); } } // Store state which is set to true if the device list has changed. @@ -502,33 +494,33 @@ public class AppRTCAudioManager { audioDevices = newAudioDevices; // Correct user selected audio devices if needed. if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE - && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { + && userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH) { // If BT is not available, it can't be the user selection. - userSelectedAudioDevice = AudioDevice.NONE; + userSelectedAudioDevice = CallIntegration.AudioDevice.NONE; } - if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { + if (hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) { // If user selected speaker phone, but then plugged wired headset then make // wired headset as user selected device. - userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; + userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET; } - if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { + if (!hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) { // If user selected wired headset, but then unplugged wired headset then make // speaker phone as user selected device. - userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } // Need to start Bluetooth if it is available and user either selected it explicitly or // user did not select any output device. boolean needBluetoothAudioStart = bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE - && (userSelectedAudioDevice == AudioDevice.NONE - || userSelectedAudioDevice == AudioDevice.BLUETOOTH); + && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE + || userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH); // Need to stop Bluetooth audio if user selected different device and // Bluetooth SCO connection is established or in the process. boolean needBluetoothAudioStop = (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) - && (userSelectedAudioDevice != AudioDevice.NONE - && userSelectedAudioDevice != AudioDevice.BLUETOOTH); + && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE + && userSelectedAudioDevice != CallIntegration.AudioDevice.BLUETOOTH); if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { @@ -545,21 +537,21 @@ public class AppRTCAudioManager { // Attempt to start Bluetooth SCO audio (takes a few second to start). if (!bluetoothManager.startScoAudio()) { // Remove BLUETOOTH from list of available devices since SCO failed. - audioDevices.remove(AudioDevice.BLUETOOTH); + audioDevices.remove(CallIntegration.AudioDevice.BLUETOOTH); audioDeviceSetUpdated = true; } } // Update selected audio device. - final AudioDevice newAudioDevice; + final CallIntegration.AudioDevice newAudioDevice; if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { // If a Bluetooth is connected, then it should be used as output audio // device. Note that it is not sufficient that a headset is available; // an active SCO channel must also be up and running. - newAudioDevice = AudioDevice.BLUETOOTH; + newAudioDevice = CallIntegration.AudioDevice.BLUETOOTH; } else if (hasWiredHeadset) { // If a wired headset is connected, but Bluetooth is not, then wired headset is used as // audio device. - newAudioDevice = AudioDevice.WIRED_HEADSET; + newAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET; } else { // No wired headset and no Bluetooth, hence the audio-device list can contain speaker // phone (on a tablet), or speaker phone and earpiece (on mobile phone). @@ -582,12 +574,6 @@ public class AppRTCAudioManager { Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); } - /** - * AudioDevice is the names of possible audio devices that we currently - * support. - */ - public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE} - /** * AudioManager state. */ @@ -615,7 +601,7 @@ public class AppRTCAudioManager { public interface AudioManagerEvents { // Callback fired once audio device is changed or list of available audio devices changed. void onAudioDeviceChanged( - AudioDevice selectedAudioDevice, Set availableAudioDevices); + CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices); } /* Receiver which handles changes in wired headset availability. */ diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java new file mode 100644 index 000000000..bf12f5f4b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -0,0 +1,408 @@ +package eu.siacs.conversations.services; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.telecom.CallAudioState; +import android.telecom.CallEndpoint; +import android.telecom.Connection; +import android.telecom.DisconnectCause; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.ui.util.MainThreadExecutor; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.Media; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class CallIntegration extends Connection { + + private final AppRTCAudioManager appRTCAudioManager; + private AudioDevice initialAudioDevice = null; + private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false); + + private List availableEndpoints = Collections.emptyList(); + + private Callback callback = null; + + public CallIntegration(final Context context) { + if (selfManaged()) { + setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + this.appRTCAudioManager = null; + } else { + this.appRTCAudioManager = new AppRTCAudioManager(context); + this.appRTCAudioManager.start(this::onAudioDeviceChanged); + // TODO WebRTCWrapper would issue one call to eventCallback.onAudioDeviceChanged + } + setRingbackRequested(true); + } + + public void setCallback(final Callback callback) { + this.callback = callback; + } + + @Override + public void onShowIncomingCallUi() { + Log.d(Config.LOGTAG, "onShowIncomingCallUi"); + this.callback.onCallIntegrationShowIncomingCallUi(); + } + + @Override + public void onAnswer() { + Log.d(Config.LOGTAG, "onAnswer()"); + } + + @Override + public void onDisconnect() { + Log.d(Config.LOGTAG, "onDisconnect()"); + this.callback.onCallIntegrationDisconnect(); + } + + @Override + public void onReject() { + Log.d(Config.LOGTAG, "onReject()"); + } + + @Override + public void onReject(final String replyMessage) { + Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")"); + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void onAvailableCallEndpointsChanged(@NonNull List availableEndpoints) { + Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")"); + this.availableEndpoints = availableEndpoints; + this.onAudioDeviceChanged( + getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()), + ImmutableSet.copyOf( + Lists.transform( + availableEndpoints, + CallIntegration::getAudioDeviceUpsideDownCake))); + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Override + public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) { + Log.d(Config.LOGTAG, "onCallEndpointChanged()"); + this.onAudioDeviceChanged( + getAudioDeviceUpsideDownCake(callEndpoint), + ImmutableSet.copyOf( + Lists.transform( + this.availableEndpoints, + CallIntegration::getAudioDeviceUpsideDownCake))); + } + + @Override + public void onCallAudioStateChanged(final CallAudioState state) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake"); + return; + } + Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")"); + this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state)); + } + + public Set getAudioDevices() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return getAudioDevicesUpsideDownCake(); + } else if (selfManaged()) { + return getAudioDevicesOreo(); + } else { + return getAudioDevicesFallback(); + } + } + + public AudioDevice getSelectedAudioDevice() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return getAudioDeviceUpsideDownCake(); + } else if (selfManaged()) { + return getAudioDeviceOreo(); + } else { + return getAudioDeviceFallback(); + } + } + + public void setAudioDevice(final AudioDevice audioDevice) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setAudioDeviceUpsideDownCake(audioDevice); + } else if (selfManaged()) { + setAudioDeviceOreo(audioDevice); + } else { + setAudioDeviceFallback(audioDevice); + } + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private Set getAudioDevicesUpsideDownCake() { + return ImmutableSet.copyOf( + Lists.transform( + this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake)); + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private AudioDevice getAudioDeviceUpsideDownCake() { + return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()); + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) { + if (callEndpoint == null) { + return AudioDevice.NONE; + } + final var endpointType = callEndpoint.getEndpointType(); + return switch (endpointType) { + case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH; + case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE; + case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE; + case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET; + case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING; + case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE; + default -> throw new IllegalStateException("Unknown endpoint type " + endpointType); + }; + } + + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) { + final var callEndpointOptional = + Iterables.tryFind( + this.availableEndpoints, + e -> getAudioDeviceUpsideDownCake(e) == audioDevice); + if (callEndpointOptional.isPresent()) { + final var endpoint = callEndpointOptional.get(); + requestCallEndpointChange( + endpoint, + MainThreadExecutor.getInstance(), + result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint)); + } else { + Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice); + } + } + + private Set getAudioDevicesOreo() { + final var audioState = getCallAudioState(); + if (audioState == null) { + Log.d( + Config.LOGTAG, + "no CallAudioState available. returning empty set for audio devices"); + return Collections.emptySet(); + } + return getAudioDevicesOreo(audioState); + } + + private static Set getAudioDevicesOreo(final CallAudioState callAudioState) { + final ImmutableSet.Builder supportedAudioDevicesBuilder = + new ImmutableSet.Builder<>(); + final var supportedRouteMask = callAudioState.getSupportedRouteMask(); + if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH) + == CallAudioState.ROUTE_BLUETOOTH) { + supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH); + } + if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) { + supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE); + } + if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) { + supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE); + } + if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) + == CallAudioState.ROUTE_WIRED_HEADSET) { + supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET); + } + return supportedAudioDevicesBuilder.build(); + } + + private AudioDevice getAudioDeviceOreo() { + final var audioState = getCallAudioState(); + if (audioState == null) { + Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device"); + return AudioDevice.NONE; + } + return getAudioDeviceOreo(audioState); + } + + private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) { + // technically we get a mask here; maybe we should query the mask instead + return switch (audioState.getRoute()) { + case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH; + case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE; + case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE; + case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET; + default -> AudioDevice.NONE; + }; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void setAudioDeviceOreo(final AudioDevice audioDevice) { + switch (audioDevice) { + case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE); + case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH); + case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET); + case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER); + } + } + + private Set getAudioDevicesFallback() { + return requireAppRtcAudioManager().getAudioDevices(); + } + + private AudioDevice getAudioDeviceFallback() { + return requireAppRtcAudioManager().getSelectedAudioDevice(); + } + + private void setAudioDeviceFallback(final AudioDevice audioDevice) { + requireAppRtcAudioManager().setDefaultAudioDevice(audioDevice); + } + + @NonNull + private AppRTCAudioManager requireAppRtcAudioManager() { + if (this.appRTCAudioManager == null) { + throw new IllegalStateException( + "You are trying to access the fallback audio manager on a modern device"); + } + return this.appRTCAudioManager; + } + + @Override + public void onStateChanged(final int state) { + Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); + if (state == STATE_DISCONNECTED) { + final var audioManager = this.appRTCAudioManager; + if (audioManager != null) { + audioManager.stop(); + } + } + } + + public void success() { + Log.d(Config.LOGTAG, "CallIntegration.success()"); + this.destroyWith(new DisconnectCause(DisconnectCause.LOCAL, null)); + } + + public void accepted() { + Log.d(Config.LOGTAG, "CallIntegration.accepted()"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null)); + } else { + this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null)); + } + } + + public void error() { + Log.d(Config.LOGTAG, "CallIntegration.error()"); + this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null)); + } + + public void retracted() { + Log.d(Config.LOGTAG, "CallIntegration.retracted()"); + // an alternative cause would be LOCAL + this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null)); + } + + public void rejected() { + Log.d(Config.LOGTAG, "CallIntegration.rejected()"); + this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null)); + } + + public void busy() { + Log.d(Config.LOGTAG, "CallIntegration.busy()"); + this.destroyWith(new DisconnectCause(DisconnectCause.BUSY, null)); + } + + private void destroyWith(final DisconnectCause disconnectCause) { + if (this.getState() == STATE_DISCONNECTED) { + Log.d(Config.LOGTAG, "CallIntegration has already been destroyed"); + return; + } + this.setDisconnected(disconnectCause); + this.destroy(); + } + + public static Uri address(final Jid contact) { + return Uri.parse(String.format("xmpp:%s", contact.toEscapedString())); + } + + public void verifyDisconnected() { + if (this.getState() == STATE_DISCONNECTED) { + return; + } + throw new AssertionError("CallIntegration has not been disconnected"); + } + + private void onAudioDeviceChanged( + final CallIntegration.AudioDevice selectedAudioDevice, + final Set availableAudioDevices) { + if (this.initialAudioDevice != null + && this.initialAudioDeviceConfigured.compareAndSet(false, true)) { + if (availableAudioDevices.contains(this.initialAudioDevice)) { + setAudioDevice(this.initialAudioDevice); + Log.d(Config.LOGTAG, "configured initial audio device"); + } else { + Log.d( + Config.LOGTAG, + "initial audio device not available. available devices: " + + availableAudioDevices); + } + } + final var callback = this.callback; + if (callback == null) { + return; + } + callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + + public static boolean selfManaged() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + } + + public void setInitialAudioDevice(final AudioDevice audioDevice) { + Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")"); + this.initialAudioDevice = audioDevice; + if (CallIntegration.selfManaged()) { + // once the 'CallIntegration' gets added to the system we receive calls to update audio + // state + return; + } + final var audioManager = requireAppRtcAudioManager(); + this.onAudioDeviceChanged( + audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices()); + } + + /** AudioDevice is the names of possible audio devices that we currently support. */ + public enum AudioDevice { + NONE, + SPEAKER_PHONE, + WIRED_HEADSET, + EARPIECE, + BLUETOOTH, + STREAMING + } + + public static AudioDevice initialAudioDevice(final Set media) { + if (Media.audioOnly(media)) { + return AudioDevice.EARPIECE; + } else { + return AudioDevice.SPEAKER_PHONE; + } + } + + public interface Callback { + void onCallIntegrationShowIncomingCallUi(); + + void onCallIntegrationDisconnect(); + + void onAudioDeviceChanged( + CallIntegration.AudioDevice selectedAudioDevice, + Set availableAudioDevices); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java new file mode 100644 index 000000000..cfd6ae603 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -0,0 +1,255 @@ +package eu.siacs.conversations.services; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.telecom.Connection; +import android.telecom.ConnectionRequest; +import android.telecom.ConnectionService; +import android.telecom.DisconnectCause; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telecom.VideoProfile; +import android.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.RtpSessionActivity; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import eu.siacs.conversations.xmpp.jingle.Media; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class CallIntegrationConnectionService extends ConnectionService { + + private ListenableFuture serviceFuture; + + @Override + public void onCreate() { + super.onCreate(); + this.serviceFuture = ServiceConnectionService.bindService(this); + } + + @Override + public void onDestroy() { + Log.d(Config.LOGTAG, "destroying CallIntegrationConnectionService"); + super.onDestroy(); + final ServiceConnection serviceConnection; + try { + serviceConnection = serviceFuture.get().serviceConnection; + } catch (final Exception e) { + Log.d(Config.LOGTAG, "could not fetch service connection", e); + return; + } + this.unbindService(serviceConnection); + } + + @Override + public Connection onCreateOutgoingConnection( + final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { + Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")"); + final var uri = request.getAddress(); + final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); + final var extras = request.getExtras(); + final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE); + final Set media = + videoState == VideoProfile.STATE_AUDIO_ONLY + ? ImmutableSet.of(Media.AUDIO) + : ImmutableSet.of(Media.AUDIO, Media.VIDEO); + Log.d(Config.LOGTAG, "jid=" + jid); + Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId()); + Log.d(Config.LOGTAG, "media " + media); + final var service = ServiceConnectionService.get(this.serviceFuture); + if (service == null) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "service connection not found")); + } + final Account account = service.findAccountByUuid(phoneAccountHandle.getId()); + final Intent intent = new Intent(this, RtpSessionActivity.class); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, jid.toEscapedString()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + final CallIntegration callIntegration; + if (jid.isBareJid()) { + final var proposal = + service.getJingleConnectionManager() + .proposeJingleRtpSession(account, jid, media); + + if (Media.audioOnly(media)) { + intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + } else { + intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); + } + callIntegration = proposal.getCallIntegration(); + } else { + final JingleRtpConnection jingleRtpConnection = + service.getJingleConnectionManager().initializeRtpSession(account, jid, media); + final String sessionId = jingleRtpConnection.getId().sessionId; + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); + callIntegration = jingleRtpConnection.getCallIntegration(); + } + Log.d(Config.LOGTAG, "start activity!"); + startActivity(intent); + return callIntegration; + } + + public Connection onCreateIncomingConnection( + final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { + final var service = ServiceConnectionService.get(this.serviceFuture); + final Bundle extras = request.getExtras(); + final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); + final String incomingCallAddress = + extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS); + final String sid = extraExtras == null ? null : extraExtras.getString("sid"); + Log.d(Config.LOGTAG, "sid " + sid); + final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress); + Log.d(Config.LOGTAG, "uri=" + uri); + if (uri == null || sid == null) { + return Connection.createFailedConnection( + new DisconnectCause( + DisconnectCause.ERROR, + "connection request is missing required information")); + } + if (service == null) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "service connection not found")); + } + final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); + final Account account = service.findAccountByUuid(phoneAccountHandle.getId()); + final var weakReference = + service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid); + if (weakReference == null) { + Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid); + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found")); + } + final var jingleRtpConnection = weakReference.get(); + if (jingleRtpConnection == null) { + Log.d(Config.LOGTAG, "connection has been terminated"); + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated")); + } + Log.d(Config.LOGTAG, "registering call integration for incoming call"); + return jingleRtpConnection.getCallIntegration(); + } + + public static void registerPhoneAccount(final Context context, final Account account) { + final var builder = + PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid()); + builder.setSupportedUriSchemes(Collections.singletonList("xmpp")); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setCapabilities( + PhoneAccount.CAPABILITY_SELF_MANAGED + | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING); + } + final var phoneAccount = builder.build(); + + context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount); + } + + public static void registerPhoneAccounts( + final Context context, final Collection accounts) { + for (final Account account : accounts) { + registerPhoneAccount(context, account); + } + } + + public static PhoneAccountHandle getHandle(final Context context, final Account account) { + final var competentName = + new ComponentName(context, CallIntegrationConnectionService.class); + return new PhoneAccountHandle(competentName, account.getUuid()); + } + + public static void placeCall( + final Context context, final Account account, final Jid with, final Set media) { + Log.d(Config.LOGTAG, "place call media=" + media); + final var extras = new Bundle(); + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account)); + extras.putInt( + TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, + Media.audioOnly(media) + ? VideoProfile.STATE_AUDIO_ONLY + : VideoProfile.STATE_BIDIRECTIONAL); + context.getSystemService(TelecomManager.class) + .placeCall(CallIntegration.address(with), extras); + } + + public static void addNewIncomingCall( + final Context context, final AbstractJingleConnection.Id id) { + final var phoneAccountHandle = + CallIntegrationConnectionService.getHandle(context, id.account); + final var bundle = new Bundle(); + bundle.putString( + TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, + CallIntegration.address(id.with).toString()); + final var extras = new Bundle(); + extras.putString("sid", id.sessionId); + bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras); + context.getSystemService(TelecomManager.class) + .addNewIncomingCall(phoneAccountHandle, bundle); + } + + public static class ServiceConnectionService { + private final ServiceConnection serviceConnection; + private final XmppConnectionService service; + + public ServiceConnectionService( + final ServiceConnection serviceConnection, final XmppConnectionService service) { + this.serviceConnection = serviceConnection; + this.service = service; + } + + public static XmppConnectionService get( + final ListenableFuture future) { + try { + return future.get(2, TimeUnit.SECONDS).service; + } catch (final ExecutionException | InterruptedException | TimeoutException e) { + return null; + } + } + + public static ListenableFuture bindService( + final Context context) { + final SettableFuture serviceConnectionFuture = + SettableFuture.create(); + final var intent = new Intent(context, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED); + final var serviceConnection = + new ServiceConnection() { + + @Override + public void onServiceConnected( + final ComponentName name, final IBinder iBinder) { + final XmppConnectionService.XmppConnectionBinder binder = + (XmppConnectionService.XmppConnectionBinder) iBinder; + serviceConnectionFuture.set( + new ServiceConnectionService(this, binder.getService())); + } + + @Override + public void onServiceDisconnected(final ComponentName name) {} + }; + context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + return serviceConnectionFuture; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 584237156..5f48fca58 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -198,6 +198,7 @@ public class XmppConnectionService extends Service { public static final String ACTION_DISMISS_CALL = "dismiss_call"; public static final String ACTION_END_CALL = "end_call"; public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; + public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = "call_integration_service_started"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG"; @@ -303,16 +304,6 @@ public class XmppConnectionService extends Service { return false; } }; - private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false); - private final PhoneStateListener phoneStateListener = new PhoneStateListener() { - @Override - public void onCallStateChanged(final int state, final String phoneNumber) { - isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE); - if (state == TelephonyManager.CALL_STATE_OFFHOOK) { - mJingleConnectionManager.notifyPhoneCallStarted(); - } - } - }; private boolean destroyed = false; @@ -1288,6 +1279,8 @@ public class XmppConnectionService extends Service { toggleSetProfilePictureActivity(hasEnabledAccounts); reconfigurePushDistributor(); + CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts); + restoreFromDatabase(); if (QuickConversationsService.isContactListIntegration(this) @@ -1351,23 +1344,10 @@ public class XmppConnectionService extends Service { ContextCompat.RECEIVER_EXPORTED); mForceDuringOnCreate.set(false); toggleForegroundService(); - setupPhoneStateListener(); internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS); } - private void setupPhoneStateListener() { - final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return; - } - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - } - - public boolean isPhoneInCall() { - return isPhoneInCall.get(); - } - private void checkForDeletedFiles() { if (destroyed) { Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed"); @@ -4413,7 +4393,7 @@ public class XmppConnectionService extends Service { } } - public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices) { for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); } @@ -5110,7 +5090,7 @@ public class XmppConnectionService extends Service { public interface OnJingleRtpConnectionUpdate { void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state); - void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices); } public interface OnAccountUpdate { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index d19bc4a55..ccb08f95f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -86,6 +86,7 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.CallIntegrationConnectionService; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; @@ -1652,13 +1653,14 @@ public class ConversationFragment extends XmppFragment } private void triggerRtpSession(final Account account, final Jid with, final String action) { - final Intent intent = new Intent(activity, RtpSessionActivity.class); + CallIntegrationConnectionService.placeCall(requireActivity(),account,with,RtpSessionActivity.actionToMedia(action)); + /*final Intent intent = new Intent(activity, RtpSessionActivity.class); intent.setAction(action); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); + startActivity(intent);*/ } private void handleAttachmentSelection(MenuItem item) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 5b5c82bae..8f546918a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -49,6 +49,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.AppRTCAudioManager; +import eu.siacs.conversations.services.CallIntegration; +import eu.siacs.conversations.services.CallIntegrationConnectionService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MainThreadExecutor; @@ -133,7 +135,7 @@ public class RtpSessionActivity extends XmppActivity } }; - private static Set actionToMedia(final String action) { + public static Set actionToMedia(final String action) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) { return ImmutableSet.of(Media.AUDIO, Media.VIDEO); } else { @@ -416,11 +418,11 @@ public class RtpSessionActivity extends XmppActivity if (Media.audioOnly(media)) { final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; - final AppRTCAudioManager audioManager = - rtpConnection == null ? null : rtpConnection.getAudioManager(); - if (audioManager == null - || audioManager.getSelectedAudioDevice() - == AppRTCAudioManager.AudioDevice.EARPIECE) { + final CallIntegration callIntegration = + rtpConnection == null ? null : rtpConnection.getCallIntegration(); + if (callIntegration == null + || callIntegration.getSelectedAudioDevice() + == CallIntegration.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } } @@ -466,8 +468,8 @@ public class RtpSessionActivity extends XmppActivity } private void putProximityWakeLockInProperState( - final AppRTCAudioManager.AudioDevice audioDevice) { - if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { + final CallIntegration.AudioDevice audioDevice) { + if (audioDevice == CallIntegration.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } else { releaseProximityWakeLock(); @@ -581,12 +583,7 @@ public class RtpSessionActivity extends XmppActivity .getJingleConnectionManager() .proposeJingleRtpSession(account, with, media); } else { - final String sessionId = - xmppConnectionService - .getJingleConnectionManager() - .initializeRtpSession(account, with, media); - initializeActivityWithRunningRtpSession(account, with, sessionId); - resetIntent(account, with, sessionId); + throw new IllegalStateException("We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!"); } putScreenInCallMode(media); } @@ -1032,10 +1029,10 @@ public class RtpSessionActivity extends XmppActivity updateInCallButtonConfigurationVideo( rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); } else { - final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); + final CallIntegration callIntegration = requireRtpConnection().getCallIntegration(); updateInCallButtonConfigurationSpeaker( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size()); + callIntegration.getSelectedAudioDevice(), + callIntegration.getAudioDevices().size()); this.binding.inCallActionFarRight.setVisibility(View.GONE); } if (media.contains(Media.AUDIO)) { @@ -1053,7 +1050,7 @@ public class RtpSessionActivity extends XmppActivity @SuppressLint("RestrictedApi") private void updateInCallButtonConfigurationSpeaker( - final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { + final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE -> { this.binding.inCallActionRight.setImageResource( @@ -1294,19 +1291,19 @@ public class RtpSessionActivity extends XmppActivity private void switchToEarpiece(View view) { requireRtpConnection() - .getAudioManager() - .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); + .getCallIntegration() + .setAudioDevice(CallIntegration.AudioDevice.EARPIECE); acquireProximityWakeLock(); } private void switchToSpeaker(View view) { requireRtpConnection() - .getAudioManager() - .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + .getCallIntegration() + .setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); releaseProximityWakeLock(); } - private void retry(View view) { + private void retry(final View view) { final Intent intent = getIntent(); final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); @@ -1315,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity final Set media = actionToMedia(lastAction == null ? action : lastAction); this.rtpConnectionReference = null; Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString()); - proposeJingleRtpSession(account, with, media); + CallIntegrationConnectionService.placeCall(this,account,with,media); } private void exit(final View view) { @@ -1411,8 +1408,8 @@ public class RtpSessionActivity extends XmppActivity @Override public void onAudioDeviceChanged( - final AppRTCAudioManager.AudioDevice selectedAudioDevice, - final Set availableAudioDevices) { + final CallIntegration.AudioDevice selectedAudioDevice, + final Set availableAudioDevices) { Log.d( Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" @@ -1428,11 +1425,11 @@ public class RtpSessionActivity extends XmppActivity "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) { - final AppRTCAudioManager audioManager = - requireRtpConnection().getAudioManager(); + final CallIntegration callIntegration = + requireRtpConnection().getCallIntegration(); updateInCallButtonConfigurationSpeaker( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size()); + callIntegration.getSelectedAudioDevice(), + callIntegration.getAudioDevices().size()); } Log.d( Config.LOGTAG, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 23d4f175b..d4c9189eb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import android.os.Bundle; +import android.telecom.TelecomManager; import android.util.Base64; import android.util.Log; @@ -21,6 +23,8 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.CallIntegration; +import eu.siacs.conversations.services.CallIntegrationConnectionService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -135,6 +139,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } connections.put(id, connection); + + CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + mXmppConnectionService.updateConversationUi(); connection.deliverPacket(packet); } else { @@ -148,12 +155,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public boolean isBusy() { - if (mXmppConnectionService.isPhoneInCall()) { - return true; - } for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { - if (((JingleRtpConnection) connection).isTerminated()) { + if (connection.isTerminated()) { continue; } return true; @@ -181,17 +185,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { return false; } - public void notifyPhoneCallStarted() { - for (AbstractJingleConnection connection : connections.values()) { - if (connection instanceof JingleRtpConnection rtpConnection) { - if (rtpConnection.isTerminated()) { - continue; - } - rtpConnection.notifyPhoneCall(); - } - } - } - private Optional findMatchingSessionProposal( final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { @@ -390,6 +383,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); + + CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); // TODO actually do the automatic accept?! } else { Log.d( @@ -439,6 +434,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); + + CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); } } else { Log.d( @@ -457,7 +454,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (proposal != null) { rtpSessionProposals.remove(proposal); final JingleRtpConnection rtpConnection = - new JingleRtpConnection(this, id, account.getJid()); + new JingleRtpConnection(this, id, account.getJid(), proposal.callIntegration); rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); @@ -490,6 +487,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (proposal != null && rtpSessionProposals.remove(proposal) != null) { + proposal.callIntegration.busy(); writeLogMissedOutgoing( account, proposal.with, proposal.sessionId, serverMsgId, timestamp); toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); @@ -628,10 +626,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Optional.absent(); } - void finishConnection(final AbstractJingleConnection connection) { - this.connections.remove(connection.getId()); - } - void finishConnectionOrThrow(final AbstractJingleConnection connection) { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { @@ -680,6 +674,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { + ": retracting rtp session proposal with " + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); + rtpSessionProposal.callIntegration.retracted(); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); writeLogMissedOutgoing( @@ -691,7 +686,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { mXmppConnectionService.sendMessagePacket(account, messagePacket); } - public String initializeRtpSession( + public JingleRtpConnection initializeRtpSession( final Account account, final Jid with, final Set media) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with); final JingleRtpConnection rtpConnection = @@ -699,15 +694,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.setProposedMedia(media); this.connections.put(id, rtpConnection); rtpConnection.sendSessionInitiate(); - return id.sessionId; + return rtpConnection; } - public void proposeJingleRtpSession( + public RtpSessionProposal proposeJingleRtpSession( final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : + for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { - RtpSessionProposal proposal = entry.getKey(); + final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); if (preexistingState != null @@ -716,7 +711,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, with, proposal.sessionId, endUserState); - return; + return proposal; } } } @@ -725,19 +720,23 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d( Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us"); - return; + // TODO return something that we can parse the connection of of + return null; } throw new IllegalStateException( "There is already a running RTP session. This should have been caught by the UI"); } + final CallIntegration callIntegration = new CallIntegration(mXmppConnectionService.getApplicationContext()); + callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); final RtpSessionProposal proposal = - RtpSessionProposal.of(account, with.asBareJid(), media); + RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); + return proposal; } } @@ -826,6 +825,21 @@ public class JingleConnectionManager extends AbstractConnectionManager { return null; } + public JingleRtpConnection findJingleRtpConnection(final Account account, final Jid with) { + for (final AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleRtpConnection rtpConnection) { + if (rtpConnection.isTerminated()) { + continue; + } + final var id = rtpConnection.getId(); + if (id.account == account && account.getJid().equals(with)) { + return rtpConnection; + } + } + } + return null; + } + private void resendSessionProposals(final Account account) { synchronized (this.rtpSessionProposals) { for (final Map.Entry entry : @@ -865,7 +879,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } this.rtpSessionProposals.put(sessionProposal, target); final RtpEndUserState endUserState = target.toEndUserState(); - toneManager.transition(endUserState, sessionProposal.media); + if (endUserState == RtpEndUserState.RINGING) { + sessionProposal.callIntegration.setDialing(); + } + //toneManager.transition(endUserState, sessionProposal.media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, sessionProposal.with, sessionProposal.sessionId, endUserState); Log.d( @@ -994,16 +1011,18 @@ public class JingleConnectionManager extends AbstractConnectionManager { public final String sessionId; public final Set media; private final Account account; + private final CallIntegration callIntegration; - private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) { + private RtpSessionProposal(Account account, Jid with, String sessionId, Set media, final CallIntegration callIntegration) { this.account = account; this.with = with; this.sessionId = sessionId; this.media = media; + this.callIntegration = callIntegration; } - public static RtpSessionProposal of(Account account, Jid with, Set media) { - return new RtpSessionProposal(account, with, nextRandomId(), media); + public static RtpSessionProposal of(Account account, Jid with, Set media, final CallIntegration callIntegration) { + return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration); } @Override @@ -1035,5 +1054,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { public String getSessionId() { return sessionId; } + + public CallIntegration getCallIntegration() { + return this.callIntegration; + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 80d7d2118..8d5aa8dfd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import android.telecom.Call; +import android.telecom.TelecomManager; import android.util.Log; import androidx.annotation.NonNull; @@ -12,13 +14,11 @@ import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -34,7 +34,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.services.AppRTCAudioManager; -import eu.siacs.conversations.utils.IP; +import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; @@ -67,7 +67,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class JingleRtpConnection extends AbstractJingleConnection - implements WebRTCWrapper.EventCallback { + implements WebRTCWrapper.EventCallback, CallIntegration.Callback { public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( @@ -78,6 +78,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private final Queue>> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); + private final CallIntegration callIntegration; private final Message message; private Set proposedMedia; @@ -90,7 +91,13 @@ public class JingleRtpConnection extends AbstractJingleConnection private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; - JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { + JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) { + this(jingleConnectionManager, id, initiator, new CallIntegration(jingleConnectionManager.getXmppConnectionService().getApplicationContext())); + this.callIntegration.setAddress(CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED); + this.callIntegration.setInitialized(); + } + + JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator, final CallIntegration callIntegration) { super(jingleConnectionManager, id, initiator); final Conversation conversation = jingleConnectionManager @@ -102,6 +109,8 @@ public class JingleRtpConnection extends AbstractJingleConnection isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, Message.TYPE_RTP_SESSION, id.sessionId); + this.callIntegration = callIntegration; + this.callIntegration.setCallback(this); } @Override @@ -1158,6 +1167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection target = State.SESSION_INITIALIZED_PRE_APPROVED; } else { target = State.SESSION_INITIALIZED; + setProposedMedia(contentMap.getMedia()); } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); @@ -1628,7 +1638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection + from + " for " + media); - this.proposedMedia = Sets.newHashSet(media); + this.setProposedMedia(Sets.newHashSet(media)); })) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); @@ -1648,6 +1658,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void startRinging() { + this.callIntegration.setRinging(); Log.d( Config.LOGTAG, id.account.getJid().asBareJid() @@ -1657,6 +1668,9 @@ public class JingleRtpConnection extends AbstractJingleConnection ringingTimeoutFuture = jingleConnectionManager.schedule( this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); + if (CallIntegration.selfManaged()) { + return; + } xmppConnectionService.getNotificationService().startRinging(id, getMedia()); } @@ -2054,6 +2068,56 @@ public class JingleRtpConnection extends AbstractJingleConnection }; } + private boolean isPeerConnectionConnected() { + try { + return webRTCWrapper.getState() == PeerConnection.PeerConnectionState.CONNECTED; + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + return false; + } + } + + private void updateCallIntegrationState() { + switch (this.state) { + case NULL, PROPOSED, SESSION_INITIALIZED -> { + if (isInitiator()) { + this.callIntegration.setDialing(); + } else { + this.callIntegration.setRinging(); + } + } + case PROCEED, SESSION_INITIALIZED_PRE_APPROVED -> { + if (isInitiator()) { + this.callIntegration.setDialing(); + } else { + this.callIntegration.setInitialized(); + } + } + case SESSION_ACCEPTED -> { + if (isPeerConnectionConnected()) { + this.callIntegration.setActive(); + } else { + this.callIntegration.setInitialized(); + } + } + case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> { + if (isInitiator()) { + this.callIntegration.busy(); + } else { + this.callIntegration.rejected(); + } + } + case TERMINATED_SUCCESS -> this.callIntegration.success(); + case ACCEPTED -> this.callIntegration.accepted(); + case RETRACTED, RETRACTED_RACED, TERMINATED_CANCEL_OR_TIMEOUT -> this.callIntegration + .retracted(); + case TERMINATED_CONNECTIVITY_ERROR, + TERMINATED_APPLICATION_FAILURE, + TERMINATED_SECURITY_ERROR -> this.callIntegration.error(); + default -> throw new IllegalStateException( + String.format("%s is not handled", this.state)); + } + } + public ContentAddition getPendingContentAddition() { final RtpContentMap in = this.incomingContentAdd; final RtpContentMap out = this.outgoingContentAdd; @@ -2135,15 +2199,6 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - public void notifyPhoneCall() { - Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections"); - if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) { - rejectCall(); - } else { - endCall(); - } - } - public synchronized void rejectCall() { if (isTerminated()) { Log.w( @@ -2537,8 +2592,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { final RtpContentMap activeContents = rtpContentMap.activeContents(); setLocalContentMap(activeContents); - this.webRTCWrapper.switchSpeakerPhonePreference( - AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())); + // TODO change audio device on callIntegration was (`switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())`) updateEndUserState(); } @@ -2571,8 +2625,9 @@ public class JingleRtpConnection extends AbstractJingleConnection return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } - public AppRTCAudioManager getAudioManager() { - return webRTCWrapper.getAudioManager(); + + public CallIntegration getCallIntegration() { + return this.callIntegration; } public boolean isMicrophoneEnabled() { @@ -2603,10 +2658,26 @@ public class JingleRtpConnection extends AbstractJingleConnection return webRTCWrapper.switchCamera(); } + @Override + public void onCallIntegrationShowIncomingCallUi() { + xmppConnectionService.getNotificationService().startRinging(id, getMedia()); + } + + @Override + public void onCallIntegrationDisconnect() { + Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections"); + if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) { + rejectCall(); + } else { + endCall(); + } + } + @Override public void onAudioDeviceChanged( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices) { + final CallIntegration.AudioDevice selectedAudioDevice, + final Set availableAudioDevices) { + Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")"); xmppConnectionService.notifyJingleRtpConnectionUpdate( selectedAudioDevice, availableAudioDevices); } @@ -2614,6 +2685,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); + this.updateCallIntegrationState(); xmppConnectionService.notifyJingleRtpConnectionUpdate( id.account, id.with, id.sessionId, endUserState); } @@ -2670,6 +2742,7 @@ public class JingleRtpConnection extends AbstractJingleConnection protected void finish() { if (isTerminated()) { this.cancelRingingTimeout(); + this.callIntegration.verifyDisconnected(); this.webRTCWrapper.verifyClosed(); this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); super.finish(); @@ -2724,6 +2797,7 @@ public class JingleRtpConnection extends AbstractJingleConnection void setProposedMedia(final Set media) { this.proposedMedia = media; + this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); } public void fireStateUpdate() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 24ed790dd..885820460 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -9,7 +9,7 @@ public enum RtpEndUserState { FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received - ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through + ENDING_CALL, //libwebrt says 'closed' but session-terminate has not gone through ENDED, //close UI DECLINED_OR_BUSY, //other party declined; no retry button CONNECTIVITY_ERROR, //network error; retry button diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index da5b9ab2b..fb82b7219 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -89,7 +89,8 @@ class ToneManager { } switch (state) { case RINGING: - scheduleWaitingTone(); + // ringing can be removed as this is now handled by 'CallIntegration' + //scheduleWaitingTone(); break; case CONNECTED: scheduleConnected(); 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 cb0c8579d..fa504ed19 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -16,6 +16,7 @@ import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.services.AppRTCAudioManager; +import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.services.XmppConnectionService; import org.webrtc.AudioSource; @@ -83,16 +84,6 @@ public class WebRTCWrapper { private final EventCallback eventCallback; private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); private final Queue iceCandidates = new LinkedList<>(); - private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = - new AppRTCAudioManager.AudioManagerEvents() { - @Override - public void onAudioDeviceChanged( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices) { - eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); - } - }; - private final Handler mainHandler = new Handler(Looper.getMainLooper()); private TrackWrapper localAudioTrack = null; private TrackWrapper localVideoTrack = null; private VideoTrack remoteVideoTrack = null; @@ -214,7 +205,6 @@ public class WebRTCWrapper { }; @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; - private AppRTCAudioManager appRTCAudioManager = null; private ToneManager toneManager = null; private Context context = null; private EglBase eglBase = null; @@ -251,15 +241,6 @@ public class WebRTCWrapper { } this.context = service; this.toneManager = service.getJingleConnectionManager().toneManager; - mainHandler.post( - () -> { - appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference); - toneManager.setAppRtcAudioManagerHasControl(true); - appRTCAudioManager.start(audioManagerEvents); - eventCallback.onAudioDeviceChanged( - appRTCAudioManager.getSelectedAudioDevice(), - appRTCAudioManager.getAudioDevices()); - }); } synchronized void initializePeerConnection( @@ -462,16 +443,11 @@ public class WebRTCWrapper { final PeerConnection peerConnection = this.peerConnection; final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper; - final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { this.peerConnection = null; dispose(peerConnection); } - if (audioManager != null) { - toneManager.setAppRtcAudioManagerHasControl(false); - mainHandler.post(audioManager::stop); - } this.localVideoTrack = null; this.remoteVideoTrack = null; if (videoSourceWrapper != null) { @@ -498,8 +474,8 @@ public class WebRTCWrapper { || this.eglBase != null || this.localVideoTrack != null || this.remoteVideoTrack != null) { - final IllegalStateException e = - new IllegalStateException("WebRTCWrapper hasn't been closed properly"); + final AssertionError e = + new AssertionError("WebRTCWrapper hasn't been closed properly"); Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e); throw e; } @@ -750,27 +726,15 @@ public class WebRTCWrapper { return context; } - AppRTCAudioManager getAudioManager() { - return appRTCAudioManager; - } - void execute(final Runnable command) { this.executorService.execute(command); } - public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) { - mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference)); - } - public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); void onConnectionChange(PeerConnection.PeerConnectionState newState); - void onAudioDeviceChanged( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices); - void onRenegotiationNeeded(); } From 4378f8931bbe13656ee7be28a702b86bf7813075 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 11:55:11 +0100 Subject: [PATCH 132/556] add Config flag to debug direct call init --- .../java/eu/siacs/conversations/Config.java | 137 +++++++++--------- .../ui/ConversationFragment.java | 9 +- 2 files changed, 69 insertions(+), 77 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 982bd4781..6c9e5dd68 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -3,15 +3,15 @@ package eu.siacs.conversations; import android.graphics.Bitmap; import android.net.Uri; +import eu.siacs.conversations.crypto.XmppDomainVerifier; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.chatstate.ChatState; + import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; -import eu.siacs.conversations.crypto.XmppDomainVerifier; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; - public final class Config { private static final int UNENCRYPTED = 1; private static final int OPENPGP = 2; @@ -46,34 +46,32 @@ public final class Config { public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im"); public static final Uri HELP = Uri.parse("https://help.conversations.im"); - - public static final String DOMAIN_LOCK = null; //only allow account creation for this domain + public static final String DOMAIN_LOCK = null; // only allow account creation for this domain public static final String MAGIC_CREATE_DOMAIN = "conversations.im"; public static final Jid QUICKSY_DOMAIN = Jid.of("quicksy.im"); public static final String CHANNEL_DISCOVERY = "https://search.jabber.network"; - public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox + public static final boolean DISALLOW_REGISTRATION_IN_UI = false; // hide the register checkbox public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false; - public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true + public static final boolean ALLOW_NON_TLS_CONNECTIONS = + false; // very dangerous. you should have a good reason to set this to true public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5; - public static final boolean QUICKSTART_ENABLED = true; - //Notification settings + // Notification settings public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false; public static final boolean SUPPRESS_ERROR_NOTIFICATION = false; - public static final boolean DISABLE_BAN = false; // disables the ability to ban users from rooms public static final int PING_MAX_INTERVAL = 300; - public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs; + public static final int IDLE_PING_INTERVAL = 600; // 540 is minimum according to docs; public static final int PING_MIN_INTERVAL = 30; public static final int LOW_PING_TIMEOUT = 1; // used after push received public static final int PING_TIMEOUT = 15; @@ -83,10 +81,10 @@ public final class Config { public static final int CONNECT_DISCO_TIMEOUT = 20; public static final int MINI_GRACE_PERIOD = 750; - public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0 + public static final boolean XEP_0392 = true; // enables XEP-0392 v0.6.0 - - // media file formats. Homogenous Android or Conversations only deployments can switch to opus and webp + // media file formats. Homogenous Android or Conversations only deployments can switch to opus + // and webp public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG; public static final int AVATAR_CHAR_LIMIT = 9400; @@ -106,30 +104,39 @@ public final class Config { public static final int REFRESH_UI_INTERVAL = 500; public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096; - public static final int MAX_STORAGE_MESSAGE_CHARS = 2 * 1024 * 1024; //2MB + public static final int MAX_STORAGE_MESSAGE_CHARS = 2 * 1024 * 1024; // 2MB public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; - //remove *other* omemo devices from *your* device list announcement after not seeing any activity from them for 42 days. They will automatically add themselves after coming back online. + // remove *other* omemo devices from *your* device list announcement after not seeing any + // activity from them for 42 days. They will automatically add themselves after coming back + // online. public static final long OMEMO_AUTO_EXPIRY = 42 * MILLISECONDS_IN_DAY; public static final boolean REMOVE_BROKEN_DEVICES = false; public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true; - public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback) + public static final boolean DISABLE_PROXY_LOOKUP = + false; // disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback) public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; + public static final boolean USE_JINGLE_DIRECT_INIT = true; public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts - public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background - public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption + public static final boolean BACKGROUND_STANZA_LOGGING = + false; // log all stanzas that were received while the app is in background + public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = + true; // setting to true might increase power consumption public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false; - public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys - public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO + public static final boolean X509_VERIFICATION = + false; // use x509 certificates to verify OMEMO keys + public static final boolean REQUIRE_RTP_VERIFICATION = + false; // require a/v calls to be verified with OMEMO - public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments + public static final boolean ONLY_INTERNAL_STORAGE = + false; // use internal storage instead of sdcard to save attachments public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; public static final boolean MUC_LEAVE_BEFORE_JOIN = false; @@ -145,69 +152,61 @@ public final class Config { public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes public static final String[] ENABLED_CIPHERS = { - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA256", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", - - "TLS_DHE_RSA_WITH_CAMELLIA_256_SHA", - - // Fallback. - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_RSA_WITH_AES_128_GCM_SHA384", - "TLS_RSA_WITH_AES_256_GCM_SHA256", - "TLS_RSA_WITH_AES_256_GCM_SHA384", - "TLS_RSA_WITH_AES_128_CBC_SHA256", - "TLS_RSA_WITH_AES_128_CBC_SHA384", - "TLS_RSA_WITH_AES_256_CBC_SHA256", - "TLS_RSA_WITH_AES_256_CBC_SHA384", - "TLS_RSA_WITH_AES_128_CBC_SHA", - "TLS_RSA_WITH_AES_256_CBC_SHA", + // Fallback. + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA384", + "TLS_RSA_WITH_AES_256_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA384", + "TLS_RSA_WITH_AES_256_CBC_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", }; public static final String[] WEAK_CIPHER_PATTERNS = { - "_NULL_", - "_EXPORT_", - "_anon_", - "_RC4_", - "_DES_", - "_MD5", + "_NULL_", "_EXPORT_", "_anon_", "_RC4_", "_DES_", "_MD5", }; public static class OMEMO_EXCEPTIONS { - //if the own account matches one of the following domains OMEMO won’t be turned on automatically + // if the own account matches one of the following domains OMEMO won’t be turned on + // automatically public static final List ACCOUNT_DOMAINS = Collections.singletonList("s.ms"); - //if the contacts domain matches one of the following domains OMEMO won’t be turned on automatically - //can be used for well known, widely used gateways - private static final List CONTACT_DOMAINS = Arrays.asList( - "cheogram.com", - "*.covid.monal.im" - ); + // if the contacts domain matches one of the following domains OMEMO won’t be turned on + // automatically + // can be used for well known, widely used gateways + private static final List CONTACT_DOMAINS = + Arrays.asList("cheogram.com", "*.covid.monal.im"); public static boolean matchesContactDomain(final String domain) { return XmppDomainVerifier.matchDomain(domain, CONTACT_DOMAINS); } } - private Config() { - } + private Config() {} public static final class Map { - public final static double INITIAL_ZOOM_LEVEL = 4; - public final static double FINAL_ZOOM_LEVEL = 15; - public final static int MY_LOCATION_INDICATOR_SIZE = 10; - public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 3; - public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms - public final static float LOCATION_FIX_SPACE_DELTA = 10; // m - public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms + public static final double INITIAL_ZOOM_LEVEL = 4; + public static final double FINAL_ZOOM_LEVEL = 15; + public static final int MY_LOCATION_INDICATOR_SIZE = 10; + public static final int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 3; + public static final long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms + public static final float LOCATION_FIX_SPACE_DELTA = 10; // m + public static final int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms } // How deep nested quotes should be displayed. '2' means one quote nested in another. diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ccb08f95f..01932a04e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1633,7 +1633,7 @@ public class ConversationFragment extends XmppFragment activity.xmppConnectionService.updateAccount(account); } final Contact contact = conversation.getContact(); - if (RtpCapability.jmiSupport(contact)) { + if (Config.USE_JINGLE_DIRECT_INIT && RtpCapability.jmiSupport(contact)) { triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action); } else { final RtpCapability.Capability capability; @@ -1654,13 +1654,6 @@ public class ConversationFragment extends XmppFragment private void triggerRtpSession(final Account account, final Jid with, final String action) { CallIntegrationConnectionService.placeCall(requireActivity(),account,with,RtpSessionActivity.actionToMedia(action)); - /*final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(action); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent);*/ } private void handleAttachmentSelection(MenuItem item) { From d20cc87bda0b82ea8bf27c76dfdfbf4a93f31785 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 12:09:56 +0100 Subject: [PATCH 133/556] retract proposal when accepting other call --- .../xmpp/jingle/JingleConnectionManager.java | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d4c9189eb..5edd461fb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.Bundle; -import android.telecom.TelecomManager; import android.util.Base64; import android.util.Log; @@ -31,9 +29,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; -import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; -import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; @@ -384,7 +380,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); - CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + CallIntegrationConnectionService.addNewIncomingCall( + getXmppConnectionService(), id); // TODO actually do the automatic accept?! } else { Log.d( @@ -435,7 +432,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); - CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + CallIntegrationConnectionService.addNewIncomingCall( + getXmppConnectionService(), id); } } else { Log.d( @@ -454,7 +452,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (proposal != null) { rtpSessionProposals.remove(proposal); final JingleRtpConnection rtpConnection = - new JingleRtpConnection(this, id, account.getJid(), proposal.callIntegration); + new JingleRtpConnection( + this, id, account.getJid(), proposal.callIntegration); rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); @@ -726,10 +725,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { throw new IllegalStateException( "There is already a running RTP session. This should have been caught by the UI"); } - final CallIntegration callIntegration = new CallIntegration(mXmppConnectionService.getApplicationContext()); + final CallIntegration callIntegration = + new CallIntegration(mXmppConnectionService.getApplicationContext()); callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration); + callIntegration.setCallback(new ProposalStateCallback(proposal)); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); @@ -775,7 +776,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { sid = null; } if (sid == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": unable to deliver ibb packet. missing sid"); account.getXmppConnection() .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); return; @@ -799,7 +802,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": unable to deliver ibb packet with sid=" + sid); account.getXmppConnection() .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } @@ -882,7 +887,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (endUserState == RtpEndUserState.RINGING) { sessionProposal.callIntegration.setDialing(); } - //toneManager.transition(endUserState, sessionProposal.media); + // toneManager.transition(endUserState, sessionProposal.media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, sessionProposal.with, sessionProposal.sessionId, endUserState); Log.d( @@ -1013,7 +1018,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { private final Account account; private final CallIntegration callIntegration; - private RtpSessionProposal(Account account, Jid with, String sessionId, Set media, final CallIntegration callIntegration) { + private RtpSessionProposal( + Account account, + Jid with, + String sessionId, + Set media, + final CallIntegration callIntegration) { this.account = account; this.with = with; this.sessionId = sessionId; @@ -1021,8 +1031,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.callIntegration = callIntegration; } - public static RtpSessionProposal of(Account account, Jid with, Set media, final CallIntegration callIntegration) { - return new RtpSessionProposal(account, with, nextRandomId(), media,callIntegration); + public static RtpSessionProposal of( + Account account, + Jid with, + Set media, + final CallIntegration callIntegration) { + return new RtpSessionProposal(account, with, nextRandomId(), media, callIntegration); } @Override @@ -1059,4 +1073,27 @@ public class JingleConnectionManager extends AbstractConnectionManager { return this.callIntegration; } } + + public class ProposalStateCallback implements CallIntegration.Callback { + + private final RtpSessionProposal proposal; + + public ProposalStateCallback(final RtpSessionProposal proposal) { + this.proposal = proposal; + } + + @Override + public void onCallIntegrationShowIncomingCallUi() {} + + @Override + public void onCallIntegrationDisconnect() { + Log.d(Config.LOGTAG, "a phone call has just been started. retracting proposal"); + retractSessionProposal(this.proposal); + } + + @Override + public void onAudioDeviceChanged( + CallIntegration.AudioDevice selectedAudioDevice, + Set availableAudioDevices) {} + } } From a44ad6015dd60d255cd7a5a1000a7483d11769ea Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 18:05:43 +0100 Subject: [PATCH 134/556] update UI with correct state after UI gets invoked with ACTION_VIEW --- .../services/CallIntegrationConnectionService.java | 13 +++++++++++-- .../siacs/conversations/ui/RtpSessionActivity.java | 8 ++++---- .../xmpp/jingle/JingleConnectionManager.java | 13 ++++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index cfd6ae603..28f59a060 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -29,6 +29,7 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; +import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import java.util.Collection; import java.util.Collections; @@ -83,6 +84,7 @@ public class CallIntegrationConnectionService extends ConnectionService { } final Account account = service.findAccountByUuid(phoneAccountHandle.getId()); final Intent intent = new Intent(this, RtpSessionActivity.class); + intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, jid.toEscapedString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -93,10 +95,17 @@ public class CallIntegrationConnectionService extends ConnectionService { service.getJingleConnectionManager() .proposeJingleRtpSession(account, jid, media); + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, + RtpEndUserState.FINDING_DEVICE.toString()); if (Media.audioOnly(media)) { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_ACTION, + RtpSessionActivity.ACTION_MAKE_VOICE_CALL); } else { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_ACTION, + RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } callIntegration = proposal.getCallIntegration(); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 8f546918a..598232467 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -467,8 +467,7 @@ public class RtpSessionActivity extends XmppActivity } } - private void putProximityWakeLockInProperState( - final CallIntegration.AudioDevice audioDevice) { + private void putProximityWakeLockInProperState(final CallIntegration.AudioDevice audioDevice) { if (audioDevice == CallIntegration.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } else { @@ -583,7 +582,8 @@ public class RtpSessionActivity extends XmppActivity .getJingleConnectionManager() .proposeJingleRtpSession(account, with, media); } else { - throw new IllegalStateException("We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!"); + throw new IllegalStateException( + "We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!"); } putScreenInCallMode(media); } @@ -1312,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity final Set media = actionToMedia(lastAction == null ? action : lastAction); this.rtpConnectionReference = null; Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString()); - CallIntegrationConnectionService.placeCall(this,account,with,media); + CallIntegrationConnectionService.placeCall(this, account, with, media); } private void exit(final View view) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5edd461fb..5a5117a4c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -636,17 +636,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public boolean fireJingleRtpConnectionStateUpdates() { - boolean firedUpdates = false; for (final AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection jingleRtpConnection) { if (jingleRtpConnection.isTerminated()) { continue; } jingleRtpConnection.fireStateUpdate(); - firedUpdates = true; + return true; } } - return firedUpdates; + return false; } public void retractSessionProposal(final Account account, final Jid with) { @@ -745,8 +744,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { synchronized (this.rtpSessionProposals) { for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + final var state = entry.getValue(); final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { + // CallIntegrationConnectionService starts RtpSessionActivity with ACTION_VIEW + // and an EXTRA_LAST_REPORTED_STATE of DISCOVERING devices. however due to + // possible race conditions the state might have already moved on so we are going + // to update the UI + final RtpEndUserState endUserState = state.toEndUserState(); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, proposal.with, proposal.sessionId, endUserState); return true; } } From dfa389f61f3e6c04ad67098838ff9f8c0d6320e2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 14 Jan 2024 18:17:55 +0100 Subject: [PATCH 135/556] update UI in case proposal gets retracted by system --- .../xmpp/jingle/JingleConnectionManager.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5a5117a4c..a4781870c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -663,7 +663,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) { + private void retractSessionProposal(final RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); Log.d( @@ -673,6 +673,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); rtpSessionProposal.callIntegration.retracted(); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + rtpSessionProposal.with, + rtpSessionProposal.sessionId, + RtpEndUserState.RETRACTED); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); writeLogMissedOutgoing( @@ -749,7 +754,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (proposal.account == account && with.asBareJid().equals(proposal.with)) { // CallIntegrationConnectionService starts RtpSessionActivity with ACTION_VIEW // and an EXTRA_LAST_REPORTED_STATE of DISCOVERING devices. however due to - // possible race conditions the state might have already moved on so we are going + // possible race conditions the state might have already moved on so we are + // going // to update the UI final RtpEndUserState endUserState = state.toEndUserState(); mXmppConnectionService.notifyJingleRtpConnectionUpdate( From b9e4296321581f396af52e29e2eb3b340c88e73a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 11:48:56 +0100 Subject: [PATCH 136/556] reformat debug info --- .../conversations/xmpp/jingle/JingleConnectionManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index a4781870c..8153dc254 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -512,8 +512,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d( Config.LOGTAG, account.getJid() - + ": retrieved out of order jingle message from " + + ": received out of order jingle message from=" + from + + ", message=" + message + ", addressedDirectly=" + addressedDirectly); From f119c36bffa318ca16a78728120fbaf222aeef92 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 12:50:50 +0100 Subject: [PATCH 137/556] (un)register phone account on xmpp account creation/deletion --- .../services/CallIntegrationConnectionService.java | 4 ++++ .../siacs/conversations/services/XmppConnectionService.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 28f59a060..01f246844 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -181,6 +181,10 @@ public class CallIntegrationConnectionService extends ConnectionService { } } + public static void unregisterPhoneAccount(final Context context, final Account account) { + context.getSystemService(TelecomManager.class).unregisterPhoneAccount(getHandle(context, account)); + } + public static PhoneAccountHandle getHandle(final Context context, final Account account) { final var competentName = new ComponentName(context, CallIntegrationConnectionService.class); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5f48fca58..41c467b15 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -2460,6 +2460,7 @@ public class XmppConnectionService extends Service { public void createAccount(final Account account) { account.initAccountServices(this); databaseBackend.createAccount(account); + CallIntegrationConnectionService.registerPhoneAccount(this, account); this.accounts.add(account); this.reconnectAccountInBackground(account); updateAccountUi(); @@ -2644,6 +2645,7 @@ public class XmppConnectionService extends Service { }; mDatabaseWriterExecutor.execute(runnable); this.accounts.remove(account); + CallIntegrationConnectionService.unregisterPhoneAccount(this, account); this.mRosterSyncTaskManager.clear(account); updateAccountUi(); mNotificationService.updateErrorNotification(); From 6975299a28e7915c0f88e1e2f59f66eea7fae8bc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 13:20:39 +0100 Subject: [PATCH 138/556] hook into onAnswer/onReject of CallIntegration the Operating System shows a notification on our behalf if there is currently a call going on that can not be put on hold (For example a Quicksy call is going on while a Conversations call is coming on) --- .../services/CallIntegration.java | 9 ++- .../xmpp/jingle/JingleConnectionManager.java | 10 +++ .../xmpp/jingle/JingleRtpConnection.java | 81 ++++++++++++++----- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index bf12f5f4b..fbd3e8c2c 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -60,7 +60,7 @@ public class CallIntegration extends Connection { @Override public void onAnswer() { - Log.d(Config.LOGTAG, "onAnswer()"); + this.callback.onCallIntegrationAnswer(); } @Override @@ -71,12 +71,13 @@ public class CallIntegration extends Connection { @Override public void onReject() { - Log.d(Config.LOGTAG, "onReject()"); + this.callback.onCallIntegrationReject(); } @Override public void onReject(final String replyMessage) { Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")"); + this.callback.onCallIntegrationReject(); } @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @@ -404,5 +405,9 @@ public class CallIntegration extends Connection { void onAudioDeviceChanged( CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices); + + void onCallIntegrationReject(); + + void onCallIntegrationAnswer(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 8153dc254..9101b939a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1109,5 +1109,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void onAudioDeviceChanged( CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices) {} + + @Override + public void onCallIntegrationReject() { + + } + + @Override + public void onCallIntegrationAnswer() { + + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 8d5aa8dfd..2ecd91253 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,6 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; -import android.telecom.Call; +import android.content.Intent; import android.telecom.TelecomManager; import android.util.Log; @@ -35,6 +35,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.CallIntegration; +import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; @@ -75,7 +76,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private static final long BUSY_TIME_OUT = 30; private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final Queue>> + private final Queue< + Map.Entry>> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final CallIntegration callIntegration; @@ -91,13 +93,28 @@ public class JingleRtpConnection extends AbstractJingleConnection private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; - JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) { - this(jingleConnectionManager, id, initiator, new CallIntegration(jingleConnectionManager.getXmppConnectionService().getApplicationContext())); - this.callIntegration.setAddress(CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED); + JingleRtpConnection( + final JingleConnectionManager jingleConnectionManager, + final Id id, + final Jid initiator) { + this( + jingleConnectionManager, + id, + initiator, + new CallIntegration( + jingleConnectionManager + .getXmppConnectionService() + .getApplicationContext())); + this.callIntegration.setAddress( + CallIntegration.address(id.with.asBareJid()), TelecomManager.PRESENTATION_ALLOWED); this.callIntegration.setInitialized(); } - JingleRtpConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator, final CallIntegration callIntegration) { + JingleRtpConnection( + final JingleConnectionManager jingleConnectionManager, + final Id id, + final Jid initiator, + final CallIntegration callIntegration) { super(jingleConnectionManager, id, initiator); final Conversation conversation = jingleConnectionManager @@ -231,8 +248,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private void receiveTransportInfo( final JinglePacket jinglePacket, final RtpContentMap contentMap) { - final Set>> candidates = - contentMap.contents.entrySet(); + final Set>> + candidates = contentMap.contents.entrySet(); final RtpContentMap remote = getRemoteContentMap(); final Set remoteContentIds = remote == null ? Collections.emptySet() : remote.contents.keySet(); @@ -1004,14 +1021,17 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void processCandidates( - final Set>> contents) { - for (final Map.Entry> content : contents) { + final Set>> + contents) { + for (final Map.Entry> + content : contents) { processCandidate(content); } } private void processCandidate( - final Map.Entry> content) { + final Map.Entry> + content) { final RtpContentMap rtpContentMap = getRemoteContentMap(); final List indices = toIdentificationTags(rtpContentMap); final String sdpMid = content.getKey(); // aka content name @@ -1382,7 +1402,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void addIceCandidatesFromBlackLog() { - Map.Entry> foo; + Map.Entry> foo; while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidate(foo); Log.d( @@ -1960,12 +1980,10 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionTerminate(reason, null); } - protected void sendSessionTerminate(final Reason reason, final String text) { - sendSessionTerminate(reason,text, this::writeLogMessage); + sendSessionTerminate(reason, text, this::writeLogMessage); } - private void sendTransportInfo( final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; @@ -2351,7 +2369,6 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionAccept(); } - @Override protected synchronized boolean transition(final State target, final Runnable runnable) { if (super.transition(target, runnable)) { @@ -2592,7 +2609,6 @@ public class JingleRtpConnection extends AbstractJingleConnection private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { final RtpContentMap activeContents = rtpContentMap.activeContents(); setLocalContentMap(activeContents); - // TODO change audio device on callIntegration was (`switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia())`) updateEndUserState(); } @@ -2625,7 +2641,6 @@ public class JingleRtpConnection extends AbstractJingleConnection return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } - public CallIntegration getCallIntegration() { return this.callIntegration; } @@ -2673,11 +2688,37 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + @Override + public void onCallIntegrationReject() { + Log.d(Config.LOGTAG, "rejecting call from system notification / call integration"); + try { + rejectCall(); + } catch (final IllegalStateException e) { + Log.w(Config.LOGTAG, "race condition on rejecting call from notification", e); + } + } + + @Override + public void onCallIntegrationAnswer() { + // we need to start the UI to a) show it and b) be able to ask for permissions + final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class); + intent.setAction(RtpSessionActivity.ACTION_ACCEPT_CALL); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); + Log.d(Config.LOGTAG, "start activity to accept call from call integration"); + xmppConnectionService.startActivity(intent); + } + @Override public void onAudioDeviceChanged( final CallIntegration.AudioDevice selectedAudioDevice, final Set availableAudioDevices) { - Log.d(Config.LOGTAG,"onAudioDeviceChanged("+selectedAudioDevice+","+availableAudioDevices+")"); + Log.d( + Config.LOGTAG, + "onAudioDeviceChanged(" + selectedAudioDevice + "," + availableAudioDevices + ")"); xmppConnectionService.notifyJingleRtpConnectionUpdate( selectedAudioDevice, availableAudioDevices); } @@ -2732,7 +2773,7 @@ public class JingleRtpConnection extends AbstractJingleConnection onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); } } - + @Override protected void terminateTransport() { this.webRTCWrapper.close(); From ebb48e9320d07459d898556e3f48fca786b7201b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 15 Jan 2024 13:54:26 +0100 Subject: [PATCH 139/556] set correct video state for calls --- .../xmpp/jingle/JingleConnectionManager.java | 13 +++++++------ .../xmpp/jingle/JingleRtpConnection.java | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 9101b939a..fd5c4c9af 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.telecom.VideoProfile; import android.util.Base64; import android.util.Log; @@ -732,6 +733,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } final CallIntegration callIntegration = new CallIntegration(mXmppConnectionService.getApplicationContext()); + callIntegration.setVideoState( + Media.audioOnly(media) + ? VideoProfile.STATE_AUDIO_ONLY + : VideoProfile.STATE_BIDIRECTIONAL); callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration); @@ -1111,13 +1116,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { Set availableAudioDevices) {} @Override - public void onCallIntegrationReject() { - - } + public void onCallIntegrationReject() {} @Override - public void onCallIntegrationAnswer() { - - } + public void onCallIntegrationAnswer() {} } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 2ecd91253..dc0980e1b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Intent; import android.telecom.TelecomManager; +import android.telecom.VideoProfile; import android.util.Log; import androidx.annotation.NonNull; @@ -2838,6 +2839,10 @@ public class JingleRtpConnection extends AbstractJingleConnection void setProposedMedia(final Set media) { this.proposedMedia = media; + this.callIntegration.setVideoState( + Media.audioOnly(media) + ? VideoProfile.STATE_AUDIO_ONLY + : VideoProfile.STATE_BIDIRECTIONAL); this.callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); } From 6ba9208eea5a12ecbaf8dcbeb6a0453e5ec5995a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Jan 2024 13:56:39 +0100 Subject: [PATCH 140/556] switch audio device when switching to video --- .../eu/siacs/conversations/services/CallIntegration.java | 7 +++++++ .../conversations/xmpp/jingle/JingleRtpConnection.java | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index fbd3e8c2c..a4dffffaa 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -145,6 +145,13 @@ public class CallIntegration extends Connection { } } + public void setAudioDeviceWhenAvailable(final AudioDevice audioDevice) { + final var available = getAudioDevices(); + if (available.contains(audioDevice)) { + this.setAudioDevice(audioDevice); + } + } + @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) private Set getAudioDevicesUpsideDownCake() { return ImmutableSet.copyOf( diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index dc0980e1b..e9d750319 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2610,6 +2610,8 @@ public class JingleRtpConnection extends AbstractJingleConnection private void modifyLocalContentMap(final RtpContentMap rtpContentMap) { final RtpContentMap activeContents = rtpContentMap.activeContents(); setLocalContentMap(activeContents); + this.callIntegration.setAudioDeviceWhenAvailable( + CallIntegration.initialAudioDevice(activeContents.getMedia())); updateEndUserState(); } From d79fc1bb790ef70476886167575349035f2ff9a0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 16 Jan 2024 18:50:40 +0100 Subject: [PATCH 141/556] run some AppRTCAudioManager actions on main thread --- .../services/AppRTCAudioManager.java | 77 +------- .../services/AppRTCBluetoothManager.java | 2 - .../services/AppRTCProximitySensor.java | 171 ------------------ .../services/CallIntegration.java | 23 ++- .../CallIntegrationConnectionService.java | 4 +- .../xmpp/jingle/JingleRtpConnection.java | 3 +- .../xmpp/jingle/WebRTCWrapper.java | 4 +- 7 files changed, 30 insertions(+), 254 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index a472445d3..894b2ace6 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -23,6 +23,7 @@ import android.os.Build; import android.util.Log; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import org.webrtc.ThreadUtils; @@ -44,8 +45,6 @@ public class AppRTCAudioManager { private final Context apprtcContext; // Contains speakerphone setting: auto, true or false - @Nullable - private SpeakerPhonePreference speakerPhonePreference; // Handles all tasks related to Bluetooth headset devices. private final AppRTCBluetoothManager bluetoothManager; @Nullable @@ -70,12 +69,7 @@ public class AppRTCAudioManager { // TODO(henrika): always set to AudioDevice.NONE today. Add support for // explicit selection based on choice by userSelectedAudioDevice. private CallIntegration.AudioDevice userSelectedAudioDevice; - // Proximity sensor object. It measures the proximity of an object in cm - // relative to the view screen of a device and can therefore be used to - // assist device switching (close to ear <=> use headset earpiece if - // available, far from ear <=> use speaker phone). - @Nullable - private AppRTCProximitySensor proximitySensor; + // Contains a list of available audio devices. A Set collection is used to // avoid duplicate elements. private Set audioDevices = new HashSet<>(); @@ -86,7 +80,6 @@ public class AppRTCAudioManager { private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; public AppRTCAudioManager(final Context context) { - ThreadUtils.checkIsOnMainThread(); apprtcContext = context; audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); bluetoothManager = AppRTCBluetoothManager.create(context, this); @@ -98,28 +91,10 @@ public class AppRTCAudioManager { } else { defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } - // Create and initialize the proximity sensor. - // Tablet devices (e.g. Nexus 7) does not support proximity sensors. - // Note that, the sensor will not be active until start() has been called. - proximitySensor = AppRTCProximitySensor.create(context, - // This method will be called each time a state change is detected. - // Example: user holds his hand over the device (closer than ~5 cm), - // or removes his hand from the device. - this::onProximitySensorChangedState); Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice); AppRTCUtils.logDeviceInfo(Config.LOGTAG); } - public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { - this.speakerPhonePreference = speakerPhonePreference; - if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { - defaultAudioDevice = CallIntegration.AudioDevice.EARPIECE; - } else { - defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; - } - updateAudioDeviceState(); - } - public static boolean isMicrophoneAvailable() { microphoneLatch = new CountDownLatch(1); AudioRecord audioRecord = null; @@ -156,30 +131,6 @@ public class AppRTCAudioManager { } } - /** - * This method is called when the proximity sensor reports a state change, - * e.g. from "NEAR to FAR" or from "FAR to NEAR". - */ - private void onProximitySensorChangedState() { - if (speakerPhonePreference != SpeakerPhonePreference.AUTO) { - return; - } - // The proximity sensor should only be activated when there are exactly two - // available audio devices. - if (audioDevices.size() == 2 && audioDevices.contains(CallIntegration.AudioDevice.EARPIECE) - && audioDevices.contains(CallIntegration.AudioDevice.SPEAKER_PHONE)) { - if (proximitySensor.sensorReportsNearState()) { - // Sensor reports that a "handset is being held up to a person's ear", - // or "something is covering the light sensor". - setAudioDeviceInternal(CallIntegration.AudioDevice.EARPIECE); - } else { - // Sensor reports that a "handset is removed from a person's ear", or - // "the light sensor is no longer covered". - setAudioDeviceInternal(CallIntegration.AudioDevice.SPEAKER_PHONE); - } - } - } - @SuppressWarnings("deprecation") public void start(AudioManagerEvents audioManagerEvents) { Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()"); @@ -280,6 +231,7 @@ public class AppRTCAudioManager { @SuppressWarnings("deprecation") public void stop() { + Log.d(Config.LOGTAG,"appRtpAudioManager.stop()"); Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()"); ThreadUtils.checkIsOnMainThread(); if (amState != AudioManagerState.RUNNING) { @@ -296,12 +248,8 @@ public class AppRTCAudioManager { // Abandon audio focus. Gives the previous focus owner, if any, focus. audioManager.abandonAudioFocus(audioFocusChangeListener); audioFocusChangeListener = null; - Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams"); - if (proximitySensor != null) { - proximitySensor.stop(); - proximitySensor = null; - } audioManagerEvents = null; + Log.d(Config.LOGTAG,"appRtpAudioManager.stopped()"); } /** @@ -375,7 +323,6 @@ public class AppRTCAudioManager { * Returns the currently selected audio device. */ public CallIntegration.AudioDevice getSelectedAudioDevice() { - ThreadUtils.checkIsOnMainThread(); return selectedAudioDevice; } @@ -574,6 +521,10 @@ public class AppRTCAudioManager { Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); } + public void executeOnMain(final Runnable runnable) { + ContextCompat.getMainExecutor(apprtcContext).execute(runnable); + } + /** * AudioManager state. */ @@ -583,18 +534,6 @@ public class AppRTCAudioManager { RUNNING, } - public enum SpeakerPhonePreference { - AUTO, EARPIECE, SPEAKER; - - public static SpeakerPhonePreference of(final Set media) { - if (media.contains(Media.VIDEO)) { - return SPEAKER; - } else { - return EARPIECE; - } - } - } - /** * Selected audio device change event. */ diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java index 484072605..d915459a4 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java @@ -68,8 +68,6 @@ public class AppRTCBluetoothManager { }; protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { - Log.d(Config.LOGTAG, "ctor"); - ThreadUtils.checkIsOnMainThread(); apprtcContext = context; apprtcAudioManager = audioManager; this.audioManager = getAudioManager(context); diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java b/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java deleted file mode 100644 index 2f24787c0..000000000 --- a/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2014 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ -package eu.siacs.conversations.services; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.webrtc.ThreadUtils; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.AppRTCUtils; - -/** - * AppRTCProximitySensor manages functions related to the proximity sensor in - * the AppRTC demo. - * On most device, the proximity sensor is implemented as a boolean-sensor. - * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX - * value i.e. the LUX value of the light sensor is compared with a threshold. - * A LUX-value more than the threshold means the proximity sensor returns "FAR". - * Anything less than the threshold value and the sensor returns "NEAR". - */ -public class AppRTCProximitySensor implements SensorEventListener { - // This class should be created, started and stopped on one thread - // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is - // the case. Only active when |DEBUG| is set to true. - private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); - private final Runnable onSensorStateListener; - private final SensorManager sensorManager; - @Nullable - private Sensor proximitySensor; - private boolean lastStateReportIsNear; - - private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { - Log.d(Config.LOGTAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo()); - onSensorStateListener = sensorStateListener; - sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); - } - - /** - * Construction - */ - static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { - return new AppRTCProximitySensor(context, sensorStateListener); - } - - /** - * Activate the proximity sensor. Also do initialization if called for the - * first time. - */ - public boolean start() { - threadChecker.checkIsOnValidThread(); - Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo()); - if (!initDefaultSensor()) { - // Proximity sensor is not supported on this device. - return false; - } - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); - return true; - } - - /** - * Deactivate the proximity sensor. - */ - public void stop() { - threadChecker.checkIsOnValidThread(); - Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo()); - if (proximitySensor == null) { - return; - } - sensorManager.unregisterListener(this, proximitySensor); - } - - /** - * Getter for last reported state. Set to true if "near" is reported. - */ - public boolean sensorReportsNearState() { - threadChecker.checkIsOnValidThread(); - return lastStateReportIsNear; - } - - @Override - public final void onAccuracyChanged(Sensor sensor, int accuracy) { - threadChecker.checkIsOnValidThread(); - AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY); - if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { - Log.e(Config.LOGTAG, "The values returned by this sensor cannot be trusted"); - } - } - - @Override - public final void onSensorChanged(SensorEvent event) { - threadChecker.checkIsOnValidThread(); - AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY); - // As a best practice; do as little as possible within this method and - // avoid blocking. - float distanceInCentimeters = event.values[0]; - if (distanceInCentimeters < proximitySensor.getMaximumRange()) { - Log.d(Config.LOGTAG, "Proximity sensor => NEAR state"); - lastStateReportIsNear = true; - } else { - Log.d(Config.LOGTAG, "Proximity sensor => FAR state"); - lastStateReportIsNear = false; - } - // Report about new state to listening client. Client can then call - // sensorReportsNearState() to query the current state (NEAR or FAR). - if (onSensorStateListener != null) { - onSensorStateListener.run(); - } - Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " - + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" - + event.values[0]); - } - - /** - * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) - * does not support this type of sensor and false will be returned in such - * cases. - */ - private boolean initDefaultSensor() { - if (proximitySensor != null) { - return true; - } - proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - if (proximitySensor == null) { - return false; - } - logProximitySensorInfo(); - return true; - } - - /** - * Helper method for logging information about the proximity sensor. - */ - private void logProximitySensorInfo() { - if (proximitySensor == null) { - return; - } - StringBuilder info = new StringBuilder("Proximity sensor: "); - info.append("name=").append(proximitySensor.getName()); - info.append(", vendor: ").append(proximitySensor.getVendor()); - info.append(", power: ").append(proximitySensor.getPower()); - info.append(", resolution: ").append(proximitySensor.getResolution()); - info.append(", max range: ").append(proximitySensor.getMaximumRange()); - info.append(", min delay: ").append(proximitySensor.getMinDelay()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - // Added in API level 20. - info.append(", type: ").append(proximitySensor.getStringType()); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Added in API level 21. - info.append(", max delay: ").append(proximitySensor.getMaxDelay()); - info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); - info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); - } - Log.d(Config.LOGTAG, info.toString()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index a4dffffaa..051a7de9a 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -11,6 +11,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -42,8 +43,8 @@ public class CallIntegration extends Connection { this.appRTCAudioManager = null; } else { this.appRTCAudioManager = new AppRTCAudioManager(context); - this.appRTCAudioManager.start(this::onAudioDeviceChanged); - // TODO WebRTCWrapper would issue one call to eventCallback.onAudioDeviceChanged + ContextCompat.getMainExecutor(context) + .execute(() -> this.appRTCAudioManager.start(this::onAudioDeviceChanged)); } setRingbackRequested(true); } @@ -149,6 +150,12 @@ public class CallIntegration extends Connection { final var available = getAudioDevices(); if (available.contains(audioDevice)) { this.setAudioDevice(audioDevice); + } else { + Log.d( + Config.LOGTAG, + "application requested to switch to " + + audioDevice + + " but device was not available"); } } @@ -269,7 +276,8 @@ public class CallIntegration extends Connection { } private void setAudioDeviceFallback(final AudioDevice audioDevice) { - requireAppRtcAudioManager().setDefaultAudioDevice(audioDevice); + final var audioManager = requireAppRtcAudioManager(); + audioManager.executeOnMain(() -> audioManager.setDefaultAudioDevice(audioDevice)); } @NonNull @@ -287,7 +295,7 @@ public class CallIntegration extends Connection { if (state == STATE_DISCONNECTED) { final var audioManager = this.appRTCAudioManager; if (audioManager != null) { - audioManager.stop(); + audioManager.executeOnMain(audioManager::stop); } } } @@ -382,8 +390,11 @@ public class CallIntegration extends Connection { return; } final var audioManager = requireAppRtcAudioManager(); - this.onAudioDeviceChanged( - audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices()); + audioManager.executeOnMain( + () -> + this.onAudioDeviceChanged( + audioManager.getSelectedAudioDevice(), + audioManager.getAudioDevices())); } /** AudioDevice is the names of possible audio devices that we currently support. */ diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 01f246844..e1a9b3383 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -122,6 +122,7 @@ public class CallIntegrationConnectionService extends ConnectionService { public Connection onCreateIncomingConnection( final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { + Log.d(Config.LOGTAG, "onCreateIncomingConnection()"); final var service = ServiceConnectionService.get(this.serviceFuture); final Bundle extras = request.getExtras(); final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); @@ -182,7 +183,8 @@ public class CallIntegrationConnectionService extends ConnectionService { } public static void unregisterPhoneAccount(final Context context, final Account account) { - context.getSystemService(TelecomManager.class).unregisterPhoneAccount(getHandle(context, account)); + context.getSystemService(TelecomManager.class) + .unregisterPhoneAccount(getHandle(context, account)); } public static PhoneAccountHandle getHandle(final Context context, final Account account) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e9d750319..2cbd8c739 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2300,8 +2300,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final boolean trickle) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); - this.webRTCWrapper.setup( - this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); + this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle); } 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 fa504ed19..128a35bf0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -222,9 +222,7 @@ public class WebRTCWrapper { } } - public void setup( - final XmppConnectionService service, - @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) + public void setup(final XmppConnectionService service) throws InitializationException { try { PeerConnectionFactory.initialize( From 0ffc2958886691a8bafc45824e7c8c49d33ac55a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Jan 2024 09:18:24 +0100 Subject: [PATCH 142/556] provide alternative method to create calls for Android <8 --- .../services/CallIntegration.java | 4 + .../CallIntegrationConnectionService.java | 113 ++++++++++++------ .../ui/ConversationFragment.java | 2 +- .../conversations/ui/RtpSessionActivity.java | 2 +- 4 files changed, 84 insertions(+), 37 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 051a7de9a..b9b21e578 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -381,6 +381,10 @@ public class CallIntegration extends Connection { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } + public static boolean notSelfManaged() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.O; + } + public void setInitialAudioDevice(final AudioDevice audioDevice) { Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")"); this.initialAudioDevice = audioDevice; diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index e1a9b3383..71ea897c2 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -18,6 +18,8 @@ import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.util.Log; +import androidx.annotation.NonNull; + import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -44,6 +46,7 @@ public class CallIntegrationConnectionService extends ConnectionService { @Override public void onCreate() { + Log.d(Config.LOGTAG, "CallIntegrationService.onCreate()"); super.onCreate(); this.serviceFuture = ServiceConnectionService.bindService(this); } @@ -62,38 +65,39 @@ public class CallIntegrationConnectionService extends ConnectionService { this.unbindService(serviceConnection); } - @Override - public Connection onCreateOutgoingConnection( - final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { - Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")"); - final var uri = request.getAddress(); - final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); - final var extras = request.getExtras(); - final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE); - final Set media = - videoState == VideoProfile.STATE_AUDIO_ONLY - ? ImmutableSet.of(Media.AUDIO) - : ImmutableSet.of(Media.AUDIO, Media.VIDEO); - Log.d(Config.LOGTAG, "jid=" + jid); - Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId()); - Log.d(Config.LOGTAG, "media " + media); - final var service = ServiceConnectionService.get(this.serviceFuture); + private static Connection createOutgoingRtpConnection( + final XmppConnectionService service, + final String phoneAccountHandle, + final Jid with, + final Set media) { if (service == null) { + Log.d( + Config.LOGTAG, + "CallIntegrationConnection service was unable to bind to XmppConnectionService"); return Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR, "service connection not found")); } - final Account account = service.findAccountByUuid(phoneAccountHandle.getId()); - final Intent intent = new Intent(this, RtpSessionActivity.class); + final var account = service.findAccountByUuid(phoneAccountHandle); + return createOutgoingRtpConnection(service, account, with, media); + } + + private static Connection createOutgoingRtpConnection( + @NonNull final XmppConnectionService service, + @NonNull final Account account, + final Jid with, + final Set media) { + Log.d(Config.LOGTAG, "create outgoing rtp connection!"); + final Intent intent = new Intent(service, RtpSessionActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, jid.toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); final CallIntegration callIntegration; - if (jid.isBareJid()) { + if (with.isBareJid()) { final var proposal = service.getJingleConnectionManager() - .proposeJingleRtpSession(account, jid, media); + .proposeJingleRtpSession(account, with, media); intent.putExtra( RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, @@ -110,16 +114,35 @@ public class CallIntegrationConnectionService extends ConnectionService { callIntegration = proposal.getCallIntegration(); } else { final JingleRtpConnection jingleRtpConnection = - service.getJingleConnectionManager().initializeRtpSession(account, jid, media); + service.getJingleConnectionManager().initializeRtpSession(account, with, media); final String sessionId = jingleRtpConnection.getId().sessionId; intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); callIntegration = jingleRtpConnection.getCallIntegration(); } - Log.d(Config.LOGTAG, "start activity!"); - startActivity(intent); + service.startActivity(intent); return callIntegration; } + @Override + public Connection onCreateOutgoingConnection( + final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { + Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")"); + final var uri = request.getAddress(); + final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); + final var extras = request.getExtras(); + final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE); + final Set media = + videoState == VideoProfile.STATE_AUDIO_ONLY + ? ImmutableSet.of(Media.AUDIO) + : ImmutableSet.of(Media.AUDIO, Media.VIDEO); + Log.d(Config.LOGTAG, "jid=" + jid); + Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId()); + Log.d(Config.LOGTAG, "media " + media); + final var service = ServiceConnectionService.get(this.serviceFuture); + return createOutgoingRtpConnection(service, phoneAccountHandle.getId(), jid, media); + } + + @Override public Connection onCreateIncomingConnection( final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { Log.d(Config.LOGTAG, "onCreateIncomingConnection()"); @@ -194,22 +217,42 @@ public class CallIntegrationConnectionService extends ConnectionService { } public static void placeCall( - final Context context, final Account account, final Jid with, final Set media) { + final XmppConnectionService service, + final Account account, + final Jid with, + final Set media) { Log.d(Config.LOGTAG, "place call media=" + media); - final var extras = new Bundle(); - extras.putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account)); - extras.putInt( - TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, - Media.audioOnly(media) - ? VideoProfile.STATE_AUDIO_ONLY - : VideoProfile.STATE_BIDIRECTIONAL); - context.getSystemService(TelecomManager.class) - .placeCall(CallIntegration.address(with), extras); + if (CallIntegration.selfManaged()) { + final var extras = new Bundle(); + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(service, account)); + extras.putInt( + TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, + Media.audioOnly(media) + ? VideoProfile.STATE_AUDIO_ONLY + : VideoProfile.STATE_BIDIRECTIONAL); + service.getSystemService(TelecomManager.class) + .placeCall(CallIntegration.address(with), extras); + } else { + final var connection = createOutgoingRtpConnection(service, account, with, media); + if (connection != null) { + Log.d( + Config.LOGTAG, + "not adding outgoing call to TelecomManager on Android " + + Build.VERSION.RELEASE); + } + } } public static void addNewIncomingCall( final Context context, final AbstractJingleConnection.Id id) { + if (CallIntegration.notSelfManaged()) { + Log.d( + Config.LOGTAG, + "not adding incoming call to TelecomManager on Android " + + Build.VERSION.RELEASE); + return; + } final var phoneAccountHandle = CallIntegrationConnectionService.getHandle(context, id.account); final var bundle = new Bundle(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 01932a04e..a00a47209 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1653,7 +1653,7 @@ public class ConversationFragment extends XmppFragment } private void triggerRtpSession(final Account account, final Jid with, final String action) { - CallIntegrationConnectionService.placeCall(requireActivity(),account,with,RtpSessionActivity.actionToMedia(action)); + CallIntegrationConnectionService.placeCall(activity.xmppConnectionService, account,with,RtpSessionActivity.actionToMedia(action)); } private void handleAttachmentSelection(MenuItem item) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 598232467..2e4576f57 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1312,7 +1312,7 @@ public class RtpSessionActivity extends XmppActivity final Set media = actionToMedia(lastAction == null ? action : lastAction); this.rtpConnectionReference = null; Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString()); - CallIntegrationConnectionService.placeCall(this, account, with, media); + CallIntegrationConnectionService.placeCall(xmppConnectionService, account, with, media); } private void exit(final View view) { From 66cd50e1638905c76e9d508de9e6282eabca1ade Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Jan 2024 09:32:49 +0100 Subject: [PATCH 143/556] add permission check to placeCall method --- .../CallIntegrationConnectionService.java | 11 ++++++- .../ui/ConversationFragment.java | 30 ++++++++----------- src/main/res/values/strings.xml | 1 + 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 71ea897c2..99f565ff1 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -1,9 +1,11 @@ package eu.siacs.conversations.services; +import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -17,6 +19,7 @@ import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telecom.VideoProfile; import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; @@ -25,6 +28,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xmpp.Jid; @@ -221,7 +225,6 @@ public class CallIntegrationConnectionService extends ConnectionService { final Account account, final Jid with, final Set media) { - Log.d(Config.LOGTAG, "place call media=" + media); if (CallIntegration.selfManaged()) { final var extras = new Bundle(); extras.putParcelable( @@ -231,6 +234,12 @@ public class CallIntegrationConnectionService extends ConnectionService { Media.audioOnly(media) ? VideoProfile.STATE_AUDIO_ONLY : VideoProfile.STATE_BIDIRECTIONAL); + if (service.checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(service, R.string.no_permission_to_place_call, Toast.LENGTH_SHORT) + .show(); + return; + } service.getSystemService(TelecomManager.class) .placeCall(CallIntegration.address(with), extras); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index a00a47209..6eea73d39 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1981,26 +1981,22 @@ public class ConversationFragment extends XmppFragment } private boolean hasPermissions(int requestCode, List permissions) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final List missingPermissions = new ArrayList<>(); - for (String permission : permissions) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - continue; - } - if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { - missingPermissions.add(permission); - } + final List missingPermissions = new ArrayList<>(); + for (String permission : permissions) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + continue; } - if (missingPermissions.size() == 0) { - return true; - } else { - requestPermissions( - missingPermissions.toArray(new String[0]), - requestCode); - return false; + if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { + missingPermissions.add(permission); } - } else { + } + if (missingPermissions.size() == 0) { return true; + } else { + requestPermissions( + missingPermissions.toArray(new String[0]), + requestCode); + return false; } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3ee875fcf..996977eab 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -595,6 +595,7 @@ Me Contact asks for presence subscription Allow + No permission to place phone call No permission to access %s Remote server not found Remote server timeout From d31b24d05a2dd24373867819a9133e6e967a700b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Jan 2024 12:18:47 +0100 Subject: [PATCH 144/556] get rid of ToneManager and play sounds in CallIntegration instead CallIntegration takes care of audio routing so it makes sense to play and sounds here too --- .../java/eu/siacs/conversations/Config.java | 2 +- .../services/CallIntegration.java | 35 ++- .../ui/ConversationFragment.java | 2 +- .../xmpp/jingle/JingleConnectionManager.java | 7 +- .../xmpp/jingle/JingleRtpConnection.java | 1 - .../xmpp/jingle/ToneManager.java | 238 ------------------ .../xmpp/jingle/WebRTCWrapper.java | 10 +- 7 files changed, 35 insertions(+), 260 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 6c9e5dd68..febe12b14 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -120,7 +120,7 @@ public final class Config { public static final boolean DISABLE_PROXY_LOOKUP = false; // disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback) public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; - public static final boolean USE_JINGLE_DIRECT_INIT = true; + public static final boolean USE_JINGLE_MESSAGE_INIT = true; public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index b9b21e578..548a1507d 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.services; import android.content.Context; +import android.media.AudioManager; +import android.media.ToneGenerator; import android.net.Uri; import android.os.Build; import android.telecom.CallAudioState; @@ -20,11 +22,13 @@ import com.google.common.collect.Lists; import eu.siacs.conversations.Config; import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.Media; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class CallIntegration extends Connection { @@ -32,6 +36,7 @@ public class CallIntegration extends Connection { private final AppRTCAudioManager appRTCAudioManager; private AudioDevice initialAudioDevice = null; private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false); + private final AtomicBoolean delayedDestructionInitiated = new AtomicBoolean(false); private List availableEndpoints = Collections.emptyList(); @@ -302,7 +307,9 @@ public class CallIntegration extends Connection { public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - this.destroyWith(new DisconnectCause(DisconnectCause.LOCAL, null)); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 100); + toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); } public void accepted() { @@ -316,6 +323,9 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80); + toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null)); } @@ -332,16 +342,33 @@ public class CallIntegration extends Connection { public void busy() { Log.d(Config.LOGTAG, "CallIntegration.busy()"); - this.destroyWith(new DisconnectCause(DisconnectCause.BUSY, null)); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80); + toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); + this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500); + } + + private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) { + if (this.delayedDestructionInitiated.compareAndSet(false, true)) { + JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule( + () -> { + this.setDisconnected(disconnectCause); + this.destroy(); + }, + delay, + TimeUnit.MILLISECONDS); + } else { + Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!"); + } } private void destroyWith(final DisconnectCause disconnectCause) { - if (this.getState() == STATE_DISCONNECTED) { + if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) { Log.d(Config.LOGTAG, "CallIntegration has already been destroyed"); return; } this.setDisconnected(disconnectCause); this.destroy(); + Log.d(Config.LOGTAG, "destroyed!"); } public static Uri address(final Jid contact) { @@ -349,7 +376,7 @@ public class CallIntegration extends Connection { } public void verifyDisconnected() { - if (this.getState() == STATE_DISCONNECTED) { + if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) { return; } throw new AssertionError("CallIntegration has not been disconnected"); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 6eea73d39..da281617e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1633,7 +1633,7 @@ public class ConversationFragment extends XmppFragment activity.xmppConnectionService.updateAccount(account); } final Contact contact = conversation.getContact(); - if (Config.USE_JINGLE_DIRECT_INIT && RtpCapability.jmiSupport(contact)) { + if (Config.USE_JINGLE_MESSAGE_INIT && RtpCapability.jmiSupport(contact)) { triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action); } else { final RtpCapability.Capability capability; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index fd5c4c9af..e90a35a0c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -54,9 +54,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class JingleConnectionManager extends AbstractConnectionManager { - static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = + public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); - final ToneManager toneManager; private final HashMap rtpSessionProposals = new HashMap<>(); private final ConcurrentHashMap @@ -67,7 +66,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { public JingleConnectionManager(XmppConnectionService service) { super(service); - this.toneManager = new ToneManager(service); } static String nextRandomId() { @@ -490,7 +488,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { proposal.callIntegration.busy(); writeLogMissedOutgoing( account, proposal.with, proposal.sessionId, serverMsgId, timestamp); - toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, @@ -667,7 +664,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void retractSessionProposal(final RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; - toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -713,7 +709,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { final RtpEndUserState endUserState = preexistingState.toEndUserState(); - toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, with, proposal.sessionId, endUserState); return proposal; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 2cbd8c739..0368f1a51 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2727,7 +2727,6 @@ public class JingleRtpConnection extends AbstractJingleConnection private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); - jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); this.updateCallIntegrationState(); xmppConnectionService.notifyJingleRtpConnectionUpdate( id.account, id.with, id.sessionId, endUserState); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java deleted file mode 100644 index fb82b7219..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ /dev/null @@ -1,238 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import android.content.Context; -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.os.Build; -import android.util.Log; - -import java.util.Arrays; -import java.util.Set; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import eu.siacs.conversations.Config; - -import static java.util.Arrays.asList; - -import androidx.core.content.ContextCompat; - -class ToneManager { - - private ToneGenerator toneGenerator; - private final Context context; - - private ToneState state = null; - private RtpEndUserState endUserState = null; - private ScheduledFuture currentTone; - private ScheduledFuture currentResetFuture; - private boolean appRtcAudioManagerHasControl = false; - - ToneManager(final Context context) { - this.context = context; - } - - private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set media) { - if (isInitiator) { - if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) { - return ToneState.RINGING; - } - if (state == RtpEndUserState.DECLINED_OR_BUSY) { - return ToneState.BUSY; - } - } - if (state == RtpEndUserState.ENDING_CALL) { - if (media.contains(Media.VIDEO)) { - return ToneState.NULL; - } else { - return ToneState.ENDING_CALL; - } - } - if (Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING, - RtpEndUserState.INCOMING_CONTENT_ADD) - .contains(state)) { - if (media.contains(Media.VIDEO)) { - return ToneState.NULL; - } else { - return ToneState.CONNECTED; - } - } - return ToneState.NULL; - } - - void transition(final RtpEndUserState state, final Set media) { - transition(state, of(true, state, media), media); - } - - void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { - transition(state, of(isInitiator, state, media), media); - } - - private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set media) { - final RtpEndUserState normalizeEndUserState = normalize(endUserState); - if (this.endUserState == normalizeEndUserState) { - return; - } - this.endUserState = normalizeEndUserState; - if (this.state == state) { - return; - } - if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) { - return; - } - cancelCurrentTone(); - Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")"); - if (state != ToneState.NULL) { - configureAudioManagerForCall(media); - } - switch (state) { - case RINGING: - // ringing can be removed as this is now handled by 'CallIntegration' - //scheduleWaitingTone(); - break; - case CONNECTED: - scheduleConnected(); - break; - case BUSY: - scheduleBusy(); - break; - case ENDING_CALL: - scheduleEnding(); - break; - case NULL: - if (noResetScheduled()) { - resetAudioManager(); - } - break; - default: - throw new IllegalStateException("Unable to handle transition to "+state); - } - this.state = state; - } - - private static RtpEndUserState normalize(final RtpEndUserState endUserState) { - if (Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING, - RtpEndUserState.INCOMING_CONTENT_ADD) - .contains(endUserState)) { - return RtpEndUserState.CONNECTED; - } else { - return endUserState; - } - } - - void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) { - this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl; - } - - private void scheduleConnected() { - this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { - startTone(ToneGenerator.TONE_PROP_PROMPT, 200); - }, 0, TimeUnit.SECONDS); - } - - private void scheduleEnding() { - this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { - startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); - }, 0, TimeUnit.SECONDS); - this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS); - } - - private void scheduleBusy() { - this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { - startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); - }, 0, TimeUnit.SECONDS); - this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS); - } - - private void scheduleWaitingTone() { - this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { - startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); - }, 0, 3, TimeUnit.SECONDS); - } - - private boolean noResetScheduled() { - return this.currentResetFuture == null || this.currentResetFuture.isDone(); - } - - private void cancelCurrentTone() { - if (currentTone != null) { - currentTone.cancel(true); - } - stopTone(toneGenerator); - } - - private static void stopTone(final ToneGenerator toneGenerator) { - if (toneGenerator == null) { - return; - } - try { - toneGenerator.stopTone(); - } catch (final RuntimeException e) { - Log.w(Config.LOGTAG,"tone has already stopped"); - } - } - - private void startTone(final int toneType, final int durationMs) { - if (this.toneGenerator != null) { - this.toneGenerator.release();; - - } - final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class); - final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL; - this.toneGenerator = getToneGenerator(ringerModeNormal); - if (toneGenerator != null) { - this.toneGenerator.startTone(toneType, durationMs); - } - } - - private static ToneGenerator getToneGenerator(final boolean ringerModeNormal) { - try { - // when silent and on Android 12+ use STREAM_MUSIC - if (ringerModeNormal || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return new ToneGenerator(AudioManager.STREAM_VOICE_CALL,60); - } else { - return new ToneGenerator(AudioManager.STREAM_MUSIC,100); - } - } catch (final Exception e) { - Log.d(Config.LOGTAG,"could not create tone generator",e); - return null; - } - } - - private void configureAudioManagerForCall(final Set media) { - if (appRtcAudioManagerHasControl) { - Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control"); - return; - } - final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - if (audioManager == null) { - return; - } - final boolean isSpeakerPhone = media.contains(Media.VIDEO); - Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone); - audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); - audioManager.setSpeakerphoneOn(isSpeakerPhone); - } - - private void resetAudioManager() { - if (appRtcAudioManagerHasControl) { - Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control"); - return; - } - final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - if (audioManager == null) { - return; - } - Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode"); - audioManager.setMode(AudioManager.MODE_NORMAL); - audioManager.setSpeakerphoneOn(false); - } - - private enum ToneState { - NULL, RINGING, CONNECTED, BUSY, ENDING_CALL - } -} 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 128a35bf0..8bc7c6f6f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import com.google.common.base.Optional; @@ -15,8 +13,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; -import eu.siacs.conversations.services.AppRTCAudioManager; -import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.services.XmppConnectionService; import org.webrtc.AudioSource; @@ -52,7 +48,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; -@SuppressWarnings("UnstableApiUsage") public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); @@ -205,7 +200,6 @@ public class WebRTCWrapper { }; @Nullable private PeerConnectionFactory peerConnectionFactory = null; @Nullable private PeerConnection peerConnection = null; - private ToneManager toneManager = null; private Context context = null; private EglBase eglBase = null; private VideoSourceWrapper videoSourceWrapper; @@ -222,8 +216,7 @@ public class WebRTCWrapper { } } - public void setup(final XmppConnectionService service) - throws InitializationException { + public void setup(final XmppConnectionService service) throws InitializationException { try { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(service) @@ -238,7 +231,6 @@ public class WebRTCWrapper { throw new InitializationException("Unable to create EGL base", e); } this.context = service; - this.toneManager = service.getJingleConnectionManager().toneManager; } synchronized void initializePeerConnection( From 7f9d836f1a2df2af4346675d32028a0760111ee2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 17 Jan 2024 13:53:12 +0100 Subject: [PATCH 145/556] play connected sound from sound file --- .../services/CallIntegration.java | 20 +++++++++++++++--- src/main/res/raw/connected.ogg | Bin 0 -> 10581 bytes 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/main/res/raw/connected.ogg diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 548a1507d..489d71256 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -20,6 +20,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; @@ -33,6 +34,10 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CallIntegration extends Connection { + private static final int DEFAULT_VOLUME = 80; + + private final Context context; + private final AppRTCAudioManager appRTCAudioManager; private AudioDevice initialAudioDevice = null; private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false); @@ -43,6 +48,7 @@ public class CallIntegration extends Connection { private Callback callback = null; public CallIntegration(final Context context) { + this.context = context.getApplicationContext(); if (selfManaged()) { setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); this.appRTCAudioManager = null; @@ -297,7 +303,9 @@ public class CallIntegration extends Connection { @Override public void onStateChanged(final int state) { Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); - if (state == STATE_DISCONNECTED) { + if (state == STATE_ACTIVE) { + playConnectedSound(); + } else if (state == STATE_DISCONNECTED) { final var audioManager = this.appRTCAudioManager; if (audioManager != null) { audioManager.executeOnMain(audioManager::stop); @@ -305,9 +313,15 @@ public class CallIntegration extends Connection { } } + private void playConnectedSound() { + final var mediaPlayer = MediaPlayer.create(context, R.raw.connected); + mediaPlayer.setVolume(DEFAULT_VOLUME / 100f, DEFAULT_VOLUME / 100f); + mediaPlayer.start(); + } + public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 100); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, DEFAULT_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); } @@ -323,7 +337,7 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, DEFAULT_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null)); diff --git a/src/main/res/raw/connected.ogg b/src/main/res/raw/connected.ogg new file mode 100644 index 0000000000000000000000000000000000000000..29e9fabba379b3d86fcd7205318e8c6039ef60b2 GIT binary patch literal 10581 zcmb_>c|4SF_y0|{Y)O)2%@WF*eF=l?VeDgzvBX$Hwyb4I42dyzg&9joXtHG~8EbZf ztRcIkEM@)Pqv!KH&-eGo@AdoRcfanLIoEa0b*^*X=UnHy?t2VeT#Nxy;9pmxP^<@$ z9H(Y3AmJeK_wsRY^(A(YwAc`T0N{xo$?0zii2-rszc1oQBB!UYiT9lP$$ww7Xa4Gu z1lbm@_wR`q__%PnyE>SiYUk4Ck`R*+6O$2>*!&t1T)7AudJYG;9K);Y#O zIv@2XnF7QC7&dc5A8p~KY~KPF^B(VIE|B`h<=>3sx=a*voa z2NIoFf?LnyU#>< zI?fRQsO}2H704zoSR`XDb19=@%1D6-03ZY9Xf-RD^tX8Xek%6;`akc#({cd-@L0f+ z<;*`JpyFn|td;|1w5Dc;u$@<9hxDqIU?XP*qxs;2#$~Eh+##Dvd(*+2e?!0k0QO9> zv5(q$skoyYn69aN7jl8rH(rabpk-dFsYnJ$ZFX>X9XR3?3ed+Et|$fwCm z!NPQhIKg%{-yZRHu1H#m<(DaUiQQ=M@&6NcRlQOJ!VFnW_Fz>Mx1QWPE$A!8-k6^sNes-PhF2710oU^K-9roD-okb zeHxr35`Y1SD3Fbt*8<~_$Y+_%?}~N_LKg*P7C+@U6#)YSq6S_O`KMvywL}-X<`xly z{pwX*VacmkZkdIy|M#0TkGm8A&H?~?yEmJrH(R?m(W_I0xy}Gw0H9{Xrr~p=Jy}v0 z^l?bNZ`;~P+RxbBU1QD$GHh&jj04nu&91dGK70z1p4GDNr5Pa*(me zmP!xNVUI%T=m?`xu%7IcK~+s`WD-=H?_Dw!$(K5&qa%(+Ik*eszc`dhfg}Dhgx|4R zA*D2ts-P-xax8I<5*%n1s7juaFBRo*gAbPuGC)<7+}LEOJ0EroB3S;HG3L;N!;QHK zl(Z4&z_M5&aqq_5)-i9Hp-(|`9pWnq1Xy_WMGuaiUw0Ki8U zEK6iR7X}4qV2wgK&_l3H@Luf_lmiGu1{_QtK6tMj6<;)ijy4|_j4mJ61g!6m&HySA zbeW9tXP8*47y+uY02c!^PJD?Al?>H657CpO;EsYq-GsqVFS11$>gbf4>WPCCsHFs_ zBaY7o%a5QS4h@!LVxJ(EAhGzj4?7H_1WS+(I1vH1MyyDJSj;dCaoUH)pkVRhfBTZ5 z2yrx&I2Hv(2;-qTf<#KUye%Ch!LeW;i!n%nV=LQO;2;D9a4en$q5lsBF9(Y|n=Dw6 zSyZoGHf6RiID3gB;xuKAK%~TD#Xqt5B&v42RPPl(n-SF^r-@BT);^n;N{|rDOHGC? zfO%%rRg6MITr)6(uzPs`@a7C9_@T+`Pf7(;H~`>L5)=vg2ttF;_N5oZvVdR~vA}!4 zy~r3Tz6fqeL zS6YV{s0PFu1+FP75GsK-mQzT5=}((5Jj(+{BI@)I0V0q{_Ndb)ivlPNA}8)oj0u+D zaN`taaRQ7eOaPY&h#Z)+FoaGCjs;$ZBai|u7ubLZYIh6vub6u`-kkbK0kkjxpjp5f zdT^uUM?E^NJ|#r>h>k}=S+S>aEs@lb1LF#Xc4em+f}sLK53V=R?D9x3I|OmeaLPOs z3d|ThJw%AuOkh7vAUum2D9j8;;$n#T1cp!el#5%05u2=t&tOtVQ4mqTVx}o8$VziV z(vc5Hq6Ad9K7o6Y+!K@o@gSfECS5uaZI-`igT{fUhQB8SBIDoZg2aD^;XeWC1?#!c zeXtU~F>>^M{QTvqk6-`#*bc%AbxVH?qRofthZLj}Lj^)9v5di(fO+3WbQA-Hf)rSz zi0Oy~AwNw~2PBT@yl|TZy!_PLavTeSju-}aaY81zw!th$RuVFG!Lgu7-J&(<@;eJe zxfEC?z|dqOdK-%f4nz^(?~MSELP-e}lU;GK(3Zc>MkjKL!xbQc>l5f7arY%|?x&0N zltMvuFyK=EhYRA5we1TX)Kp(B8eVw68+oIPb=AAr^1?^*=qR!&b0e;KGj zqAD=9r%gWM9RFfKb^bE~GLBjwvGp2Em072h&@y15*v2zv$o$ z5Xzh=Cbwd~!WeNX?gzxQdS~HFk%s{*q%hth^0N|{YG5S=h2<{IgH;_n>`;nZVncpE zE!o6;``&o3UmINeFyHS=ly-_>c>vQ-iINj3mXAU^lvNVgLeAf2G1Ac$#%Bh9Ze)Q+ z=?LOs!M$}$*2wd>u*~q+b$c8kA|HRxfqTc^T;#c1RQAXu@x?LUAB~sb$a5b%tbIj3 zvdqVeef-VkUS3COhXnP(LpG&;zYG@tNX3H$zovW#G=76C_BM-M6bVpwo*sy$xy=Z5 zxopPjJ%5(vY7)5iNC0uLW`k!DOcDq-Slj>Cu`~O$k^(>r8F8n(K9*_;!x>EuYuj$C z&R&prxGAv!6>|2uy8N8gt&(xQpW5>W>1ingE(5><28DtNvqHL5U8Q0>xW4hW@m_@| zJsx~qlq4Rj-Ng>4<|Z%k?-!3|Dqw|DXF#dlUdhEIiWiGt`wBgar2sRB0pQ}|1zG^r z543=otO|=9!!i}u{d;EsTJ;XSyM|PB^b#bqBv&CqfR>&iO5}km)p<-jS3IRicJ0yLA!JZkXBg4X#nyy}j;rvfU#EO?ycCb^oQkx}}l zwxy$gYE8#)2*J}XfcOAO^jV->0n5Utg3SH;11bPtBB0Z_bn+g5*&c_zCtRWMyTU+cq}VjoaEy!|a-V zb_MtYDuqjvJ4aKEZjidh%-E|pf3EIsRa))l^==+XyfuF)e>E(1)ctCKxZhfU&e_se zrtS|G51f=s+uQMR2VTchWwz`JY!}8WpHH-RN4>qFnR9=py*3moPbXaU*+DUb4os1p zVCv4wO+T&m&rc+He;>Ax@<}&b2+VsU zz-IM$avn;ONZv?6&7Kj;L4D#dgmsvPwv(1@vVc5DQq|hU`_T z9~$`XU-TZnzgqXNRcYrP=vB*n72kHimX?*&@UAu-4eV9r=B$(pRA?LscXopBB{s*^ z$$)J%q4ERz&f4KOjuMiB(_Ux%nV?L$ekQ z-IsG$Hs%wC$3$daam+XRfN`x#t9$dSY4T-fM&UyKNd1v@bP4JB$mIiZKx~$EnaeG0 zcGKx;x<&~!2>NS#Y*#a^!}c0;DpLr-AwIzVu|lPCS;xhyC;^6U!CC8io{#`M&j<0$ z2i&~cQw2;SZi+`y>I`*jHlHe8@Uy%uQF=O_(&Lx!CMk&RsbOoaJMdZK;WGVSAIs|a z3`bqI(3=lOjCiWU&}AR`VXD`?%XgV(|rI2M6a@+<6mxOQk)8YUreU`||lOtN5?+qlp}9uF~^AA~W_1LK@Rq%@^Q-SFgJ0 z!1;kK=L%Zbm+Q0Bx-{tB-j{E^>kS4F6_qkYJ1Ey%=UOFLqh0V;A`}JsKhCiB(hFoPE(`zsen{zh zTCt5}hS4}Ome~7+@)DU>Bj5Dpi|Nf)7=kybg zI@fp{yCP+fSNoXegh*44h{qj~7Ca@9W zHGRus#t3G7&|Y0Mu{)-)AdT!sG&>v*TJ9c>{PI2gaZxJd$r11P@IE7V#t56`;ft-; z1-tWM%OZvqD4ASCJN;UG^-|WYua6-iE9HY3HJzM7pfNywQC+RSW`J}#?}3|VXS^ps z(PBu?HBf(rRIAJ|;q`JsEszl0GKQsj=`v+@vC)IDJ%YJaGd=xLV`e3N+RD4(70+f> z6gy{_pY&UGoWZh~RFuKVkz-h0tUSJn7bI-DubP^__bV7{E~S3mcAosmB*mm+in zr8zej)?}CGe)7!3(hE8ze;{1R^CGTazA(lI(#t>ZsJpqt(aM+hI3hyb`r+BANP*y) zG{YzUg32^d3!^51T2#MCM0O0xX*wWoPIsOuqv~tf&$%O1>;@7ItdAsc5l;I)| zvRQber5gxnn8jD^N1V-2BDlxQprv@ES>Ty0-I=331DQPMLMtmFlP8Gtn&e5JjsQR~ z>bjv`{i2dWYjc-{QsCms`{sA$j*V4>g`CCS>hBhvCTdh#MS_X!FM@Qd=A-62hh`}j zF}u=aUHLM%%Jl%T(4LqyEfUh}ZTu(^vOS7)L;7=1Qm)TS0oE@feikpjR|_4=`pT5v z!+&k2cnJ1P*;blk?k!0l-q{*7ufX;upsiqwul1G$tS#OqJ{S*Ka;oEqUauk_=d4;B zmscW(OjDDA?~++xE*yIUKx>4*$%FN|ILUXYSD2Z)7@W&M<-$Xg+G!6v&zNosEvL+H zd|D-{tO>KnDrbLcflsdyQ#|oTWQfCWrKy)GyaFYi63}l%KYIm`REnykNZJhs|9)rj zl;;WuI(({s@}7k~tD`w^?aduI(-TJbQW3Z+zk|{j65GYVSpvS|X~B?ux9Icq`UV?m zo*Skz2F>IbY%h^$PjVUn%O5hMlD*~ufFfdD>1$lsn%gJN)~(volg7!S$sF$q1E&pZ zZ)2U7F`ekqI|5bAg}uQSah{euI>w(E8qL9@9Z!SxNu1@wU$~W4j_Yhbx6w<^oZ+^& zSl1RUg4gV}sPiS&xrFtQFpmYrW6k@mCWS!l^H88EHSh=2;gf9(q3ILTYnLVL`mhc4 zQe6$|HDU7=$y2mbXV*TzC@-p^Gb`T4 zL_dc#V3-JdgV^1Xine^LD(=4hVe-nIOw1o@K*ZzSmSbJrVf*pErs+}RZ=c4)8@l*u z3YpQ=Df#j3z{-`G_*5M{vD>GvnBIxjWt z?ziW5oFeuUgGRDx`I`^RrG`~fR9uD0Rm!SAn>MiYS#Er4-D@?+E;VWHg}UYlOchnZ z?ZTKCA=CpR^mkZoR7Zu(XpJz}Is8hMc#Vz{t+U;PHfOwVmIAZ*qwhjS?Yi3NmQkgi zYI}|aD;dg2XU#Z;oF6~0-up6X7{N2;&Zp8x`(dlNHPEdwDpd%*E)o`+@(!NqpZ;8i z37Af;U3qnvI`hsxw7LHvMOpR@g%7<8_luN1#+MtU+jq5Bs3--LG#pC=w}Nado5qLo zZ{=n(U8*lpG%(qCY=&85h*T}p$Sh8@^1n&ouqm;NuhqTq_<>n&dF7lZa_^nK)6C5f z>=d(|m1`g6kY{!4eX)v>n7Avdl44EnD2w7hFF9wF^M{(MS4XeCxomYT{HMG|h?iSB z2};^+Lx9oT-y{bZtWphu-`;;7%@{BP)6unm2rY7%+KW|Br-B?3x*v6H@31Ng|Fk4E zIfw30F%luCZPh&gVufch_dr*Am_pd8aSNMk9)584tF?a*t3?z4l^WO}PXIG>o4W(1 zt<2|euwrSqsEy02o6c`-^36x(nVjtkA*i&)*c{TWKp7|9wLNBlK!#09DLEZ~-jk>& zNZcun%pJV3N3KF|g&V^5Q(rKT;?2=f9X-tQexk-vrb61X9aEtI0E+ZLGx(GmCRO`K z-7k$Sso_S&ttPt|BXnD`RKfUFgKlP%hjmP`M%gUL=36W_LsmZ?{-!KRsj)w)({#kD zJseqbmcp;Rr;gN1>ngGp-A!@7nIiwrb45obRYafvf)btgG1BK=++FXu zdu-vjxp&7~`3B`y>_LO@M9H})N4w_nk7$L|0I@UI^tdMSi3(Sq zp_9H~H~QoS6A-cginPU?6)aOuwSjHf@5x%QH@2UD^gV0iWPy&}qp{aP0s6qVdO!e@ zJv=YEivN5ebs%8q{+s**qj$;8Ggp?(9Yplh zPY4Zkk;Hx7z-l8v?ah8&fDJVGOdvDay>0OM9tiWRUFmT)UgkQ0z7Lza)z@ixGu7s} zvV~uc`JMt0z+QOiEbx8#=JC&X)~STO#WAZOmTWIG%uF1Mcvlu3x1MJzYk}%KTZ-7) zpI--_L*`6W$M=QADyD8U(wkAQkd0XR@-XH2nHzM#5#EgWYSXjxGsdu1=wV6s{LJ_l z#7@PyF+ywXRP;5|zR2@MI9ex?M>3R5QVH7=Mj17S|J-pSR7d;JS(M|MYr zX3nNqKHhH{+BUfkq(Zu_BzL!MU_tFPl}8L7AM|B=_VGVAZgXH1bgGYZl5WNg%pFZL zOcm?NoD^)Pc2TIfllQzEwr5c0*lQ}Y6xkSe4offyIj|Al-Qsjo7`5F?>#I#q7{(j1 zuQ>!|n|>(FVK`SbQ9rNAeBAqHe8BofSge zPcCrDog_v3HKXXc7FMJI+EEN?OWjbKGTVbqW9Pz|OLN)@=+Kl#o-9vmeN-Z~^}44K z-Xtgfp)*za=$o+ni1HA#Z%DN_<4P8j<*p~9x(aRn#bL+`u2yqi76xW^OB8O0E0|V$ zjUf)s@6u~nv-P#N^z&PRn^~V{RDfLD^v(n+4kS+0Q?wu(`RlX%OS_6QiYF&e&jYu} z182#sb;YNU9+ySx#OSyYJJw|x$(u9x~&0Lk=tQR1mLY-8L}kX`}x*XH_5Yy zb_J#jW*ejkLeHFl>CTf$9jYk;CkuDf3!CGoXwRd z)RBn2lrIcs?%%Qp3%{Fmc22SEP{rT>NLeGo%WsUfF+lH7WZ6*&A2ir@%VOOpcXmX& z=yw}wxLknpS8K*x$7Hpx=aSQ?^JjjilQmZ|hLc<*{FL!;VqVjz5m=W>_ZJRHPDSCtx$&E5|#p9l8 zgxc19>voxmA2$~oJWhJM_LsiiiH$uwJrVtgW#d-e$h#1Mt`P?gunXJhUcWYtF_rm7UmK@+;A9M51hVJAmh~_9-x5J zcM4~~U#eKMH2TVSMTM_%Z~FQNM+V-v9y8Vc`6O7T|KO*Hc!2G#;&Aaydip*78YxS( zN`pum9z)`X%qU2+$$bHpxW3Pjbs=cLQmeAD>#cU@fl%yCK!*_@Y*1uA@T@)FvJbYq z)L^C{JJO3De(UjMsVjPfVQ=(!;r$`X^7_dnd@1XbXQSoiY=XA#>aFsoYnJglE7{iE zMgiQHSE;|$&3nXqk|{cgqdn+rJ)+bZRd}Tn_-7Pa$E=fdVH3d1+gk>}EHfC9Ag|~T zF%E3ot<5JEnEGB^*?48^2laL)(gP+@2MtDKJBsORKN(6&d$c+35M}n z|MSpul<5sm68B$AjviU%C!YrO}jWhUN@Zlt_IWE<+0Y-K`P1J?Q>Kw9Aq6Sn(N>dqSe51kA&6Iu1VuU)c{!`@n+{#dNQ7QQ6 zY9+R?uX32g1nn>QgHE#|2`|zo`)j)SYJd+DX1d5kO}@P2iD{*N5TBy(h7nC4r1X}O zDtmR_^VU)w`w%g6^)il=f<2$!E_CP4pLC~s$(RRHbaLOufsAwX|846=k*h<_Z98`SYQ90KmFDV&`Kq=c<3CD?6CMFX>;C0@Gx&Z|^DniDj!Z zII+D*qmr}Bs>nntNn%o{OEU8Q@V6GF*NH=s!Rw0cFKZcy8s?H7{h1NtB0Spt;$)Ra zkz*P4O-awX_@5}0y_I=#Zr%$w``FL9Wm{`i)1^-Sj8kUDucUM`s(SePFk~(TcdDVP z{LDLSb{pYop+ThmqOoC9M%^$`#N&tWy7}}O-ctag(N@6h%m*@^$0R-PLJsJD1u{kO zY8B}6BzOml0S(d;;uvMcI7*)P0XvHLcGg^E_S8gT<_ zGCaW|5F?S-;bJ6ykSiusMF?}1D`!FW8fO4(iC;e1@I6_COaml=H~NN`@zb$J>F2uQ1pVV zVZC)LNtgPUL&?_e?(c(~{!&MKCY2}mHcX4cvwQsw%5L7wGtPs}x_aCJj-JM(y zuAY`&Q{XY6*8B5t{@Ukz)t167{O=CI;A`pK17?I?U)sszx{_ioE!G>VtiI#afb`A< zrX?*dCm5n0lcm-D`u^J3O5<8Jl07i05BPG2>pD(1-L(|0BQ@z#cD*~JN$}5O3hFh2b6K{=Aju`3p-%VM&<0uhXvB&z7j4y&^#g%Mu z3ucVZN+G%r3kT*bh5a*7YxkV1cq|uhm0ybe=sGek2kY&t5A`exIB!t z$*Zq$KMscD{cv$wqtX`{joFqZIc5^cTcTh8Dj3kz&Y|Q~u2K?F4nO+DJ-l=x8Vo*2 z9ZP29_Q$)6*}vGapLDNmu8~EcIqO$LLsrUQL%A!0d}baEo%5c9ulKE^L?q&6c7q2B z^YI-$N#+v5zp5-BREKZG4;xhJc7z&VS{v58<;7P|(C0jN+iN}xcBJL9nQ~a_aAUk= zl3#RbE7?G_f)(7v^qG7eY5#=wi$`27>dvdTGYGSBBOKHmuJw5+vMh?RyxGtlRJ#z9 z=}}%p_X{_!$^RmBnRm1?Mo{Rd|pbd!pNk**QQZxc*MbLauAfwHFNd4T52~aH1P-67H_l zp)-E0A;Isl6&@4BF2lFBk#l{a5`C4X+2lrQnSq#|TW=4)S61W>!?fu1?crUxvcZ5N z<-plL?%{?@*3S6bE;}E;ytSKO@Swt7Xtz-iWg4olPc~pnd0)fwB8=^wOiF2uQp3Ia zvXM&1x6>w5xo%|}&h8LlXw#M0uW#7fGvq!r+f2!*{-P_3-#H+tWffjqk2%sj(Wpw0 zz4si9(Ds;oSNC9yFTeHvA6T$Q-dwuSz^1Un3VGFgMQoI}2twIE5X#iBq3`D&wg)X& zV7Z#I!@4H8uxR9ouBaP_VT=!UT>|+%y6+*C6g|7IW*3LHehS>SvR9V3Q|R5O^)|fb zY+`<3^+yF3(#bp;Yh^7T?|6((ef}vhc(+quO^zE?BKPJOSxJ)qdzns?JzX2RgfGE! zJcR>SHuJpR&AdpvHOP5#usyGvo9@5rIm%DZF16xgj{DrSIU!L|>jaxaAr32#rNSpX zWbDJtWtYI$YPbB-*TuZ*u4-BDcduP@LpxcbozNPtKNK+(%X)pu54B^{Tm8>mhzW{T zIv(w*uo^wU(sP?X|DKHOA71c{yUiDxt=P%yN)KyeNEOdIe$+ADHzt|8JVJZ1H>Bsq zuR!6aQWGAR8y_|nZTClsSSN@u+fLnyacM~S#^TtZ=<+@I$AQM;y+HpfA1cOYoa5yO zooSXH%u7!kTXb`7+kGpy6aKQGVDM=+)blycQcw-&EMt-DCcF0RsrRqEL9gi@B8Fv<;S}8H#7;i&&L_!r``9cH8ul<;%{-@ z&UMqetG{hv_Bp>tsIQPzZqhhyGR>=zaXyQ;Y4X#pzVPo~KXT3#t7cwFljg%+)GzZb zI-kB#nwas!b1B2Z*4*Bd264p!F=u!7uoC$g71%ZnoP%?J&C}7YAYCNBc%3G*P^v#D zKYZ22?7L4Z($pwsu7cP@G8k0k=cxKS%F}im46Wk{u{D~eMRyD`7AxQB80pq(qT5@C zIz!x+MR*UM(_pp=Y<9epM{pYpy2w4d@{)@Ep+AbIij(($WEbTOuQttnp*%o~mP!)Z zwGX%8-#o*^ofOMbfB)>RpU^-YCqZ(2UJdXPR$Rlvf9{Ozbn*mT#Am=l_l2>9XCM8t ltuz#cnlOB}KZk Date: Wed, 17 Jan 2024 19:02:30 +0100 Subject: [PATCH 146/556] fix regression with screen not being put in right state --- .../siacs/conversations/ui/RtpSessionActivity.java | 13 +++++++++---- .../xmpp/jingle/JingleRtpConnection.java | 6 ++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2e4576f57..c40a49a72 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -532,6 +532,8 @@ public class RtpSessionActivity extends XmppActivity setWith(account.getRoster().getContact(with), null); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); + final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); + final Set media = actionToMedia(lastAction); final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); if (state != null) { @@ -548,10 +550,13 @@ public class RtpSessionActivity extends XmppActivity .fireJingleRtpConnectionStateUpdates()) { return; } - if (END_CARD.contains(state) - || xmppConnectionService - .getJingleConnectionManager() - .hasMatchingProposal(account, with)) { + if (END_CARD.contains(state)) { + return; + } + if (xmppConnectionService + .getJingleConnectionManager() + .hasMatchingProposal(account, with)) { + putScreenInCallMode(media); return; } Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0368f1a51..edb586565 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -34,7 +34,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.RtpSessionStatus; -import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xml.Element; @@ -73,7 +72,10 @@ public class JingleRtpConnection extends AbstractJingleConnection public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( - State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); + State.PROPOSED, + State.PROCEED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED); private static final long BUSY_TIME_OUT = 30; private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); From 1090b2edd33b969cdae715f56c71aeace635dbf4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 18 Jan 2024 13:09:04 +0100 Subject: [PATCH 147/556] add optional strict offline checking for calls --- .../java/eu/siacs/conversations/Config.java | 2 ++ .../conversations/entities/Presences.java | 6 ++++ .../services/CallIntegration.java | 1 + .../CallIntegrationConnectionService.java | 28 ++++++++++----- .../conversations/ui/RtpSessionActivity.java | 5 ++- .../xmpp/jingle/RtpEndUserState.java | 35 ++++++++++--------- src/main/res/values/strings.xml | 1 + 7 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index febe12b14..da3628b6f 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -121,6 +121,8 @@ public final class Config { false; // disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback) public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean USE_JINGLE_MESSAGE_INIT = true; + + public static final boolean JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK = false; public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index 59480b0ce..d3bd706f8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -83,6 +83,12 @@ public class Presences { } } + public boolean isEmpty() { + synchronized (this.presences) { + return this.presences.isEmpty(); + } + } + public String[] toResourceArray() { synchronized (this.presences) { final String[] presencesArray = new String[presences.size()]; diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 489d71256..f07ebe28d 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -303,6 +303,7 @@ public class CallIntegration extends Connection { @Override public void onStateChanged(final int state) { Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); + // TODO devices before selfManaged() will likely have to play their own ringback sound if (state == STATE_ACTIVE) { playConnectedSound(); } else if (state == STATE_DISCONNECTED) { diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 99f565ff1..de39abb51 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -97,15 +97,26 @@ public class CallIntegrationConnectionService extends ConnectionService { intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - final CallIntegration callIntegration; + final Connection callIntegration; if (with.isBareJid()) { - final var proposal = - service.getJingleConnectionManager() - .proposeJingleRtpSession(account, with, media); - - intent.putExtra( - RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, - RtpEndUserState.FINDING_DEVICE.toString()); + final var contact = account.getRoster().getContact(with); + if (Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK + && contact.getPresences().isEmpty()) { + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, + RtpEndUserState.CONTACT_OFFLINE.toString()); + callIntegration = + Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "contact is offline")); + } else { + final var proposal = + service.getJingleConnectionManager() + .proposeJingleRtpSession(account, with, media); + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, + RtpEndUserState.FINDING_DEVICE.toString()); + callIntegration = proposal.getCallIntegration(); + } if (Media.audioOnly(media)) { intent.putExtra( RtpSessionActivity.EXTRA_LAST_ACTION, @@ -115,7 +126,6 @@ public class CallIntegrationConnectionService extends ConnectionService { RtpSessionActivity.EXTRA_LAST_ACTION, RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } - callIntegration = proposal.getCallIntegration(); } else { final JingleRtpConnection jingleRtpConnection = service.getJingleConnectionManager().initializeRtpSession(account, with, media); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index c40a49a72..ee0770023 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -95,6 +95,7 @@ public class RtpSessionActivity extends XmppActivity RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.SECURITY_ERROR, RtpEndUserState.DECLINED_OR_BUSY, + RtpEndUserState.CONTACT_OFFLINE, RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.CONNECTIVITY_LOST_ERROR, RtpEndUserState.RETRACTED); @@ -881,6 +882,7 @@ public class RtpSessionActivity extends XmppActivity case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device); case RINGING -> setTitle(R.string.rtp_state_ringing); case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy); + case CONTACT_OFFLINE -> setTitle(R.string.rtp_state_contact_offline); case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error); case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error); case RETRACTED -> setTitle(R.string.rtp_state_retracted); @@ -974,7 +976,8 @@ public class RtpSessionActivity extends XmppActivity this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition))); this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24); this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { + } else if (asList(RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONTACT_OFFLINE) + .contains(state)) { this.binding.rejectCall.setContentDescription(getString(R.string.exit)); this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 885820460..fff82031a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -1,20 +1,23 @@ package eu.siacs.conversations.xmpp.jingle; public enum RtpEndUserState { - INCOMING_CALL, //received a 'propose' message - CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet - CONNECTED, //session-accepted and webrtc peer connection is connected - RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed - INCOMING_CONTENT_ADD, //session-accepted with a pending, incoming content-add - FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet - RINGING, //'propose' has been sent out and it has been 184 acked - ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received - ENDING_CALL, //libwebrt says 'closed' but session-terminate has not gone through - ENDED, //close UI - DECLINED_OR_BUSY, //other party declined; no retry button - CONNECTIVITY_ERROR, //network error; retry button - CONNECTIVITY_LOST_ERROR, //network error but for call duration > 0 - RETRACTED, //user pressed home or power button during 'ringing' - shows retry button - APPLICATION_ERROR, //something rather bad happened; libwebrtc failed or we got in IQ-error - SECURITY_ERROR //problem with DTLS (missing) or verification + INCOMING_CALL, // received a 'propose' message + CONNECTING, // session-initiate or session-accepted but no webrtc peer connection yet + CONNECTED, // session-accepted and webrtc peer connection is connected + RECONNECTING, // session-accepted and webrtc peer connection was connected once but is currently + // disconnected or failed + INCOMING_CONTENT_ADD, // session-accepted with a pending, incoming content-add + FINDING_DEVICE, // 'propose' has been sent out; no 184 ack yet + RINGING, // 'propose' has been sent out and it has been 184 acked + ACCEPTING_CALL, // 'proceed' message has been sent; but no session-initiate has been received + ENDING_CALL, // libwebrt says 'closed' but session-terminate has not gone through + ENDED, // close UI + DECLINED_OR_BUSY, // other party declined; no retry button + CONTACT_OFFLINE, // when `JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK` is true this shows up when + // the contact is offline, generally similar to BUSY + CONNECTIVITY_ERROR, // network error; retry button + CONNECTIVITY_LOST_ERROR, // network error but for call duration > 0 + RETRACTED, // user pressed home or power button during 'ringing' - shows retry button + APPLICATION_ERROR, // something rather bad happened; libwebrtc failed or we got in IQ-error + SECURITY_ERROR // problem with DTLS (missing) or verification } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 996977eab..f47731d06 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -929,6 +929,7 @@ Discovering devices Ringing Busy + Contact is not available Could not connect call Connection lost Retracted call From bcc0c32af3277a5628264fd96efca64d389c7080 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 18 Jan 2024 13:16:14 +0100 Subject: [PATCH 148/556] fix crash when using direct jingle init on offline contacts --- .../eu/siacs/conversations/ui/util/PresenceSelector.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java b/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java index 6edcf3349..d9cda665a 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java +++ b/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java @@ -32,6 +32,7 @@ package eu.siacs.conversations.ui.util; import android.app.Activity; import android.content.Context; import android.util.Pair; +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; @@ -60,7 +61,9 @@ public class PresenceSelector { public static void selectFullJidForDirectRtpConnection(final Activity activity, final Contact contact, final RtpCapability.Capability required, final OnFullJidSelected onFullJidSelected) { final String[] resources = RtpCapability.filterPresences(contact, required); - if (resources.length == 1) { + if (resources.length == 0) { + Toast.makeText(activity,R.string.rtp_state_contact_offline,Toast.LENGTH_LONG).show(); + } else if (resources.length == 1) { onFullJidSelected.onFullJidSelected(contact.getJid().withResource(resources[0])); } else { showPresenceSelectionDialog(activity, contact, resources, onFullJidSelected); From b7da7f3367fc2b2e53f9eb5bf8b23f7710addef1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 18 Jan 2024 19:54:55 +0100 Subject: [PATCH 149/556] add generator for JMI finish message --- .../generator/MessageGenerator.java | 16 +++++++++++++++- .../conversations/parser/MessageParser.java | 4 +++- .../CallIntegrationConnectionService.java | 12 ++++++++++++ .../xmpp/jingle/JingleConnectionManager.java | 10 ++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 4e99ab086..18322453e 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { @@ -228,6 +229,20 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public MessagePacket sessionFinish( + final Jid with, final String sessionId, final Reason reason) { + final MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setTo(with); + packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + sessionId); + final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE); + finish.setAttribute("id", sessionId); + final Element reasonElement = finish.addChild("reason", Namespace.JINGLE); + reasonElement.addChild(reason.toString()); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } + public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those @@ -238,7 +253,6 @@ public class MessageGenerator extends AbstractGenerator { for (final Media media : proposal.media) { propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString()); } - packet.addChild("request", "urn:xmpp:receipts"); packet.addChild("store", "urn:xmpp:hints"); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index bf3cba178..d20e4dd46 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -53,7 +53,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); private static final List JINGLE_MESSAGE_ELEMENT_NAMES = - Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing"); + Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish"); public MessageParser(XmppConnectionService service) { super(service); @@ -913,6 +913,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.d(Config.LOGTAG, "unable to find original rtp session message for received propose"); } + } else if ("finish".equals(action)) { + Log.d(Config.LOGTAG,"received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration"); } } else { //MAM reloads (non catchups diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index de39abb51..013a3964a 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -30,16 +30,19 @@ import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import java.util.Collection; import java.util.Collections; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -108,6 +111,9 @@ public class CallIntegrationConnectionService extends ConnectionService { callIntegration = Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR, "contact is offline")); + // we can use a JMI 'finish' message to notify the contact of a call we never + // actually attempted + // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR); } else { final var proposal = service.getJingleConnectionManager() @@ -137,6 +143,12 @@ public class CallIntegrationConnectionService extends ConnectionService { return callIntegration; } + private static void sendJingleFinishMessage( + final XmppConnectionService service, final Contact contact, final Reason reason) { + service.getJingleConnectionManager() + .sendJingleMessageFinish(contact, UUID.randomUUID().toString(), reason); + } + @Override public Connection onCreateOutgoingConnection( final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index e90a35a0c..c07a3b60f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -746,6 +746,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void sendJingleMessageFinish( + final Contact contact, final String sessionId, final Reason reason) { + final var account = contact.getAccount(); + final MessagePacket messagePacket = + mXmppConnectionService + .getMessageGenerator() + .sessionFinish(contact.getJid(), sessionId, reason); + mXmppConnectionService.sendMessagePacket(account, messagePacket); + } + public boolean hasMatchingProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { for (Map.Entry entry : From ef5508e5b1ea3c50adefe3264de574ab86f47a54 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 10:49:48 +0100 Subject: [PATCH 150/556] trigger incoming call integration only for rtp connections --- .../conversations/xmpp/jingle/JingleConnectionManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index c07a3b60f..d9389c506 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -135,7 +135,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } connections.put(id, connection); - CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + if (connection instanceof JingleRtpConnection) { + CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + } mXmppConnectionService.updateConversationUi(); connection.deliverPacket(packet); From 32da5853d7570749a602c97093593da64f4906d8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 16:01:07 +0100 Subject: [PATCH 151/556] track offline message queue --- .../services/XmppConnectionService.java | 5 +++++ .../conversations/xmpp/XmppConnection.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 41c467b15..8e79ef1b1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -371,14 +371,19 @@ public class XmppConnectionService extends Service { } final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); final boolean catchup = getMessageArchiveService().inCatchup(account); + final boolean trackOfflineMessageRetrieval; if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) { + trackOfflineMessageRetrieval = false; sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages"); } }); + } else { + trackOfflineMessageRetrieval = true; } sendPresence(account); + account.getXmppConnection().trackOfflineMessageRetrieval(trackOfflineMessageRetrieval); if (mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 9141e1c70..0500517dc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -165,6 +165,7 @@ public class XmppConnection implements Runnable { private boolean inSmacksSession = false; private boolean quickStartInProgress = false; private boolean isBound = false; + private boolean offlineMessagesRetrieved = false; private Element streamFeatures; private Element boundStreamFeatures; private StreamId streamId = null; @@ -2200,6 +2201,7 @@ public class XmppConnection implements Runnable { } private void finalizeBind() { + this.offlineMessagesRetrieved = false; if (bindListener != null) { bindListener.onBind(account); } @@ -2727,6 +2729,24 @@ public class XmppConnection implements Runnable { return mXmppConnectionService.getIqGenerator(); } + public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) { + if (trackOfflineMessageRetrieval) { + final IqPacket iqPing = new IqPacket(IqPacket.TYPE.GET); + iqPing.addChild("ping", Namespace.PING); + this.sendIqPacket( + iqPing, + (a, response) -> { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received ping response after sending initial presence"); + XmppConnection.this.offlineMessagesRetrieved = true; + }); + } else { + this.offlineMessagesRetrieved = true; + } + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { From 5158fc4530d9de249ed5eeb7e2f89fc1bd29a45d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 19 Jan 2024 18:01:29 +0100 Subject: [PATCH 152/556] do not process JMI from offline queue as live messages --- .../conversations/parser/MessageParser.java | 43 ++++++++++++------- .../conversations/xmpp/XmppConnection.java | 6 ++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index d20e4dd46..cb0620fa1 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -378,6 +378,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Element result = MessageArchiveService.Version.findResult(original); final String queryId = result == null ? null : result.getAttribute("queryid"); final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId); + final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved(); if (query != null && query.validFrom(original.getFrom())) { final Pair f = original.getForwardedMessagePacket("result", query.version.namespace); if (f == null) { @@ -852,7 +853,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (sessionId == null) { break; } - if (query == null) { + if (query == null && offlineMessagesRetrieved) { if (serverMsgId == null) { serverMsgId = extractStanzaId(account, packet); } @@ -873,24 +874,25 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece && contact.showInContactList()) { processMessageReceipts(account, packet, remoteMsgId, null); } - } else if (query.isCatchup()) { + } else if ((query != null && query.isCatchup()) || !offlineMessagesRetrieved) { if ("propose".equals(action)) { final Element description = child.findChild("description"); - final String namespace = description == null ? null : description.getNamespace(); + final String namespace = + description == null ? null : description.getNamespace(); if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); - final Message preExistingMessage = c.findRtpSession(sessionId, status); + final Conversation c = + mXmppConnectionService.findOrCreateConversation( + account, counterpart.asBareJid(), false, false); + final Message preExistingMessage = + c.findRtpSession(sessionId, status); if (preExistingMessage != null) { preExistingMessage.setServerMsgId(serverMsgId); mXmppConnectionService.updateMessage(preExistingMessage); break; } - final Message message = new Message( - c, - status, - Message.TYPE_RTP_SESSION, - sessionId - ); + final Message message = + new Message( + c, status, Message.TYPE_RTP_SESSION, sessionId); message.setServerMsgId(serverMsgId); message.setTime(timestamp); message.setBody(new RtpSessionStatus(false, 0).toString()); @@ -898,9 +900,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.databaseBackend.createMessage(message); } } else if ("proceed".equals(action)) { - //status needs to be flipped to find the original propose - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); - final int s = packet.fromAccount(account) ? Message.STATUS_RECEIVED : Message.STATUS_SEND; + // status needs to be flipped to find the original propose + final Conversation c = + mXmppConnectionService.findOrCreateConversation( + account, counterpart.asBareJid(), false, false); + final int s = + packet.fromAccount(account) + ? Message.STATUS_RECEIVED + : Message.STATUS_SEND; final Message message = c.findRtpSession(sessionId, s); if (message != null) { message.setBody(new RtpSessionStatus(true, 0).toString()); @@ -910,11 +917,15 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setTime(timestamp); mXmppConnectionService.updateMessage(message, true); } else { - Log.d(Config.LOGTAG, "unable to find original rtp session message for received propose"); + Log.d( + Config.LOGTAG, + "unable to find original rtp session message for received propose"); } } else if ("finish".equals(action)) { - Log.d(Config.LOGTAG,"received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration"); + Log.d( + Config.LOGTAG, + "received JMI 'finish' during MAM catch-up. Can be used to update success/failure and duration"); } } else { //MAM reloads (non catchups diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 0500517dc..7011c4d54 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2739,7 +2739,7 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": received ping response after sending initial presence"); + + ": got ping response after sending initial presence"); XmppConnection.this.offlineMessagesRetrieved = true; }); } else { @@ -2747,6 +2747,10 @@ public class XmppConnection implements Runnable { } } + public boolean isOfflineMessagesRetrieved() { + return this.offlineMessagesRetrieved; + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { From bff1ac5ebc0db303385166b83ae5181ca29a0d20 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 20 Jan 2024 11:41:37 +0100 Subject: [PATCH 153/556] do not nofiy UI if UI triggered retract --- .../xmpp/jingle/JingleConnectionManager.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d9389c506..4356a0686 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -659,12 +659,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } if (matchingProposal != null) { - retractSessionProposal(matchingProposal); + retractSessionProposal(matchingProposal, false); } } } private void retractSessionProposal(final RtpSessionProposal rtpSessionProposal) { + retractSessionProposal(rtpSessionProposal, true); + } + + private void retractSessionProposal( + final RtpSessionProposal rtpSessionProposal, final boolean refresh) { final Account account = rtpSessionProposal.account; Log.d( Config.LOGTAG, @@ -673,11 +678,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); rtpSessionProposal.callIntegration.retracted(); - mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - rtpSessionProposal.with, - rtpSessionProposal.sessionId, - RtpEndUserState.RETRACTED); + if (refresh) { + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + rtpSessionProposal.with, + rtpSessionProposal.sessionId, + RtpEndUserState.RETRACTED); + } final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); writeLogMissedOutgoing( From bfe2aff7a158e767aad1a567f070a799690660b9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 10:07:35 +0100 Subject: [PATCH 154/556] show speaker selection during 'ringing' --- .../conversations/ui/RtpSessionActivity.java | 89 +++++++++++++++---- .../xmpp/jingle/AbstractJingleConnection.java | 8 +- .../xmpp/jingle/JingleConnectionManager.java | 32 +++++-- .../xmpp/jingle/JingleRtpConnection.java | 19 +++- .../xmpp/jingle/OngoingRtpSession.java | 9 ++ 5 files changed, 127 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index ee0770023..429f2cfdb 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -63,6 +63,7 @@ import eu.siacs.conversations.xmpp.jingle.ContentAddition; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; +import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -117,6 +118,13 @@ public class RtpSessionActivity extends XmppActivity RtpEndUserState.ACCEPTING_CALL, RtpEndUserState.CONNECTING, RtpEndUserState.RECONNECTING); + private static final List STATES_SHOWING_SPEAKER_CONFIGURATION = + new ImmutableList.Builder() + .add(RtpEndUserState.FINDING_DEVICE) + .add(RtpEndUserState.RINGING) + .add(RtpEndUserState.CONNECTING) + .addAll(STATES_CONSIDERED_CONNECTED) + .build(); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; private static final int REQUEST_ACCEPT_CONTENT = 0x1112; @@ -139,8 +147,13 @@ public class RtpSessionActivity extends XmppActivity public static Set actionToMedia(final String action) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) { return ImmutableSet.of(Media.AUDIO, Media.VIDEO); - } else { + } else if (ACTION_MAKE_VOICE_CALL.equals(action)) { return ImmutableSet.of(Media.AUDIO); + } else { + Log.w( + Config.LOGTAG, + "actionToMedia can not get media set from unknown action " + action); + return Collections.emptySet(); } } @@ -274,14 +287,15 @@ public class RtpSessionActivity extends XmppActivity private void retractSessionProposal() { final Intent intent = getIntent(); final String action = intent.getAction(); + final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); if (!Intent.ACTION_VIEW.equals(action) || state == null || !END_CARD.contains(RtpEndUserState.valueOf(state))) { - resetIntent( - account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); + final Set media = actionToMedia(lastAction == null ? action : lastAction); + resetIntent(account, with, RtpEndUserState.RETRACTED, media); } xmppConnectionService .getJingleConnectionManager() @@ -1049,6 +1063,14 @@ public class RtpSessionActivity extends XmppActivity } else { this.binding.inCallActionLeft.setVisibility(View.GONE); } + } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state) + && !isPictureInPicture() + && Media.audioOnly(media)) { + final CallIntegration callIntegration = requireCallIntegration(); + updateInCallButtonConfigurationSpeaker( + callIntegration.getSelectedAudioDevice(), + callIntegration.getAudioDevices().size()); + this.binding.inCallActionFarRight.setVisibility(View.GONE); } else { this.binding.inCallActionLeft.setVisibility(View.GONE); this.binding.inCallActionRight.setVisibility(View.GONE); @@ -1297,17 +1319,13 @@ public class RtpSessionActivity extends XmppActivity } } - private void switchToEarpiece(View view) { - requireRtpConnection() - .getCallIntegration() - .setAudioDevice(CallIntegration.AudioDevice.EARPIECE); + private void switchToEarpiece(final View view) { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); acquireProximityWakeLock(); } - private void switchToSpeaker(View view) { - requireRtpConnection() - .getCallIntegration() - .setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); + private void switchToSpeaker(final View view) { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); releaseProximityWakeLock(); } @@ -1359,6 +1377,33 @@ public class RtpSessionActivity extends XmppActivity return connection; } + private CallIntegration requireCallIntegration() { + return requireOngoingRtpSession().getCallIntegration(); + } + + private OngoingRtpSession requireOngoingRtpSession() { + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection != null) { + return connection; + } + final Intent currentIntent = getIntent(); + final String withExtra = + currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); + final var account = extractAccount(currentIntent); + if (withExtra == null) { + throw new IllegalStateException("Current intent has no EXTRA_WITH"); + } + final var matching = + xmppConnectionService + .getJingleConnectionManager() + .matchingProposal(account, Jid.of(withExtra)); + if (matching.isPresent()) { + return matching.get(); + } + throw new IllegalStateException("No matching session proposal"); + } + @Override public void onJingleRtpConnectionUpdate( Account account, Jid with, final String sessionId, RtpEndUserState state) { @@ -1425,16 +1470,23 @@ public class RtpSessionActivity extends XmppActivity + ", available:" + availableAudioDevices); try { - final RtpEndUserState endUserState = requireRtpConnection().getEndUserState(); - final Set media = getMedia(); + final OngoingRtpSession ongoingRtpSession = requireOngoingRtpSession(); + final RtpEndUserState endUserState; + if (ongoingRtpSession instanceof JingleRtpConnection jingleRtpConnection) { + endUserState = jingleRtpConnection.getEndUserState(); + } else { + // for session proposals all end user states are functionally the same + endUserState = RtpEndUserState.RINGING; + } + final Set media = ongoingRtpSession.getMedia(); if (END_CARD.contains(endUserState)) { Log.d( Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { - if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) { - final CallIntegration callIntegration = - requireRtpConnection().getCallIntegration(); + if (Media.audioOnly(media) + && STATES_SHOWING_SPEAKER_CONFIGURATION.contains(endUserState)) { + final CallIntegration callIntegration = requireCallIntegration(); updateInCallButtonConfigurationSpeaker( callIntegration.getSelectedAudioDevice(), callIntegration.getAudioDevices().size()); @@ -1457,16 +1509,17 @@ public class RtpSessionActivity extends XmppActivity if (withExtra == null) { return; } + final Set media = actionToMedia(currentIntent.getStringExtra(EXTRA_LAST_ACTION)); if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) { runOnUiThread( () -> { updateVerifiedShield(false); updateStateDisplay(state); - updateButtonConfiguration(state); + updateButtonConfiguration(state, media, null); updateIncomingCallScreen(state); invalidateOptionsMenu(); }); - resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); + resetIntent(account, with, state, media); } } 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 efc32f5ff..6aeb348c1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -219,8 +219,7 @@ public abstract class AbstractJingleConnection { if (isTerminated()) { this.jingleConnectionManager.finishConnectionOrThrow(this); } else { - throw new AssertionError( - String.format("Unable to call finish from %s", this.state)); + throw new AssertionError(String.format("Unable to call finish from %s", this.state)); } } @@ -348,7 +347,7 @@ public abstract class AbstractJingleConnection { return features != null && features.contains(feature); } - public static class Id implements OngoingRtpSession { + public static class Id { public final Account account; public final Jid with; public final String sessionId; @@ -400,17 +399,14 @@ public abstract class AbstractJingleConnection { return Objects.hashCode(account.getUuid(), with, sessionId); } - @Override public Account getAccount() { return account; } - @Override public Jid getWith() { return with; } - @Override public String getSessionId() { return sessionId; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 4356a0686..9dee8904d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -601,11 +601,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { public Optional getOngoingRtpConnection(final Contact contact) { for (final Map.Entry entry : this.connections.entrySet()) { - if (entry.getValue() instanceof JingleRtpConnection) { + if (entry.getValue() instanceof JingleRtpConnection jingleRtpConnection) { final AbstractJingleConnection.Id id = entry.getKey(); if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { - return Optional.of(id); + return Optional.of(jingleRtpConnection); } } } @@ -765,9 +765,22 @@ public class JingleConnectionManager extends AbstractConnectionManager { mXmppConnectionService.sendMessagePacket(account, messagePacket); } + public Optional matchingProposal(final Account account, final Jid with) { + synchronized (this.rtpSessionProposals) { + for (final Map.Entry entry : + this.rtpSessionProposals.entrySet()) { + final RtpSessionProposal proposal = entry.getKey(); + if (proposal.account == account && with.asBareJid().equals(proposal.with)) { + return Optional.of(proposal); + } + } + } + return Optional.absent(); + } + public boolean hasMatchingProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : + for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { final var state = entry.getValue(); final RtpSessionProposal proposal = entry.getKey(); @@ -1102,9 +1115,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { return sessionId; } + @Override public CallIntegration getCallIntegration() { return this.callIntegration; } + + @Override + public Set getMedia() { + return this.media; + } } public class ProposalStateCallback implements CallIntegration.Callback { @@ -1126,8 +1145,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { @Override public void onAudioDeviceChanged( - CallIntegration.AudioDevice selectedAudioDevice, - Set availableAudioDevices) {} + final CallIntegration.AudioDevice selectedAudioDevice, + final Set availableAudioDevices) { + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + selectedAudioDevice, availableAudioDevices); + } @Override public void onCallIntegrationReject() {} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index edb586565..df48ee8d7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -30,6 +30,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.CryptoFailedException; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; @@ -68,7 +69,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class JingleRtpConnection extends AbstractJingleConnection - implements WebRTCWrapper.EventCallback, CallIntegration.Callback { + implements WebRTCWrapper.EventCallback, CallIntegration.Callback, OngoingRtpSession { public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( @@ -2645,6 +2646,7 @@ public class JingleRtpConnection extends AbstractJingleConnection return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } + @Override public CallIntegration getCallIntegration() { return this.callIntegration; } @@ -2870,6 +2872,21 @@ public class JingleRtpConnection extends AbstractJingleConnection return remoteHasFeature(Namespace.SDP_OFFER_ANSWER); } + @Override + public Account getAccount() { + return id.account; + } + + @Override + public Jid getWith() { + return id.with; + } + + @Override + public String getSessionId() { + return id.sessionId; + } + private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OngoingRtpSession.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OngoingRtpSession.java index ebd2d8850..358411bcb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OngoingRtpSession.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OngoingRtpSession.java @@ -1,10 +1,19 @@ package eu.siacs.conversations.xmpp.jingle; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.xmpp.Jid; +import java.util.Set; + public interface OngoingRtpSession { Account getAccount(); + Jid getWith(); + String getSessionId(); + + CallIntegration getCallIntegration(); + + Set getMedia(); } From a78747eaa275083cbd52159bc7bc2b0e55cb4bee Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 11:08:43 +0100 Subject: [PATCH 155/556] react to onSilence() and stop ringtone --- .../eu/siacs/conversations/services/CallIntegration.java | 7 +++++++ .../conversations/xmpp/jingle/JingleConnectionManager.java | 5 +++++ .../conversations/xmpp/jingle/JingleRtpConnection.java | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index f07ebe28d..fa262768b 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -300,6 +300,11 @@ public class CallIntegration extends Connection { return this.appRTCAudioManager; } + @Override + public void onSilence() { + this.callback.onCallIntegrationSilence(); + } + @Override public void onStateChanged(final int state) { Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); @@ -473,5 +478,7 @@ public class CallIntegration extends Connection { void onCallIntegrationReject(); void onCallIntegrationAnswer(); + + void onCallIntegrationSilence(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 9dee8904d..8541f6a44 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1156,5 +1156,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { @Override public void onCallIntegrationAnswer() {} + + @Override + public void onCallIntegrationSilence() { + + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index df48ee8d7..74eb4abd0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2718,6 +2718,11 @@ public class JingleRtpConnection extends AbstractJingleConnection xmppConnectionService.startActivity(intent); } + @Override + public void onCallIntegrationSilence() { + xmppConnectionService.getNotificationService().stopSoundAndVibration(); + } + @Override public void onAudioDeviceChanged( final CallIntegration.AudioDevice selectedAudioDevice, From 21b8bf424a7488807358e7c0b56dd8c6de39faed Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 13:53:41 +0100 Subject: [PATCH 156/556] skip automatic audio device selection when BT is available --- .../siacs/conversations/services/CallIntegration.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index fa262768b..378dd6277 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -159,14 +159,15 @@ public class CallIntegration extends Connection { public void setAudioDeviceWhenAvailable(final AudioDevice audioDevice) { final var available = getAudioDevices(); - if (available.contains(audioDevice)) { + if (available.contains(audioDevice) && !available.contains(AudioDevice.BLUETOOTH)) { this.setAudioDevice(audioDevice); } else { Log.d( Config.LOGTAG, "application requested to switch to " + audioDevice - + " but device was not available"); + + " but we won't because available devices are " + + available); } } @@ -407,13 +408,14 @@ public class CallIntegration extends Connection { final Set availableAudioDevices) { if (this.initialAudioDevice != null && this.initialAudioDeviceConfigured.compareAndSet(false, true)) { - if (availableAudioDevices.contains(this.initialAudioDevice)) { + if (availableAudioDevices.contains(this.initialAudioDevice) + && !availableAudioDevices.contains(AudioDevice.BLUETOOTH)) { setAudioDevice(this.initialAudioDevice); Log.d(Config.LOGTAG, "configured initial audio device"); } else { Log.d( Config.LOGTAG, - "initial audio device not available. available devices: " + "not setting initial audio device. available devices: " + availableAudioDevices); } } From 18dea352b05622978422605b6408765e71ac78c2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 14:48:07 +0100 Subject: [PATCH 157/556] send jmi finish alongside session terminate --- .../conversations/generator/MessageGenerator.java | 1 - .../conversations/xmpp/jingle/JingleRtpConnection.java | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 18322453e..e217f7f1d 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -234,7 +234,6 @@ public class MessageGenerator extends AbstractGenerator { final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); packet.setTo(with); - packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + sessionId); final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE); finish.setAttribute("id", sessionId); final Element reasonElement = finish.addChild("reason", Namespace.JINGLE); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 74eb4abd0..e22d6574e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1986,6 +1986,7 @@ public class JingleRtpConnection extends AbstractJingleConnection protected void sendSessionTerminate(final Reason reason, final String text) { sendSessionTerminate(reason, text, this::writeLogMessage); + sendJingleMessageFinish(reason); } private void sendTransportInfo( @@ -2358,6 +2359,15 @@ public class JingleRtpConnection extends AbstractJingleConnection xmppConnectionService.sendMessagePacket(id.account, messagePacket); } + private void sendJingleMessageFinish(final Reason reason) { + final var account = id.getAccount(); + final MessagePacket messagePacket = + xmppConnectionService + .getMessageGenerator() + .sessionFinish(id.with, id.sessionId, reason); + xmppConnectionService.sendMessagePacket(account, messagePacket); + } + private boolean isOmemoEnabled() { final Conversational conversational = message.getConversation(); if (conversational instanceof Conversation) { From d2d76322b9204224db54903c05b2dd192d121661 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 21 Jan 2024 18:09:28 +0100 Subject: [PATCH 158/556] show speaker configuration during ACCEPTING --- src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 429f2cfdb..08351b7d5 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -122,6 +122,7 @@ public class RtpSessionActivity extends XmppActivity new ImmutableList.Builder() .add(RtpEndUserState.FINDING_DEVICE) .add(RtpEndUserState.RINGING) + .add(RtpEndUserState.ACCEPTING_CALL) .add(RtpEndUserState.CONNECTING) .addAll(STATES_CONSIDERED_CONNECTED) .build(); From a04dc6e4ad6512912129604213ad7b077a816cd7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 7 Feb 2024 15:02:06 +0100 Subject: [PATCH 159/556] show warning when call integration accounts exceed 10 --- .../CallIntegrationConnectionService.java | 32 +++++++++++++++++-- src/main/res/values/strings.xml | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 013a3964a..0455d4d6e 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -211,6 +211,25 @@ public class CallIntegrationConnectionService extends ConnectionService { } public static void registerPhoneAccount(final Context context, final Account account) { + try { + registerPhoneAccountOrThrow(context, account); + } catch (final IllegalArgumentException e) { + Toast.makeText(context, R.string.call_integration_not_available, Toast.LENGTH_LONG) + .show(); + } + } + + public static void registerPhoneAccountOrThrow(final Context context, final Account account) { + final var handle = getHandle(context, account); + final var telecomManager = context.getSystemService(TelecomManager.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (telecomManager.getOwnSelfManagedPhoneAccounts().contains(handle)) { + Log.d( + Config.LOGTAG, + "a phone account for " + account.getJid().asBareJid() + " already exists"); + return; + } + } final var builder = PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid()); builder.setSupportedUriSchemes(Collections.singletonList("xmpp")); @@ -220,14 +239,21 @@ public class CallIntegrationConnectionService extends ConnectionService { | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING); } final var phoneAccount = builder.build(); - - context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount); + telecomManager.registerPhoneAccount(phoneAccount); } public static void registerPhoneAccounts( final Context context, final Collection accounts) { for (final Account account : accounts) { - registerPhoneAccount(context, account); + try { + registerPhoneAccountOrThrow(context, account); + } catch (final IllegalArgumentException e) { + Log.w( + Config.LOGTAG, + "could not register phone account for " + account.getJid().asBareJid(), + e); + return; + } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f47731d06..4f67b8a95 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1027,4 +1027,5 @@ Report spam and block spammer Privacy policy Contact list integration is not available + Call integration not available! From e416a6c4eb4c0c77fe612af78ade88e761d20ed0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 Feb 2024 15:27:33 +0100 Subject: [PATCH 160/556] maintain phone accounts only for enabled accounts --- .../CallIntegrationConnectionService.java | 97 ++++++++++++++----- .../services/XmppConnectionService.java | 13 +-- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 0455d4d6e..01602dc70 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -22,6 +22,7 @@ import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; @@ -44,11 +45,16 @@ import java.util.Collections; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class CallIntegrationConnectionService extends ConnectionService { + private static final ExecutorService ACCOUNT_REGISTRATION_EXECUTOR = + Executors.newSingleThreadExecutor(); + private ListenableFuture serviceFuture; @Override @@ -210,16 +216,36 @@ public class CallIntegrationConnectionService extends ConnectionService { return jingleRtpConnection.getCallIntegration(); } - public static void registerPhoneAccount(final Context context, final Account account) { - try { - registerPhoneAccountOrThrow(context, account); - } catch (final IllegalArgumentException e) { - Toast.makeText(context, R.string.call_integration_not_available, Toast.LENGTH_LONG) - .show(); + public static void togglePhoneAccountAsync(final Context context, final Account account) { + ACCOUNT_REGISTRATION_EXECUTOR.execute(() -> togglePhoneAccount(context, account)); + } + + private static void togglePhoneAccount(final Context context, final Account account) { + if (account.isEnabled()) { + registerPhoneAccount(context, account); + } else { + unregisterPhoneAccount(context, account); } } - public static void registerPhoneAccountOrThrow(final Context context, final Account account) { + private static void registerPhoneAccount(final Context context, final Account account) { + try { + registerPhoneAccountOrThrow(context, account); + } catch (final IllegalArgumentException e) { + Log.w( + Config.LOGTAG, + "could not register phone account for " + account.getJid().asBareJid(), + e); + ContextCompat.getMainExecutor(context) + .execute(() -> showCallIntegrationNotAvailable(context)); + } + } + + private static void showCallIntegrationNotAvailable(final Context context) { + Toast.makeText(context, R.string.call_integration_not_available, Toast.LENGTH_LONG).show(); + } + + private static void registerPhoneAccountOrThrow(final Context context, final Account account) { final var handle = getHandle(context, account); final var telecomManager = context.getSystemService(TelecomManager.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -242,24 +268,39 @@ public class CallIntegrationConnectionService extends ConnectionService { telecomManager.registerPhoneAccount(phoneAccount); } - public static void registerPhoneAccounts( + public static void togglePhoneAccountsAsync( + final Context context, final Collection accounts) { + ACCOUNT_REGISTRATION_EXECUTOR.execute(() -> togglePhoneAccounts(context, accounts)); + } + + private static void togglePhoneAccounts( final Context context, final Collection accounts) { for (final Account account : accounts) { - try { - registerPhoneAccountOrThrow(context, account); - } catch (final IllegalArgumentException e) { - Log.w( - Config.LOGTAG, - "could not register phone account for " + account.getJid().asBareJid(), - e); - return; + if (account.isEnabled()) { + try { + registerPhoneAccountOrThrow(context, account); + } catch (final IllegalArgumentException e) { + Log.w( + Config.LOGTAG, + "could not register phone account for " + account.getJid().asBareJid(), + e); + } + } else { + unregisterPhoneAccount(context, account); } } } public static void unregisterPhoneAccount(final Context context, final Account account) { - context.getSystemService(TelecomManager.class) - .unregisterPhoneAccount(getHandle(context, account)); + final var handle = getHandle(context, account); + final var telecomManager = context.getSystemService(TelecomManager.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (telecomManager.getOwnSelfManagedPhoneAccounts().contains(handle)) { + telecomManager.unregisterPhoneAccount(handle); + } + } else { + telecomManager.unregisterPhoneAccount(handle); + } } public static PhoneAccountHandle getHandle(final Context context, final Account account) { @@ -288,8 +329,13 @@ public class CallIntegrationConnectionService extends ConnectionService { .show(); return; } - service.getSystemService(TelecomManager.class) - .placeCall(CallIntegration.address(with), extras); + try { + service.getSystemService(TelecomManager.class) + .placeCall(CallIntegration.address(with), extras); + } catch (final SecurityException e) { + Toast.makeText(service, R.string.call_integration_not_available, Toast.LENGTH_LONG) + .show(); + } } else { final var connection = createOutgoingRtpConnection(service, account, with, media); if (connection != null) { @@ -319,8 +365,15 @@ public class CallIntegrationConnectionService extends ConnectionService { final var extras = new Bundle(); extras.putString("sid", id.sessionId); bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras); - context.getSystemService(TelecomManager.class) - .addNewIncomingCall(phoneAccountHandle, bundle); + try { + context.getSystemService(TelecomManager.class) + .addNewIncomingCall(phoneAccountHandle, bundle); + } catch (final SecurityException e) { + Log.e( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": call integration not available", + e); + } } public static class ServiceConnectionService { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8e79ef1b1..849c7daa8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -40,8 +40,6 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.security.KeyChain; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; @@ -1284,15 +1282,13 @@ public class XmppConnectionService extends Service { toggleSetProfilePictureActivity(hasEnabledAccounts); reconfigurePushDistributor(); - CallIntegrationConnectionService.registerPhoneAccounts(this, this.accounts); + CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts); restoreFromDatabase(); if (QuickConversationsService.isContactListIntegration(this) - && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || ContextCompat.checkSelfPermission( - this, Manifest.permission.READ_CONTACTS) - == PackageManager.PERMISSION_GRANTED)) { + && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED) { startContactObserver(); } FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); @@ -2465,7 +2461,7 @@ public class XmppConnectionService extends Service { public void createAccount(final Account account) { account.initAccountServices(this); databaseBackend.createAccount(account); - CallIntegrationConnectionService.registerPhoneAccount(this, account); + CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); this.accounts.add(account); this.reconnectAccountInBackground(account); updateAccountUi(); @@ -2589,6 +2585,7 @@ public class XmppConnectionService extends Service { toggleForegroundService(); syncEnabledAccountSetting(); mChannelDiscoveryService.cleanCache(); + CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); return true; } else { return false; From ca1d8b4d1b3f4e77f56fe450f3759d40e9f9091a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 17 Feb 2024 09:05:40 +0100 Subject: [PATCH 161/556] fix race condition accessing rtpSender --- .../eu/siacs/conversations/xmpp/jingle/TrackWrapper.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java index e62aa18fd..16e89ca3b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -65,8 +65,14 @@ class TrackWrapper { public static RtpTransceiver getTransceiver( @Nonnull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { final RtpSender rtpSender = trackWrapper.rtpSender; + final String rtpSenderId; + try { + rtpSenderId = rtpSender.id(); + } catch (final IllegalStateException e) { + return null; + } for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) { - if (transceiver.getSender().id().equals(rtpSender.id())) { + if (transceiver.getSender().id().equals(rtpSenderId)) { return transceiver; } } From 7eaad9842cf025f54e096ed56d9d9f44e8e2a6ea Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 18 Feb 2024 08:50:19 +0100 Subject: [PATCH 162/556] remove mic availability check --- .../services/AppRTCAudioManager.java | 37 +-------------- .../conversations/ui/RtpSessionActivity.java | 45 ++----------------- 2 files changed, 4 insertions(+), 78 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index 894b2ace6..b256d6655 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -95,44 +95,9 @@ public class AppRTCAudioManager { AppRTCUtils.logDeviceInfo(Config.LOGTAG); } - public static boolean isMicrophoneAvailable() { - microphoneLatch = new CountDownLatch(1); - AudioRecord audioRecord = null; - boolean available = true; - try { - final int sampleRate = 44100; - final int channel = AudioFormat.CHANNEL_IN_MONO; - final int format = AudioFormat.ENCODING_PCM_16BIT; - final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format); - audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize); - audioRecord.startRecording(); - final short[] buffer = new short[bufferSize]; - final int audioStatus = audioRecord.read(buffer, 0, bufferSize); - if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED) - available = false; - } catch (Exception e) { - available = false; - } finally { - release(audioRecord); - - } - microphoneLatch.countDown(); - return available; - } - - private static void release(final AudioRecord audioRecord) { - if (audioRecord == null) { - return; - } - try { - audioRecord.release(); - } catch (Exception e) { - //ignore - } - } @SuppressWarnings("deprecation") - public void start(AudioManagerEvents audioManagerEvents) { + public void start(final AudioManagerEvents audioManagerEvents) { Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()"); ThreadUtils.checkIsOnMainThread(); if (amState == AudioManagerState.RUNNING) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 08351b7d5..793ef16f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -6,7 +6,6 @@ import static java.util.Arrays.asList; import android.Manifest; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.PictureInPictureParams; import android.content.ActivityNotFoundException; import android.content.Context; @@ -18,7 +17,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; -import android.os.SystemClock; import android.util.Log; import android.util.Rational; import android.view.KeyEvent; @@ -48,7 +46,6 @@ import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.services.CallIntegrationConnectionService; import eu.siacs.conversations.services.XmppConnectionService; @@ -364,7 +361,7 @@ public class RtpSessionActivity extends XmppActivity final List permissions = permissions(getMedia()); if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { putScreenInCallMode(); - checkRecorderAndAcceptCall(); + acceptCall(); } } @@ -381,8 +378,7 @@ public class RtpSessionActivity extends XmppActivity return permissions.build(); } - private void checkRecorderAndAcceptCall() { - checkMicrophoneAvailabilityAsync(); + private void acceptCall() { try { requireRtpConnection().acceptCall(); } catch (final IllegalStateException e) { @@ -390,40 +386,6 @@ public class RtpSessionActivity extends XmppActivity } } - private void checkMicrophoneAvailabilityAsync() { - new Thread(new MicrophoneAvailabilityCheck(this)).start(); - } - - private static class MicrophoneAvailabilityCheck implements Runnable { - - private final WeakReference activityReference; - - private MicrophoneAvailabilityCheck(final Activity activity) { - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; - } - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> - Toast.makeText( - activity, - R.string.microphone_unavailable, - Toast.LENGTH_LONG) - .show()); - } - } private void putScreenInCallMode() { putScreenInCallMode(requireRtpConnection().getMedia()); @@ -597,7 +559,6 @@ public class RtpSessionActivity extends XmppActivity private void proposeJingleRtpSession( final Account account, final Jid with, final Set media) { - checkMicrophoneAvailabilityAsync(); if (with.isBareJid()) { xmppConnectionService .getJingleConnectionManager() @@ -617,7 +578,7 @@ public class RtpSessionActivity extends XmppActivity PermissionUtils.removeBluetoothConnect(permissions, grantResults); if (PermissionUtils.allGranted(permissionResult.grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { - checkRecorderAndAcceptCall(); + acceptCall(); } else if (requestCode == REQUEST_ACCEPT_CONTENT) { acceptContentAdd(); } else if (requestCode == REQUEST_ADD_CONTENT) { From 3ae561d74aea3f1ef225ca07451818f845009627 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 18 Feb 2024 08:50:35 +0100 Subject: [PATCH 163/556] bump dependencies --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index e3fb55a76..36ed74d55 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.0-rc03' + classpath 'com.android.tools.build:gradle:8.2.2' } } @@ -35,13 +35,13 @@ dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.4.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.4.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' + quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") implementation 'androidx.appcompat:appcompat:1.6.1' @@ -81,7 +81,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation 'com.google.guava:guava:32.1.3-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.17' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.28' implementation 'im.conversations.webrtc:webrtc-android:119.0.0' } From f1abfbdf35f6ea6aa2a70631843d7c6b6d33cbbf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 Feb 2024 09:43:44 +0100 Subject: [PATCH 164/556] work around dead system exception when querying active network --- .../services/XmppConnectionService.java | 12 +++++------- .../conversations/utils/Compatibility.java | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 849c7daa8..f120c4c54 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1054,15 +1054,13 @@ public class XmppConnectionService extends Service { } public boolean isDataSaverDisabled() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final ConnectivityManager connectivityManager = - (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - return !connectivityManager.isActiveNetworkMetered() - || Compatibility.getRestrictBackgroundStatus(connectivityManager) - == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; - } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return true; } + final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class); + return !Compatibility.isActiveNetworkMetered(connectivityManager) + || Compatibility.getRestrictBackgroundStatus(connectivityManager) + == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) { diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 23c935a2b..6d3ba6224 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -43,11 +43,8 @@ public class Compatibility { Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(final Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - || ContextCompat.checkSelfPermission( - context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission( + context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } public static boolean s() { @@ -181,6 +178,17 @@ public class Compatibility { } } + @RequiresApi(api = Build.VERSION_CODES.N) + public static boolean isActiveNetworkMetered( + @NonNull final ConnectivityManager connectivityManager) { + try { + return connectivityManager.isActiveNetworkMetered(); + } catch (final RuntimeException e) { + // when in doubt better assume it's metered + return true; + } + } + public static Bundle pgpStartIntentSenderOptions() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { return ActivityOptions.makeBasic() From 94f3b1fb577daf127db4f22daf9dd21df1aabe30 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 Feb 2024 09:52:43 +0100 Subject: [PATCH 165/556] version bump to 2.14.0-beta + changelog --- CHANGELOG.md | 4 ++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4209504.txt | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4209504.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index e20272a2c..ccd607ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.14.0 + +* Improve integration of A/V calls into the operating system + ### Version 2.13.4 * Fix minor regressions introduced with 2.13.1 diff --git a/build.gradle b/build.gradle index 36ed74d55..96f35d13d 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42094 - versionName "2.13.4" + versionCode 42095 + versionName "2.14.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4209504.txt b/fastlane/metadata/android/en-US/changelogs/4209504.txt new file mode 100644 index 000000000..94c8f6058 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4209504.txt @@ -0,0 +1 @@ +* Improve integration of A/V calls into the operating system From 34b60bba396faf3c58e86f2322f453b124710704 Mon Sep 17 00:00:00 2001 From: ghose Date: Wed, 21 Feb 2024 06:35:53 +0000 Subject: [PATCH 166/556] Translated using Weblate (Galician) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index fac37134b..5e5bc7c07 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -517,11 +517,10 @@ Texto compartido con %s Permitir que %1$s acceda ao almacenaxe externo Permitir que %1$s acceda á cámara - Sincronice con todos os contactos - %1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP. -\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos. + Integración coa lista de contactos + %1$s procesa a túa lista de contactos de xeito local, no teu dispositivo, para mostrar os nomes e fotos de perfil dos contactos con conta XMPP. \n -\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor. +\nNingún dato da túa lista de contactos sae do teu dispositivo! Notificar todas as mensaxes Notificar só cando é mencionada Notificacións desactivadas @@ -1031,5 +1030,5 @@ Benvida a Quicksy! Quicksy solicita permiso para usar os teus datos Política de privacidade - Non está dispoñible a integración coa libreta de enderezos + Non está dispoñible a integración coa lista de contactos \ No newline at end of file From 6e7892415a660bf1ce278c6b964fdc88516ca83e Mon Sep 17 00:00:00 2001 From: Mako Date: Wed, 21 Feb 2024 12:55:33 +0000 Subject: [PATCH 167/556] Translated using Weblate (Japanese) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 107 ++++++++++++++++------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 80bb88581..d25240881 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -7,7 +7,7 @@ 会話を閉じる 連絡先の詳細 グループチャットの詳細 - チャンネルの詳細 + 談話室の詳細 アカウントを追加 名前を編集 アドレス帳に追加 @@ -35,11 +35,11 @@ 送信中… メッセージを復号しています。しばらくお待ちください… OpenPGPで暗号化されたメッセージ - ニックネームは既に使用されています - このニックネームは使えません + ニックネームはすでに使用されています + ニックネームが正しくありません 管理者 所有者 - 調停者 + 司会者 参加者 訪問者 連絡先リストから%sを削除しますか? この連絡先との会話は削除されません。 @@ -160,10 +160,10 @@ サーバーが見つかりません 接続なし 登録に失敗しました - ユーザー名は既に使用されています + ユーザー名はすでに使用されています 登録が完了しました サーバーは登録をサポートしていません - 登録トークンが無効です + 登録トークンが正しくありません TLS ネゴシエーションに失敗しました 検証不可能なドメイン ポリシー違反 @@ -216,12 +216,12 @@ 暗号化されたメッセージです。復号するには OpenKeychain をインストールしてください。 新規の OpenPGP で暗号化されたメッセージが見つかりました OpenPGP 鍵 ID - OMEMO フィンガープリント - v\\OMEMO フィンガープリント - OMEMO フィンガープリント (メッセージ起源) - v\\OMEMO フィンガープリント (メッセージ起源) + OMEMO 指紋 + v\\OMEMO 指紋 + OMEMO 指紋 (メッセージ起源) + v\\OMEMO 指紋 (メッセージ起源) 他のデバイス - OMEMO フィンガープリントを信頼 + OMEMO 指紋を信頼 暗号鍵の取得中… 完了 復号 @@ -233,7 +233,7 @@ 連絡先のブロックを解除 作成 選択 - 連絡先は既に存在します + 連絡先はすでに存在しています 参加 channel@conference.example.com/nick channel@conference.example.com @@ -249,7 +249,7 @@ トピック グループチャットに参加しています… 退出 - 連絡先があなたを連絡先名簿に追加しました + 連絡先があなたを連絡先リストに追加しました 戻りを追加 %s はここまで読みました %s はここまで読みました @@ -267,7 +267,7 @@ %s へ 非公開メッセージを %s へ送信 接続 - このアカウントは既に存在します + このアカウントはすでに存在しています 次へ セッションが確立 スキップ @@ -292,7 +292,7 @@ 消音時間の間、通知は無音になります その他 ブックマーク同期 - OMEMO フィンガープリントをクリップボードにコピーしました + OMEMO 指紋をクリップボードにコピーしました このグループチャットへの参加はブロックされています このグループチャットはメンバー制です リソース制限 @@ -316,8 +316,8 @@ XMPP アドレスをクリップボードにコピーしました エラーメッセージをクリップボードにコピーしました ウェブアドレス - 二次元バーコードをスキャン - 二次元バーコードを表示 + QR コードをスキャン + QR コードを表示 ブロック一覧を表示 アカウントの詳細 確認 @@ -354,7 +354,7 @@ グループチャットのサーバーが見つかりませんでした グループチャットを作成できません アカウントのアバター - クリップボードに OMEMO フィンガープリントをコピー + クリップボードに OMEMO 指紋をコピー OMEMO 鍵を再生成 デバイスを消去 OMEMO の告知から他のすべてのデバイスを消去してもよろしいですか? お使いのデバイスが次回接続したとき、それらのデバイスは自分自身を再告知しますが、その間に送信されたメッセージを受信できない場合があります。 @@ -396,7 +396,7 @@ 公開談話室の環境設定 非公開、メンバーのみ XMPPアドレスを誰でも見れるようにする - 談話室の調停をする + 談話室を司会ありにする あなたは参加していません グループチャットの設定が変更されました! グループチャットの設定を変更できませんでした @@ -509,11 +509,10 @@ %s でテキスト共有 %1$s に外部ストレージへのアクセス権を付与してください %1$s にカメラへのアクセス権を付与 - 連絡先と同期 - %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。 -\nこれにより、連絡先のフルネームとアバターが表示されます。 + 連絡先リストの統合 + %1$s は、デバイス上で、連絡先リストをローカルに処理し、XMPP 上の一致する連絡先の名前とプロフィール写真を表示します。 \n -\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。 +\n連絡先リストのデータがデバイスから流出することはありません。 すべてのメッセージで通知 メンションされたときにのみ通知 通知は無効 @@ -531,13 +530,14 @@ このフィールドは必須項目です メッセージを修正 修正したメッセージを送信 - あなたは信頼を確認するために、この人の指紋を安全に検証しました。“完了”を選択すると、 %s がこのグループチャットの一員であることを確認したことになります。 + すでにこの人の指紋を信頼しています。“完了”を選択すると、 %s がこのグループチャットの一員であることを確認したことになります。 このアカウントを無効化しました セキュリティエラー: 不正なファイルアクセス! URI を共有するアプリが見つかりません …で URI を共有 同意して続行 - conversations.im 上にアカウントを作成する設定の指南です。¹\nconversations.im をプロバイダーとして選択した場合、あなたの完全な XMPP アドレスを他のプロバイダーのユーザーに示すことで、その人と連絡をとることができます。 + conversations.im 上にアカウントを作成する設定の指南です。 +\nconversations.im をプロバイダーとして選択した場合、あなたの完全な XMPP アドレスを他のプロバイダーのユーザーに示すことで、その人と連絡をとることができます。 あなたの完全なXMPPアドレスは: %s アカウントを作成 自分のプロバイダーを使用 @@ -589,7 +589,7 @@ アカウントを更新できません この XMPP アドレスをスパムとして報告する。 OMEMO ID を削除 - OMEMO 鍵を再生成します。すべての連絡先を再度確認する必要があります。使用するのは最後の手段のみとしてください。 + OMEMO 鍵を再生成します。すべての連絡先を再度検証する必要があります。使用するのは最後の手段のみとしてください。 選択した鍵を削除 アバターを公開するには接続する必要があります。 エラーメッセージを表示 @@ -599,20 +599,20 @@ お使いのデバイスは、%1$s のデータセーバーを無効にできません。 一時ファイルを作成できません このデバイスは検証済です - フィンガープリントをコピー - 所有するすべての OMEMO 鍵を検証完了 - バーコードに、この会話のフィンガープリントが含まれていません。 - フィンガープリントを検証しました + 指紋をコピー + 所有するすべての OMEMO 鍵を検証しました + バーコードに、この会話の指紋が含まれていません。 + 指紋を検証しました カメラを使用して連絡先のバーコードをスキャンします 鍵が取得されるのをお待ちください バーコードで共有 XMPP URI で共有 HTTP リンクで共有 - 認証前で鍵を使用 - 認証されていない連絡先からの新規デバイスを信頼するが、認証されている連絡先からの新規デバイスについては手動での確認を求める。 - 認証せずOMEMO 鍵を信用しています。このままでは盗聴される危険性があります。 + 検証前に無条件に信頼 + 検証されていない連絡先からの新しいデバイスは信頼しますが、検証済みの連絡先については新しいデバイスを手動で確認するよう求めます。 + 無条件に信頼されている OMEMO 鍵です。他の誰かである可能性や、誰かに盗聴される危険性があります。 信頼されていない - 不正な二次元バーコード + QR コードが正しくありません キャッシュフォルダを消去します (カメラアプリで使用) キャッシュを消去 プライベートストレージを消去 @@ -625,7 +625,8 @@ 非アクティブを表示 非アクティブを非表示 信頼できないデバイス - このデバイスの検証を削除してよろしいですか?\nこのデバイスとそのデバイスからのメッセージは、“信頼できない”とマークされます。 + このデバイスの検証を削除してよろしいですか? +\nこのデバイスとそのデバイスからのメッセージは、“信頼されていない”とマークされます。 %d秒 @@ -644,7 +645,7 @@ %dか月 - 自動でメッセージを削除 + メッセージの自動削除 設定された期間よりも古いメッセージを、このデバイスから自動的に削除します。 メッセージの暗号化中 ローカル保存期間のためにメッセージを取得しません。 @@ -694,7 +695,7 @@ %1$s は %2$s に暗号化メッセージを送れません。連絡先が利用しているサーバーが古すぎるか、クライアントが OMEMO を扱えません。 デバイスの一覧を取得できません 暗号化の鍵を取得できません - ヒント: お互いが連絡先名簿に加えれば解決するでしょう。 + ヒント: お互いが連絡先リストに加えればこの問題は解決するでしょう。 この会話で OMEMO の暗号化を無効化してもよろしいですか?\nこれにより、サーバー管理者がメッセージを読むことが可能になりますが、時代遅れのクライアントを使っている人と連絡をとるには、この方法しかないかもしれません。 今すぐ無効化 下書き: @@ -771,14 +772,14 @@ 中 (360p) 高 (720p) 中止しました - あなたは既にメッセージを作成中です。 + あなたはすでにメッセージを作成中です。 実装されてない機能 - 不正な国コード + 国コードが正しくありません 国を選択 電話番号 電話番号を検証 - Quicksy から電話番号を確認するための SMS メッセージ(キャリア料金がかかる場合があります)が送信されます。国番号と電話番号を入力してください: -
%s

これでよろしいでしょうか、それとも番号を編集しますか?]]>
+ Quicksy から電話番号を検証するための SMS メッセージ(キャリア料金がかかる場合があります)が送信されます。国番号と電話番号を入力してください: + 電話番号の検証を行います

%s

これでよろしいでしょうか、それとも番号を編集しますか?
%s は有効な電話番号ではありません。 電話番号を入力してください。 国を検索 @@ -805,7 +806,7 @@ 安全な接続を確立できませんでした。 サーバーが見つかりません。 要求の処理中に、何か問題が発生しました。 - 無効なユーザーの入力 + ユーザーの入力が正しくありません 一時的に入手不可能です。後でもう一度お試しください。 ネットワーク接続なし。 %s でもう一度お試しください。 @@ -846,9 +847,9 @@ XMPP アドレスをご記入ください これは XMPP アドレスです。名前をご記入ください。 公開談話室を作成中… - この談話室は既に存在します + この談話室はすでに存在しています 存在している談話室に参加しています - 談話室の環境設定を保存できません + 談話室の環境設定を保存できませんでした 誰にでもトピックの編集を許可 誰にでも他の人の招待を許可 誰でもトピックを編集できます。 @@ -864,7 +865,7 @@ 参加者を検索 ファイルが大きすぎます 添付 - 談話室を発見 + 談話室を探索 談話室を検索 プライバシー侵害の可能性あり! 既にアカウントを持っています @@ -908,7 +909,7 @@ 接続切断 撤回された通話 アプリの失敗 - 検証に問題 + 検証の問題 通話を切る 継続中の通話 継続中のビデオ通話 @@ -951,7 +952,7 @@ 音声メールを録音 音声を再生 音声を一時停止 - 連絡先を追加、作成またはグループチャットに参加、またはチャンネルを発見する + 連絡先を追加、作成またはグループチャットに参加、または談話室を探索する %1$d人の参加者を表示 @@ -986,7 +987,7 @@ ログアウトしました 発信通話 · %s オーディオブック - 談話室の発見は<a href=https://search.jabber.network>search.jabber.network</a>というサービスを利用します.<br><br>利用するとIPアドレスと検索語はそのサービスに送信されます。詳細についてはそのサービスの<a href=https://search.jabber.network/privacy>個人情報保護方針</a>を参照してください。 + 談話室の探索は<a href=https://search.jabber.network>search.jabber.network</a>というサービスを利用します.<br><br>利用するとIPアドレスと検索語はそのサービスに送信されます。詳細についてはそのサービスの<a href=https://search.jabber.network/privacy>個人情報保護方針</a>を参照してください。 自分で保存したバックアップしか復元しないでください! XMPP経由でPushメッセージを端末に転送するユーザー指定のPushサーバー。 ログイン @@ -1002,4 +1003,14 @@ Pushメッセージを受信する際に経由するアカウント。 サーバーからアカウントを削除できませんでした 概要 + トラックを追加しますか ? + 他のホストで再接続 + 連絡先リストの統合は利用できません + 個人情報保護方針 + スパムを報告し、スパマーをブロック + MUC に出入りするときに「自動参加」フラグを立て、他のクライアントによる変更に反応します。 + Quicksy はあなたのデータを使用することについて同意を求めています + Quicksy へようこそ ! + 連絡先は未検証のデバイスを使用しています。 QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 + 未検証のデバイスを使用しています。他のデバイスで QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 \ No newline at end of file From aad750f2c87e8047256997e6ce63de30aa81e3e5 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 20 Feb 2024 21:51:16 +0000 Subject: [PATCH 168/556] Translated using Weblate (Albanian) Currently translated at 98.5% (968 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 62 ++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index c86c426b6..968d845e9 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -254,8 +254,8 @@ Adresa XMPP u kopjua në clipboard Mesazhi i gabimit u kopjua në të papastër adresë web - Skano Kod me vija 2D - Shfaq Kod me vija 2D + Skano Kod QR + Shfaq Kod QR Shfaqe listë bllokimesh Hollësi llogarie Ripohojeni @@ -403,7 +403,7 @@ Portë Ky s’është numër i vlefshëm porte Ky s’është një strehëemër i vlefshëm - Njëkohësoje me kontaktet + Integrim liste kontaktesh Njofto për krejt mesazhet Njoftomë vetëm kur përmendem Njoftime të çaktivizuara @@ -480,7 +480,7 @@ Ndajeni me të tjerë si lidhje HTTP Besim i Verbër Para Verifikimi Jo i besuar - Kod 2D me vija i pavlefshëm + Kod QR i pavlefshëm Spastro fshehtinën Spastro depozitë private Vazhdoni @@ -830,7 +830,7 @@ Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. \n \nTani do t’ju kërkohet t’i çaktivizoni ato. - I keni vlerësuar në mënyrë të parrezik shenjat e gishtave të këtij personi, për të ripohuar besimin. Duke përzgjedhur “U bë”, thjesht po ripohoni se %s është pjesë e kësaj fjalosjeje në grup. + I keni besuar tashmë shenjat e gishtave të këtij personi. Duke përzgjedhur “U bë”, thjesht po ripohoni se %s është pjesë e kësaj fjalosjeje në grup. Ju ndan një hap nga verifikimi i kyçeve OMEMO të llogarisë tuaj. Kjo është e sigurt vetëm nëse e ndoqët këtë lidhje prej një burimi të besuar, ku vetëm ju do të mund ta kishit publikuar këtë lidhje. Jeni i sigurt se doni të hiqet verifikimi i kësaj pajisjeje\? \nKësaj pajisjeje dhe mesazheve prej saj do t’u vihet shenjë si “Jo i besuar”. @@ -869,7 +869,7 @@ Aplikacioni që përdorët për të dhënë këtë kartelë, nuk jep leje të mjaftueshme. Jeni i sigurt se doni të hiqet kyçi juaj publik OpenPGP nga njoftimi juaj për prani\? \nKontaktet tuaj s’do të jenë më në gjendje t’ju dërgojnë mesazhe të fshehtëzuar me OpenPGP. - Fshirja e llogarisë tuaj fshin krejt historikun e bisedave tuaja + Jeni i sigurt se doni të fshihet llogaria juaj? Fshirja e saj shkakton fshirjen e krejt historikut të bisedave tuaja Mesazh i fshehtëzuar. Që ta shfshehtëzoni, ju lutemi, instaloni OpenKeychain. Shenja gishtash OMEMO (origjinë mesazhi) Shenja gishtash v\\OMEMO (origjinë mesazhi) @@ -925,10 +925,9 @@ Figurat iu dhanë %s Teksti iu dha %s Akordoji %1$s hyrje te depozitë e jashtme - %1$s dëshiron leje të përdorë librin tuaj të adresave, për përkim me listën tuaj të kontakteve XMPP. -\nKjo do të sjellë shfaqjen e emrave të plotë dhe avatarëve të kontakteve tuaj. + %1$s e përpunon lokalisht listën tuaj të kontakteve, në pajisjen tuaj, për t’ju shfaqur emrat dhe foto profili për kontakte me përkim në XMPP. \n -\n%1$s vetëm sa do të lexojë librin tuaj të adresave dhe bëjë lokalisht përkimin, pa ngarkuar gjë në shërbyesin tuaj. +\nNga pajisja juaj s’del kurrë ndonjë e dhënë liste kontaktesh! Ndihmëz: Përdorni “Zgjidhni kartelë”, në vend se “Zgjidhni foto”, për të dërguar figura të pangjeshura, pavarësisht nga ky rregullim. Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. \nRekomandohet të çaktivizohen ato. @@ -993,4 +992,49 @@ %1$d thirrje të humbur prej %2$d kontakti %1$d thirrje të humbur prej %2$d kontaktesh + Mos u rrekni të riktheni kopjeruajtje që s’i keni krijuar ju vetë! + Të shtohen pjesë shtesë? + XEP-0280: Message Carbons + Rilidhu te një tjetër strehë + Ju është kufizuar shpejtësia + jabber.network + kërkoni në fjalosje grupesh + Fjalosje në grup + Ruaje si fjalosjeje grupi + Po përdorni pajisje të paverifikuara. Që të kryhet verifikimi dhe të pengohen sulme MITM, skanoni Kodin QR në pajisjet tuaja të tjera. + Listë bllokimesh + I dalë + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0215: External Service Discovery + XEP-0357: Push + Njoftime për mungesë kyçi publik + Publikoje + Citim + Ç’jepet + Audiolibër + Pasje + Shërbyes A- ose adresë Onion + Quicksy kërkon pranimin tuaj që të përdorë të dhëna tuajat + Keni dalë nga kjo llogari + Mirë se vini te Quicksy! + Caktoni gjendjen tuaj, kur përpunoni mesazhin për të. + Kyçe OMEMO të besuar verbërisht, që do të thotë mund të jetë dikush tjetër, ose dikush ka arritur të përgjojë. + S’ka aplikacion tregu të instaluar. + Po rrekeni të importoni një format të vjetruar kartelash kopjeruajtje + Thirrje ardhëse (%s) · %s + Thirrje për (%s) · %s + Thirrje për · %s + Hidhe poshtë + Hiqe llogarinë nga shërbyesi + S’u fshi dot llogari nga shërbyesi + Fshihe njoftimin + Dilni + Hyni + Raportoni mesazh të padëshiruar + Raportoni mesazh të padëshiruar dhe bllokoni dërguesin + Rregulla privatësie + Integrimi i listës së kontakteve s’është i mundshmë + Kontakti juaj përdor pajisje të paverifikuara. Që të kryhet verifikimi dhe të pengohen sulme MITM, skanoni Kodin e tij QR. \ No newline at end of file From b8a56c4d618be7be59a88c1263af72ef2e888168 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 21 Feb 2024 11:02:05 +0000 Subject: [PATCH 169/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 216c355d8..98d14d533 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -529,11 +529,10 @@ 文本已分享给 %s 授予 %1$s 访问外部存储的权限 授予 %1$s 访问相机的权限 - 与联系人同步 - %1$s 想要访问通讯录权限来将它与 XMPP 联系人列表相匹配。 -\n将会显示联系人的全名和头像。 + 联系人列表集成 + %1$s 在您的设备上本地处理您的联系人列表,以向您显示 XMPP 上匹配联系人的名称和个人资料图片。 \n -\n%1$s 将只会读取通讯录并在本地匹配,不会上传任何内容到服务器。 +\n任何联系人列表数据都不会离开您的设备! 通知所有消息 仅在提及时通知 通知已禁用 @@ -1037,5 +1036,5 @@ 欢迎使用 Quicksy! Quicksy 请求您同意使用您的数据 隐私政策 - 通讯录集成不可用 + 联系人列表集成不可用 \ No newline at end of file From 6c19c46742283d2b6f567a69f656c6d04fb7f4ad Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 21 Feb 2024 11:03:38 +0000 Subject: [PATCH 170/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (57 of 57 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4209404.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4209404.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4209404.txt b/fastlane/metadata/android/zh-CN/changelogs/4209404.txt new file mode 100644 index 000000000..e89631ebe --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4209404.txt @@ -0,0 +1 @@ +* 修复 2.13.1 中出现的小问题 From 676acce1e299b7a83c23c5877ddebf84b9fdc5fa Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 20 Feb 2024 21:08:27 +0000 Subject: [PATCH 171/556] Translated using Weblate (Albanian) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-quicksy/sq/ --- .../metadata/android/sq/full_description.txt | 14 ++++++++++++++ .../metadata/android/sq/short_description.txt | 1 + 2 files changed, 15 insertions(+) create mode 100644 src/quicksy/fastlane/metadata/android/sq/full_description.txt create mode 100644 src/quicksy/fastlane/metadata/android/sq/short_description.txt diff --git a/src/quicksy/fastlane/metadata/android/sq/full_description.txt b/src/quicksy/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 000000000..5624b1bd1 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,14 @@ +Quicksy është një program me fillesë klientin popullor Jabber/XMPP, me zbulim të automatizuar kontaktesh. + +Regjistroheni me numrin tuaj të telefonit dhe Quicksy—baszuar në numrat e telefonave në librin tuaj të adresave—do t’ju sugjerojë automatikisht kontakte të mundshëm. + +Nën kapak, Quicksy është një klient i plotë Jabber, që ju lejon të komunikoni me cilindo përdorues në cilindo shërbyes federimi publik. Në mënyrë të ngjashme, me përdoruesit në Quicksy mund të lidhesh që nga jashtë thjesht duke shtuar +phonenumber@quicksy.im te lista juaj e kontakteve. + +Hiq njëkohësimin e kontakteve, ndërfaqja e përdoruesit është lënë qëllimisht sa më afër Conversations që të jetë e mundur. Kjo u lejon përdoruesve që të migrojnë, nëse duan, nga Quicksy në Conversations pa u dashur të rimësojnë se si funksionon aplikacioni. + +Kontaktet e sugjeruara përbëhen nga përdorues të tjerë të Quicksy-it dhe përdorues Jabber-i/XMPP-je të zakonshëm që kanë dhënë ID-në e tyre Jabber te Lista Quicksy (https://quicksy.im/#get-listed). + +SHËNIM: Që të jepni (https://quicksy.im/enter/) ID-në tuaj Jabber te Lista +Quicksy, lypset një tarifë regjistrimi e aplikueshme një herë vetëm. + +Për më tepër hollësi, lexoni Rregulla Privatësie (https://quicksy.im/#privacy). diff --git a/src/quicksy/fastlane/metadata/android/sq/short_description.txt b/src/quicksy/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 000000000..05a7f2969 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Jabber/XMPP me Hyrje të Lehtë dhe Zbulim të Lehtë From f3390a54bc56b6970145769a5bc8b2a67be9b9f5 Mon Sep 17 00:00:00 2001 From: Application-Maker Date: Thu, 22 Feb 2024 07:55:20 +0000 Subject: [PATCH 172/556] Translated using Weblate (Russian) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index eb43f1142..fe5e5bc2d 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -130,7 +130,7 @@ Время, на которое уведомления будут отключены, когда вы пользуетесь аккаунтом на другом устройстве. Дополнительно Не отправлять отчёты об ошибках - Отправляя отчёты об ошибках, вы помогаете разработке Quicksy + Отправляя отчёты об ошибках, вы помогаете разработке Отчёты о получении Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения Запретить скриншоты @@ -513,11 +513,10 @@ Текст отправлен %s Предоставить %1$s разрешение на использование внешнего накопителя Предоставить %1$s разрешение на использование камеры - Синхронизировать с контактами - %1$s нужно разрешение на доступ к контактам, чтобы соотнести их с вашими XMPP-контактами. -\nЭто позволит отобразить полные имена и аватары контактов. + Интеграция списка контактов + %1$s обрабатывает ваш список контактов локально, на вашем устройстве, чтобы показать вам имена и изображения профиля для сопоставления контактов по XMPP. \n -\n%1$s сделает это локально, без отправки чего-либо на ваш сервер. +\nНикакие данные списка контактов никогда не покидают ваше устройство! Все сообщения Уведомлять только при упоминании Уведомления выключены @@ -993,7 +992,7 @@ Несовместимый клиент Групповые беседы - медиафайл + файл мультимедиа Продолжить Сервер уведомлений Распределитель UnifiedPush @@ -1028,7 +1027,7 @@ Выбираемый пользователем сервер для перенаправления уведомлений на Ваше устройство. Обзор каналов использует сторонний сервис <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Эта функция передаст Ваш IP-адрес и ваш поисковый запрос этому сервису. Ознакомьтесь с их <a href=https://search.jabber.network/privacy>Политикой приватности</a> для получения подробностей. Нет (неактивно) - Вы подтверждаете ваши собственные OMEMO-ключи. Это безопасно только если вы перешли по ссылке из доверенного источника, где только вы могли разместить эту ссылку. + Вы собираетесь проверить ключи OMEMO своей учетной записи. Это безопасно только в том случае, если вы перешли по этой ссылке из надежного источника, где только вы могли опубликовать эту ссылку. Не пытайтесь восстановить резервные копии, которые не были созданы вами! %1$d пропущенный вызов от %2$s @@ -1050,8 +1049,12 @@ Скрыть уведомление Ваш контакт использует неподтверждённые устройства. Отсканируйте его QR-код для проверки и предотвращения атаки посредника. Выйти - Деавторизован + Вышел из системы Вы используете неподтверждённые устройства. Отсканируйте QR-код на подтверждённом устройстве для проверки и предотвращения атаки посредника. Пожаловаться на спам и заблокировать Пожаловаться на спам + Политика конфиденциальности + Интеграция списка контактов недоступна + Quicksy запрашивает ваше согласие на использование ваших данных + Добро пожаловать в Quicksy! \ No newline at end of file From d38c8b2e9d38590ab8b56f1b599430754824708c Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Thu, 22 Feb 2024 11:31:15 +0000 Subject: [PATCH 173/556] Translated using Weblate (Romanian) Currently translated at 100.0% (982 of 982 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 8d06a15bc..ae91b368a 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -520,11 +520,10 @@ Text partajat cu %s Permiteți %1$s acces la stocarea externă Permiteți %1$s acces la camera foto - Sincronizează cu contactele - %1$s dorește permisiunea de a vă accesa contactele pentru a putea potrivi lista de contacte XMPP cu cea din dispozitiv. -\nAșa v-a afișa numele lor complete și avatarele. + Integrarea listei de contacte + %1$s procesează lista de contacte la nivel local, pe dispozitiv, pentru a vă arăta numele și pozele de profil ale contactelor pe care le aveți și pe XMPP. \n -\n%1$s va citi și potrivi local fără a fi încărcate pe serverul dumneavoastră. +\nDatele din lista de contacte nu părăsesc niciodată dispozitivul dumneavoastră! Notifică la toate mesajele Notifică doar atunci când cineva vă menționează numele Notificări dezactivate @@ -1046,6 +1045,6 @@ Raportează spam Bine ați venit la Quicksy! Quicksy vă solicită consimțământul pentru a utiliza datele dumneavoastră - Integrarea agendei nu este disponibilă + Integrarea listei de contacte nu este disponibilă Politica de confidențialitate \ No newline at end of file From b381f125de1711745d1217c856d82c9180e46d33 Mon Sep 17 00:00:00 2001 From: ghose Date: Thu, 22 Feb 2024 05:01:10 +0000 Subject: [PATCH 174/556] Translated using Weblate (Galician) Currently translated at 50.8% (29 of 57 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4209404.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4209404.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209404.txt b/fastlane/metadata/android/gl-ES/changelogs/4209404.txt new file mode 100644 index 000000000..fc1d02e69 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4209404.txt @@ -0,0 +1 @@ +* Arranxo de regresións menores introducidas en 2.13.1 From 93867694096ab5d4f66740a6cffab11ac13efd7e Mon Sep 17 00:00:00 2001 From: acioustick Date: Thu, 22 Feb 2024 16:03:27 +0000 Subject: [PATCH 175/556] Translated using Weblate (Japanese) Currently translated at 50.0% (1 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/ja/ --- .../fastlane/metadata/android/ja-JP/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/conversations/fastlane/metadata/android/ja-JP/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt b/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt new file mode 100644 index 000000000..ade292722 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt @@ -0,0 +1 @@ +携帯端末で簡単に操作できるXMPP暗号化インスタント・メッセンジャー From bd2b9b414e1f4ca83fbc7fe55aa3527d09018226 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 23 Feb 2024 17:58:33 +0100 Subject: [PATCH 176/556] do not enforce main thread for getting audio devices fixes #206 --- .../services/AppRTCAudioManager.java | 341 ++++++++---------- 1 file changed, 156 insertions(+), 185 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index b256d6655..2cd3ac346 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -15,42 +15,31 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioDeviceInfo; -import android.media.AudioFormat; import android.media.AudioManager; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import android.os.Build; import android.util.Log; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import org.webrtc.ThreadUtils; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.CountDownLatch; +import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; -import eu.siacs.conversations.xmpp.jingle.Media; -/** - * AppRTCAudioManager manages all audio related parts of the AppRTC demo. - */ +import org.webrtc.ThreadUtils; + +import java.util.HashSet; +import java.util.Set; + +/** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */ public class AppRTCAudioManager { - private static CountDownLatch microphoneLatch; - private final Context apprtcContext; // Contains speakerphone setting: auto, true or false // Handles all tasks related to Bluetooth headset devices. private final AppRTCBluetoothManager bluetoothManager; - @Nullable - private final AudioManager audioManager; - @Nullable - private AudioManagerEvents audioManagerEvents; + @Nullable private final AudioManager audioManager; + @Nullable private AudioManagerEvents audioManagerEvents; private AudioManagerState amState; private boolean savedIsSpeakerPhoneOn; private boolean savedIsMicrophoneMute; @@ -76,8 +65,7 @@ public class AppRTCAudioManager { // Broadcast receiver for wired headset intent broadcasts. private final BroadcastReceiver wiredHeadsetReceiver; // Callback method for changes in audio focus. - @Nullable - private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; + @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; public AppRTCAudioManager(final Context context) { apprtcContext = context; @@ -95,7 +83,6 @@ public class AppRTCAudioManager { AppRTCUtils.logDeviceInfo(Config.LOGTAG); } - @SuppressWarnings("deprecation") public void start(final AudioManagerEvents audioManagerEvents) { Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()"); @@ -104,7 +91,6 @@ public class AppRTCAudioManager { Log.e(Config.LOGTAG, "AudioManager is already active"); return; } - awaitMicrophoneLatch(); this.audioManagerEvents = audioManagerEvents; amState = AudioManagerState.RUNNING; // Store current audio state so we can restore it when stop() is called. @@ -112,48 +98,45 @@ public class AppRTCAudioManager { savedIsMicrophoneMute = audioManager.isMicrophoneMute(); hasWiredHeadset = hasWiredHeadset(); // Create an AudioManager.OnAudioFocusChangeListener instance. - audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - // Called on the listener to notify if the audio focus for this listener has been changed. - // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, - // and whether that loss is transient, or whether the new focus holder will hold it for an - // unknown amount of time. - // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains - // logging for now. - @Override - public void onAudioFocusChange(int focusChange) { - final String typeOfChange; - switch (focusChange) { - case AudioManager.AUDIOFOCUS_GAIN: - typeOfChange = "AUDIOFOCUS_GAIN"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; - break; - case AudioManager.AUDIOFOCUS_LOSS: - typeOfChange = "AUDIOFOCUS_LOSS"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; - break; - default: - typeOfChange = "AUDIOFOCUS_INVALID"; - break; - } - Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange); - } - }; + audioFocusChangeListener = + new AudioManager.OnAudioFocusChangeListener() { + // Called on the listener to notify if the audio focus for this listener has + // been changed. + // The |focusChange| value indicates whether the focus was gained, whether the + // focus was lost, + // and whether that loss is transient, or whether the new focus holder will hold + // it for an + // unknown amount of time. + // TODO(henrika): possibly extend support of handling audio-focus changes. Only + // contains + // logging for now. + @Override + public void onAudioFocusChange(final int focusChange) { + final String typeOfChange = + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN -> "AUDIOFOCUS_GAIN"; + case AudioManager + .AUDIOFOCUS_GAIN_TRANSIENT -> "AUDIOFOCUS_GAIN_TRANSIENT"; + case AudioManager + .AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; + case AudioManager + .AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; + case AudioManager.AUDIOFOCUS_LOSS -> "AUDIOFOCUS_LOSS"; + case AudioManager + .AUDIOFOCUS_LOSS_TRANSIENT -> "AUDIOFOCUS_LOSS_TRANSIENT"; + case AudioManager + .AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; + default -> "AUDIOFOCUS_INVALID"; + }; + Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange); + } + }; // Request audio playout focus (without ducking) and install listener for changes in focus. - int result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + int result = + audioManager.requestAudioFocus( + audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams"); } else { @@ -182,21 +165,9 @@ public class AppRTCAudioManager { Log.d(Config.LOGTAG, "AudioManager started"); } - private void awaitMicrophoneLatch() { - final CountDownLatch latch = microphoneLatch; - if (latch == null) { - return; - } - try { - latch.await(); - } catch (InterruptedException e) { - //ignore - } - } - @SuppressWarnings("deprecation") public void stop() { - Log.d(Config.LOGTAG,"appRtpAudioManager.stop()"); + Log.d(Config.LOGTAG, "appRtpAudioManager.stop()"); Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()"); ThreadUtils.checkIsOnMainThread(); if (amState != AudioManagerState.RUNNING) { @@ -214,60 +185,44 @@ public class AppRTCAudioManager { audioManager.abandonAudioFocus(audioFocusChangeListener); audioFocusChangeListener = null; audioManagerEvents = null; - Log.d(Config.LOGTAG,"appRtpAudioManager.stopped()"); + Log.d(Config.LOGTAG, "appRtpAudioManager.stopped()"); } - /** - * Changes selection of the currently active audio device. - */ - private void setAudioDeviceInternal(CallIntegration.AudioDevice device) { + /** Changes selection of the currently active audio device. */ + private void setAudioDeviceInternal(final CallIntegration.AudioDevice device) { Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); AppRTCUtils.assertIsTrue(audioDevices.contains(device)); switch (device) { - case SPEAKER_PHONE: - setSpeakerphoneOn(true); - break; - case EARPIECE: - case WIRED_HEADSET: - case BLUETOOTH: - setSpeakerphoneOn(false); - break; - default: - Log.e(Config.LOGTAG, "Invalid audio device selection"); - break; + case SPEAKER_PHONE -> setSpeakerphoneOn(true); + case EARPIECE, WIRED_HEADSET, BLUETOOTH -> setSpeakerphoneOn(false); + default -> Log.e(Config.LOGTAG, "Invalid audio device selection"); } selectedAudioDevice = device; } /** - * Changes default audio device. - * TODO(henrika): add usage of this method in the AppRTCMobile client. + * Changes default audio device. TODO(henrika): add usage of this method in the AppRTCMobile + * client. */ - public void setDefaultAudioDevice(CallIntegration.AudioDevice defaultDevice) { + public void setDefaultAudioDevice(final CallIntegration.AudioDevice defaultDevice) { ThreadUtils.checkIsOnMainThread(); switch (defaultDevice) { - case SPEAKER_PHONE: - defaultAudioDevice = defaultDevice; - break; - case EARPIECE: + case SPEAKER_PHONE -> defaultAudioDevice = defaultDevice; + case EARPIECE -> { if (hasEarpiece()) { defaultAudioDevice = defaultDevice; } else { defaultAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; } - break; - default: - Log.e(Config.LOGTAG, "Invalid default audio device selection"); - break; + } + default -> Log.e(Config.LOGTAG, "Invalid default audio device selection"); } Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); updateAudioDeviceState(); } - /** - * Changes selection of the currently active audio device. - */ - public void selectAudioDevice(CallIntegration.AudioDevice device) { + /** Changes selection of the currently active audio device. */ + public void selectAudioDevice(final CallIntegration.AudioDevice device) { ThreadUtils.checkIsOnMainThread(); if (!audioDevices.contains(device)) { Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices); @@ -276,38 +231,27 @@ public class AppRTCAudioManager { updateAudioDeviceState(); } - /** - * Returns current set of available/selectable audio devices. - */ + /** Returns current set of available/selectable audio devices. */ public Set getAudioDevices() { - ThreadUtils.checkIsOnMainThread(); - return Collections.unmodifiableSet(new HashSet<>(audioDevices)); + return ImmutableSet.copyOf(audioDevices); } - /** - * Returns the currently selected audio device. - */ + /** Returns the currently selected audio device. */ public CallIntegration.AudioDevice getSelectedAudioDevice() { return selectedAudioDevice; } - /** - * Helper method for receiver registration. - */ + /** Helper method for receiver registration. */ private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { apprtcContext.registerReceiver(receiver, filter); } - /** - * Helper method for unregistration of an existing receiver. - */ + /** Helper method for unregistration of an existing receiver. */ private void unregisterReceiver(BroadcastReceiver receiver) { apprtcContext.unregisterReceiver(receiver); } - /** - * Sets the speaker phone mode. - */ + /** Sets the speaker phone mode. */ private void setSpeakerphoneOn(boolean on) { boolean wasOn = audioManager.isSpeakerphoneOn(); if (wasOn == on) { @@ -316,9 +260,7 @@ public class AppRTCAudioManager { audioManager.setSpeakerphoneOn(on); } - /** - * Sets the microphone mute state. - */ + /** Sets the microphone mute state. */ private void setMicrophoneMute(boolean on) { boolean wasMuted = audioManager.isMicrophoneMute(); if (wasMuted == on) { @@ -327,53 +269,57 @@ public class AppRTCAudioManager { audioManager.setMicrophoneMute(on); } - /** - * Gets the current earpiece state. - */ + /** Gets the current earpiece state. */ private boolean hasEarpiece() { return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); } /** - * Checks whether a wired headset is connected or not. - * This is not a valid indication that audio playback is actually over - * the wired headset as audio routing depends on other conditions. We - * only use it as an early indicator (during initialization) of an attached - * wired headset. + * Checks whether a wired headset is connected or not. This is not a valid indication that audio + * playback is actually over the wired headset as audio routing depends on other conditions. We + * only use it as an early indicator (during initialization) of an attached wired headset. */ @Deprecated private boolean hasWiredHeadset() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return audioManager.isWiredHeadsetOn(); - } else { - final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); - for (AudioDeviceInfo device : devices) { - final int type = device.getType(); - if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { - Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset"); - return true; - } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { - Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device"); - return true; - } + final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo device : devices) { + final int type = device.getType(); + if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { + Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset"); + return true; + } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { + Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device"); + return true; } - return false; } + return false; } /** - * Updates list of possible audio devices and make new device selection. - * TODO(henrika): add unit test to verify all state transitions. + * Updates list of possible audio devices and make new device selection. TODO(henrika): add unit + * test to verify all state transitions. */ public void updateAudioDeviceState() { ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "--- updateAudioDeviceState: " - + "wired headset=" + hasWiredHeadset + ", " - + "BT state=" + bluetoothManager.getState()); - Log.d(Config.LOGTAG, "Device status: " - + "available=" + audioDevices + ", " - + "selected=" + selectedAudioDevice + ", " - + "user selected=" + userSelectedAudioDevice); + Log.d( + Config.LOGTAG, + "--- updateAudioDeviceState: " + + "wired headset=" + + hasWiredHeadset + + ", " + + "BT state=" + + bluetoothManager.getState()); + Log.d( + Config.LOGTAG, + "Device status: " + + "available=" + + audioDevices + + ", " + + "selected=" + + selectedAudioDevice + + ", " + + "user selected=" + + userSelectedAudioDevice); // Check if any Bluetooth headset is connected. The internal BT state will // change accordingly. // TODO(henrika): perhaps wrap required state into BT manager. @@ -410,12 +356,14 @@ public class AppRTCAudioManager { // If BT is not available, it can't be the user selection. userSelectedAudioDevice = CallIntegration.AudioDevice.NONE; } - if (hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) { + if (hasWiredHeadset + && userSelectedAudioDevice == CallIntegration.AudioDevice.SPEAKER_PHONE) { // If user selected speaker phone, but then plugged wired headset then make // wired headset as user selected device. userSelectedAudioDevice = CallIntegration.AudioDevice.WIRED_HEADSET; } - if (!hasWiredHeadset && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) { + if (!hasWiredHeadset + && userSelectedAudioDevice == CallIntegration.AudioDevice.WIRED_HEADSET) { // If user selected wired headset, but then unplugged wired headset then make // speaker phone as user selected device. userSelectedAudioDevice = CallIntegration.AudioDevice.SPEAKER_PHONE; @@ -425,20 +373,30 @@ public class AppRTCAudioManager { boolean needBluetoothAudioStart = bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE && (userSelectedAudioDevice == CallIntegration.AudioDevice.NONE - || userSelectedAudioDevice == CallIntegration.AudioDevice.BLUETOOTH); + || userSelectedAudioDevice + == CallIntegration.AudioDevice.BLUETOOTH); // Need to stop Bluetooth audio if user selected different device and // Bluetooth SCO connection is established or in the process. boolean needBluetoothAudioStop = (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) + || bluetoothManager.getState() + == AppRTCBluetoothManager.State.SCO_CONNECTING) && (userSelectedAudioDevice != CallIntegration.AudioDevice.NONE - && userSelectedAudioDevice != CallIntegration.AudioDevice.BLUETOOTH); + && userSelectedAudioDevice + != CallIntegration.AudioDevice.BLUETOOTH); if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { - Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " - + "stop=" + needBluetoothAudioStop + ", " - + "BT state=" + bluetoothManager.getState()); + Log.d( + Config.LOGTAG, + "Need BT audio: start=" + + needBluetoothAudioStart + + ", " + + "stop=" + + needBluetoothAudioStop + + ", " + + "BT state=" + + bluetoothManager.getState()); } // Start or stop Bluetooth SCO connection given states set earlier. if (needBluetoothAudioStop) { @@ -467,7 +425,8 @@ public class AppRTCAudioManager { } else { // No wired headset and no Bluetooth, hence the audio-device list can contain speaker // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE + // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or + // AudioDevice.EARPIECE // depending on the user's selection. newAudioDevice = defaultAudioDevice; } @@ -475,9 +434,14 @@ public class AppRTCAudioManager { if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { // Do the required device switch. setAudioDeviceInternal(newAudioDevice); - Log.d(Config.LOGTAG, "New device status: " - + "available=" + audioDevices + ", " - + "selected=" + newAudioDevice); + Log.d( + Config.LOGTAG, + "New device status: " + + "available=" + + audioDevices + + ", " + + "selected=" + + newAudioDevice); if (audioManagerEvents != null) { // Notify a listening client that audio device has been changed. audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); @@ -490,22 +454,19 @@ public class AppRTCAudioManager { ContextCompat.getMainExecutor(apprtcContext).execute(runnable); } - /** - * AudioManager state. - */ + /** AudioManager state. */ public enum AudioManagerState { UNINITIALIZED, PREINITIALIZED, RUNNING, } - /** - * Selected audio device change event. - */ + /** Selected audio device change event. */ public interface AudioManagerEvents { // Callback fired once audio device is changed or list of available audio devices changed. void onAudioDeviceChanged( - CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices); + CallIntegration.AudioDevice selectedAudioDevice, + Set availableAudioDevices); } /* Receiver which handles changes in wired headset availability. */ @@ -520,13 +481,23 @@ public class AppRTCAudioManager { int state = intent.getIntExtra("state", STATE_UNPLUGGED); int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); String name = intent.getStringExtra("name"); - Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " - + "a=" + intent.getAction() + ", s=" - + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" - + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" - + isInitialStickyBroadcast()); + Log.d( + Config.LOGTAG, + "WiredHeadsetReceiver.onReceive" + + AppRTCUtils.getThreadInfo() + + ": " + + "a=" + + intent.getAction() + + ", s=" + + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + + ", m=" + + (microphone == HAS_MIC ? "mic" : "no mic") + + ", n=" + + name + + ", sb=" + + isInitialStickyBroadcast()); hasWiredHeadset = (state == STATE_PLUGGED); updateAudioDeviceState(); } } -} \ No newline at end of file +} From a72214bab33978e4d3ae4d91058f427cf4cbc835 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Fri, 23 Feb 2024 13:04:04 +0000 Subject: [PATCH 177/556] Translated using Weblate (German) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 77c02e0d2..ebedabda4 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1028,4 +1028,7 @@ Datenschutzbestimmungen Quicksy bittet dich um deine Zustimmung zur Verwendung deiner Daten Kontaktlistenintegration ist nicht verfügbar + Anrufintegration nicht verfügbar! + Keine Berechtigung für Telefonanrufe + Kontakt ist nicht verfügbar \ No newline at end of file From 339204212e0d28cfcf10b4f0f7d2b49e947bf517 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 23 Feb 2024 11:42:36 +0000 Subject: [PATCH 178/556] Translated using Weblate (Spanish) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index b7b87ee79..16e440bb6 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1042,4 +1042,7 @@ Quicksy pide tu consentimiento para utilizar tus datos Política de privacidad La lista de contactos no está disponible + Sin permiso para llamar por teléfono + Contacto no disponible + ¡Sin integración de llamadas! \ No newline at end of file From fa655811bc96871eb1fab8aafa144c9a1ab94ae7 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Fri, 23 Feb 2024 10:14:59 +0000 Subject: [PATCH 179/556] Translated using Weblate (Albanian) Currently translated at 98.7% (973 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index 968d845e9..ae11c901b 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -1037,4 +1037,9 @@ Rregulla privatësie Integrimi i listës së kontakteve s’është i mundshmë Kontakti juaj përdor pajisje të paverifikuara. Që të kryhet verifikimi dhe të pengohen sulme MITM, skanoni Kodin e tij QR. + Kontakti s’është gati + S’ka integrim thirrjesh! + Pa leje për bërje thirrjesh + U kalua në version të mëparshëm SASL-i + Pikasje kanalesh përdor një shërbim palë të tretë të quajtur <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Përdorim i kësaj veçorie do t’i transmetojë atij shërbimi adresën tuaj IP dhe terma kërkimesh. Për më tepër hollësi, shihni <a href=https://search.jabber.network/privacy>Rregullat e tyre mbi Privatësinë</a>. \ No newline at end of file From c7e153def9e940edeac46d60a360d0898d86c247 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Fri, 23 Feb 2024 11:38:55 +0000 Subject: [PATCH 180/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 98d14d533..fe4787e67 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1037,4 +1037,7 @@ Quicksy 请求您同意使用您的数据 隐私政策 联系人列表集成不可用 + 联系人不可用 + 没有拨打电话的权限 + 呼叫集成不可用! \ No newline at end of file From 7ee1b9ea134e5fcdc5dbc26cb2837cd009a94c6c Mon Sep 17 00:00:00 2001 From: nautilusx Date: Fri, 23 Feb 2024 13:02:45 +0000 Subject: [PATCH 181/556] Translated using Weblate (German) Currently translated at 100.0% (58 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4209504.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4209504.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4209504.txt b/fastlane/metadata/android/de-DE/changelogs/4209504.txt new file mode 100644 index 000000000..92e5e62ad --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4209504.txt @@ -0,0 +1 @@ +* Verbesserung der Integration von A/V-Aufrufen in das Betriebssystem From 4454da3a48cbd31808f417f527b64f1bf346b3a4 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Fri, 23 Feb 2024 10:18:45 +0000 Subject: [PATCH 182/556] Translated using Weblate (Albanian) Currently translated at 100.0% (58 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- fastlane/metadata/android/sq/changelogs/42037.txt | 10 +++++----- fastlane/metadata/android/sq/changelogs/42050.txt | 1 + fastlane/metadata/android/sq/changelogs/42059.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42060.txt | 1 + fastlane/metadata/android/sq/changelogs/42061.txt | 1 + fastlane/metadata/android/sq/changelogs/42062.txt | 1 + fastlane/metadata/android/sq/changelogs/42065.txt | 1 + fastlane/metadata/android/sq/changelogs/42068.txt | 2 ++ fastlane/metadata/android/sq/changelogs/42072.txt | 3 +++ fastlane/metadata/android/sq/changelogs/4207704.txt | 3 +++ fastlane/metadata/android/sq/changelogs/4208104.txt | 4 ++++ fastlane/metadata/android/sq/changelogs/4208804.txt | 3 +++ fastlane/metadata/android/sq/changelogs/4209004.txt | 2 ++ fastlane/metadata/android/sq/changelogs/4209204.txt | 2 ++ fastlane/metadata/android/sq/changelogs/4209404.txt | 1 + fastlane/metadata/android/sq/changelogs/4209504.txt | 1 + 16 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 fastlane/metadata/android/sq/changelogs/42050.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42059.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42060.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42061.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42062.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42065.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42068.txt create mode 100644 fastlane/metadata/android/sq/changelogs/42072.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4207704.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4208104.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4208804.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4209004.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4209204.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4209404.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4209504.txt diff --git a/fastlane/metadata/android/sq/changelogs/42037.txt b/fastlane/metadata/android/sq/changelogs/42037.txt index 35e334aad..dab12a7a6 100644 --- a/fastlane/metadata/android/sq/changelogs/42037.txt +++ b/fastlane/metadata/android/sq/changelogs/42037.txt @@ -1,8 +1,8 @@ Version 2.10.9 -* Kërko leje Bluetooth, kur bëhen thirrje A/V (Mund ta hidhni tej këtë, nëse s’përdorni kufje Bluetooth me mikrofon) -* Ndreqje të mete, kur thirret dikush në Movim -* Ndreqje shfaqjeje avatari gabim për fjalosje në grup +* Kërko leje Bluetooth, kur bëhen thirrje A/V (Mund ta hidhni tej, nëse s’përdorni kufje Bluetooth me mikrofon) +* Ndreqje të mete, për thirrje në Movim +* Ndreqje shfaqjeje avatari të gabuar për fjalosje në grup * Pyet përherë për lënie jashtë optimizimesh për baterinë * Vendosje flamurke “vetëm vendore” për njoftime “x llogari të lidhura” -* Ndreqje ndërveprimi me shtojcën Google Maps Share Location Plugin -* Heqje poshtëshënimi lidhur me tarifa shërbyesi +* Ndreqje ndërveprimi me shtojcën Google Maps Share Location +* Heqje poshtëshënimi lidhur me tarifë shërbyesi diff --git a/fastlane/metadata/android/sq/changelogs/42050.txt b/fastlane/metadata/android/sq/changelogs/42050.txt new file mode 100644 index 000000000..30e4121e3 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42050.txt @@ -0,0 +1 @@ +* Rritje rrezeje cepi për foto profilesh diff --git a/fastlane/metadata/android/sq/changelogs/42059.txt b/fastlane/metadata/android/sq/changelogs/42059.txt new file mode 100644 index 000000000..2896fb077 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Kalim i versionit për Target SDK sërish në 33 +* Ndreqje problemesh për shërbyes që mbulojnë SASL2 me/pa Administrim brendazi Rrjedhash diff --git a/fastlane/metadata/android/sq/changelogs/42060.txt b/fastlane/metadata/android/sq/changelogs/42060.txt new file mode 100644 index 000000000..dcd9dfe36 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42060.txt @@ -0,0 +1 @@ +* Ndreqje e interpretimit gabimisht të 'q' si një shkronjë cirilike diff --git a/fastlane/metadata/android/sq/changelogs/42061.txt b/fastlane/metadata/android/sq/changelogs/42061.txt new file mode 100644 index 000000000..bb3622e75 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42061.txt @@ -0,0 +1 @@ +* Heqje nga versioni në Google Play e veçorisë së pikasjes së kanaleve diff --git a/fastlane/metadata/android/sq/changelogs/42062.txt b/fastlane/metadata/android/sq/changelogs/42062.txt new file mode 100644 index 000000000..c9b0cac5e --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42062.txt @@ -0,0 +1 @@ +* Çaktivizim i hapjes së kartelave kopjeruajtje (ceb) nga përgjegjës kartelash diff --git a/fastlane/metadata/android/sq/changelogs/42065.txt b/fastlane/metadata/android/sq/changelogs/42065.txt new file mode 100644 index 000000000..a6a7fa9fc --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42065.txt @@ -0,0 +1 @@ +* Sjellje për herë të parë e formati të ri kartelash kopjeruajtje diff --git a/fastlane/metadata/android/sq/changelogs/42068.txt b/fastlane/metadata/android/sq/changelogs/42068.txt new file mode 100644 index 000000000..89ba9f52d --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42068.txt @@ -0,0 +1,2 @@ +* mbulim rregullimesh njoftimi bazuar në bisedë specifike +* përdorim opus-i për mesazhe zanorë në Android 10 diff --git a/fastlane/metadata/android/sq/changelogs/42072.txt b/fastlane/metadata/android/sq/changelogs/42072.txt new file mode 100644 index 000000000..e836fb46c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Kalim versioni për varësi libwebrtc në M117 dhe ndryshim edhe për libvpx +* Rikthim te AAC për mesazhe zanorë +* Mbulim rregullimesh gjuhe sipas aplikacioni diff --git a/fastlane/metadata/android/sq/changelogs/4207704.txt b/fastlane/metadata/android/sq/changelogs/4207704.txt new file mode 100644 index 000000000..9b77d5562 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Mbulim për DNS Private (DNS përmes TLS) +* Mbulim për ikonë nisësi të përshtatur nga temë +* Ndreqje problemi të rrallë me leje, kur ndahen kartela në Android 11+ diff --git a/fastlane/metadata/android/sq/changelogs/4208104.txt b/fastlane/metadata/android/sq/changelogs/4208104.txt new file mode 100644 index 000000000..6905c9f74 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Përdorim më i lehtë i “Shfaq kod QR” +* Mbulim për Faqerojtës PEP të vetëm sistemit +* Shtim mbulimi për Ofertë SDP / Model Përgjigjesh (Përdorur nga kanale SIP) +* Ngritje e versioni të synuar API në Android 14 diff --git a/fastlane/metadata/android/sq/changelogs/4208804.txt b/fastlane/metadata/android/sq/changelogs/4208804.txt new file mode 100644 index 000000000..2a49661a0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4208804.txt @@ -0,0 +1,3 @@ +* Mbulim shpërnguljesh kartelash P2P përmes kanalesh WebRTC të dhënash +* Ndreqje problemesh shkalle ndërveprimi me Bind 2.0 në ejabberd +* Paketim dëshmish Let’s Encrypt rrënje për Android <= 7 diff --git a/fastlane/metadata/android/sq/changelogs/4209004.txt b/fastlane/metadata/android/sq/changelogs/4209004.txt new file mode 100644 index 000000000..1241fb7bb --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* ndreqje të metash të vockla +* ndryshime të vockla te hapat e mirëseardhjes në Quicksy diff --git a/fastlane/metadata/android/sq/changelogs/4209204.txt b/fastlane/metadata/android/sq/changelogs/4209204.txt new file mode 100644 index 000000000..08c5f1322 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Furnizim hyrjeje më të lehtë te “Rregulla Privatësie” në versionin Play Store (Quicksy dhe Conversations) +* Heqje integrimi libri adresash te versioni në Play Store i Conversations diff --git a/fastlane/metadata/android/sq/changelogs/4209404.txt b/fastlane/metadata/android/sq/changelogs/4209404.txt new file mode 100644 index 000000000..bd417916b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4209404.txt @@ -0,0 +1 @@ +* Ndreqje prapakthimesh të vockla të sjella me 2.13.1 diff --git a/fastlane/metadata/android/sq/changelogs/4209504.txt b/fastlane/metadata/android/sq/changelogs/4209504.txt new file mode 100644 index 000000000..d9ae9cd78 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4209504.txt @@ -0,0 +1 @@ +* Përmirësim integrimi thirrjesh A/V me sistemin operativ From 5f2666835a706e8402c89d3b304241cca64486ca Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 23 Feb 2024 11:43:44 +0000 Subject: [PATCH 183/556] Translated using Weblate (Spanish) Currently translated at 100.0% (58 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/4209504.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4209504.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4209504.txt b/fastlane/metadata/android/es-ES/changelogs/4209504.txt new file mode 100644 index 000000000..4900383ba --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4209504.txt @@ -0,0 +1 @@ +* Mejorar la integración de las llamadas A/V en el sistema operativo From 1c3ad05353d23c378c3615937f527f70e136ac11 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Fri, 23 Feb 2024 11:40:39 +0000 Subject: [PATCH 184/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (58 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4209504.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4209504.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4209504.txt b/fastlane/metadata/android/zh-CN/changelogs/4209504.txt new file mode 100644 index 000000000..784da44bb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4209504.txt @@ -0,0 +1 @@ +* 改进音频/视频通话与操作系统的集成 From faf042d29e8bb55ac50bcb1e26b48adead7151da Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Fri, 23 Feb 2024 15:09:23 +0000 Subject: [PATCH 185/556] Translated using Weblate (Romanian) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index ae91b368a..48a2f61a0 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1047,4 +1047,7 @@ Quicksy vă solicită consimțământul pentru a utiliza datele dumneavoastră Integrarea listei de contacte nu este disponibilă Politica de confidențialitate + Nu este permisă efectuarea de apeluri + Integrarea apelurilor nu este disponibilă! + Contactul nu este disponibil \ No newline at end of file From 733abc4b45e2d10b141203a957658f7e5d3a5344 Mon Sep 17 00:00:00 2001 From: ghose Date: Sat, 24 Feb 2024 06:00:29 +0000 Subject: [PATCH 186/556] Translated using Weblate (Galician) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 5e5bc7c07..e517ecd31 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1031,4 +1031,7 @@ Quicksy solicita permiso para usar os teus datos Política de privacidade Non está dispoñible a integración coa lista de contactos + Sen permiso para chamar por teléfono + Non está dispoñible o servizo de chamadas! + O contacto non está dispoñible \ No newline at end of file From cebe688a149c25b5c5f7e02e075402bd2fb17043 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Fri, 23 Feb 2024 21:18:28 +0000 Subject: [PATCH 187/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 13019dcae..23186977e 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1079,4 +1079,7 @@ Quicksy просить згоду на використання Ваших даних Політика конфіденційності Інтеграція зі списком контактів недоступна + Немає дозволу на телефонні дзвінки + Контакт недоступний + Інтеграція викликів недоступна! \ No newline at end of file From e5c73927894fa245dd73b0a4a5dc072c92c35184 Mon Sep 17 00:00:00 2001 From: ghose Date: Sat, 24 Feb 2024 06:02:18 +0000 Subject: [PATCH 188/556] Translated using Weblate (Galician) Currently translated at 51.7% (30 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4209504.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4209504.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209504.txt b/fastlane/metadata/android/gl-ES/changelogs/4209504.txt new file mode 100644 index 000000000..33f208375 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4209504.txt @@ -0,0 +1 @@ +* Melloras na integración das chamadas de A/V co sistema operativo From bab9553750c6e3466244b17ad144cf450d18f6fc Mon Sep 17 00:00:00 2001 From: SomeTr Date: Fri, 23 Feb 2024 21:15:29 +0000 Subject: [PATCH 189/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (58 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4209504.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4209504.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209504.txt b/fastlane/metadata/android/uk/changelogs/4209504.txt new file mode 100644 index 000000000..102af6b1b --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4209504.txt @@ -0,0 +1 @@ +* Покращено інтеграцію аудіо- та відеовикликів в операційну систему From 6f8d8b9330f86392e6645eda7041cf077c4bc371 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 28 Feb 2024 12:01:18 +0100 Subject: [PATCH 190/556] log reason for SM resume failure --- .../eu/siacs/conversations/utils/XmlHelper.java | 13 +++++++++++-- .../eu/siacs/conversations/xmpp/XmppConnection.java | 7 ++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java index 7287297e3..54880be8f 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -1,13 +1,15 @@ package eu.siacs.conversations.utils; import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import eu.siacs.conversations.xml.Element; + +import java.util.Collection; import java.util.Collections; import java.util.List; -import eu.siacs.conversations.xml.Element; - public class XmlHelper { public static String encodeEntities(String content) { content = content.replace("&", "&"); @@ -28,4 +30,11 @@ public class XmlHelper { child -> child != null ? child.getName() : null); return Joiner.on(", ").join(features); } + + public static String print(final Collection children) { + if (children == null) { + return null; + } + return Joiner.on("").join(Iterables.transform(children, Element::toString)); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7011c4d54..e5ddbe9de 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1142,7 +1142,12 @@ public class XmppConnection implements Runnable { mXmppConnectionService.updateConversationUi(); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resumption failed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resumption failed (" + + XmlHelper.print(failed.getChildren()) + + ")"); } resetStreamId(); if (sendBindRequest) { From e1a2d81294efe636252201a2784e649ae1c5681e Mon Sep 17 00:00:00 2001 From: p42ity Date: Sat, 2 Mar 2024 12:39:22 +0100 Subject: [PATCH 191/556] Added Oukitel WP12 Pro and Volla Phone X to the list of AAC sensitive devices (truncated voice messages workaround) --- src/main/java/eu/siacs/conversations/ui/RecordingActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index d3b3a96b2..1179de143 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -106,6 +106,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener .add("ONEPLUS A6010") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133 .add("ONEPLUS A6013") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133 .add("Pixel 4a") // Pixel 4a https://github.com/iNPUTmice/Conversations/issues/4223 + .add("WP12 Pro") // Oukitel WP 12 Pro https://github.com/iNPUTmice/Conversations/issues/4223 + .add("Volla Phone X") // Volla Phone X https://github.com/iNPUTmice/Conversations/issues/4223 .build(); private boolean startRecording() { From 2ac4efa2596498dc05d9cc7e69b3cb28d62d456a Mon Sep 17 00:00:00 2001 From: inference Date: Thu, 29 Feb 2024 19:34:50 +0000 Subject: [PATCH 192/556] Improve onboarding text string --- src/conversations/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values/strings.xml b/src/conversations/res/values/strings.xml index fffee31d6..3e8a0526d 100644 --- a/src/conversations/res/values/strings.xml +++ b/src/conversations/res/values/strings.xml @@ -4,7 +4,7 @@ Use conversations.im Create new account Do you already have an XMPP account? This might be the case if you are already using a different XMPP client or have used Conversations before. If not you can create a new XMPP account right now.\nHint: Some email providers also provide XMPP accounts. - XMPP is a provider independent instant messaging network. You can use this client with what ever XMPP server you choose.\nHowever for your convenience we made it easy to create an account on conversations.im; a provider specially suited for the use with Conversations. + XMPP is a provider-independent instant messaging network. You can use this client with whichever XMPP server you choose.\nHowever, for your convenience we made it easy to create an account on conversations.im; a provider specifically suited for use with Conversations. You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address. Your server invitation @@ -13,4 +13,4 @@ If your contact is nearby, they can also scan the code below to accept your invitation. Join %1$s and chat with me: %2$s Share invite with… - \ No newline at end of file + From 86b733e159831cc26f0b8fd561eff011305a0edf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 1 Mar 2024 14:39:54 +0100 Subject: [PATCH 193/556] prevent receiving (as share with target) file URIs as Element (Matrix client) demonstrated again file URIs are unnecessarily dangerous. On Android 7+ there is no good reason to process them anymore --- .../persistance/FileBackend.java | 38 +++++++++++-------- .../ui/ConversationFragment.java | 4 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index d8050646a..eea7486ab 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -370,29 +370,35 @@ public class FileBackend { } } - public static boolean weOwnFile(final Uri uri) { - if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - return false; - } else { - return weOwnFileLollipop(uri); + public static boolean dangerousFile(final Uri uri) { + if (uri == null || Strings.isNullOrEmpty(uri.getScheme())) { + return true; } + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // On Android 7 (and apps that target 7) it is now longer possible to share files + // with a file scheme. By now you should probably not be running apps that target + // anything less than 7 any more + return true; + } else { + return isFileOwnedByProcess(uri); + } + } + return false; } - private static boolean weOwnFileLollipop(final Uri uri) { + private static boolean isFileOwnedByProcess(final Uri uri) { final String path = uri.getPath(); if (path == null) { - return false; + return true; } - try { - File file = new File(path); - FileDescriptor fd = - ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) - .getFileDescriptor(); - StructStat st = Os.fstat(fd); + try (final var pfd = + ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY)) { + final FileDescriptor fd = pfd.getFileDescriptor(); + final StructStat st = Os.fstat(fd); return st.st_uid == android.os.Process.myUid(); - } catch (FileNotFoundException e) { - return false; - } catch (Exception e) { + } catch (final Exception e) { + // when in doubt. better safe than sorry return true; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index da281617e..c97aea645 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2619,10 +2619,10 @@ public class ConversationFragment extends XmppFragment final Iterator iterator = uris.iterator(); while (iterator.hasNext()) { final Uri uri = iterator.next(); - if (FileBackend.weOwnFile(uri)) { + if (FileBackend.dangerousFile(uri)) { iterator.remove(); Toast.makeText( - getActivity(), + requireActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT) .show(); From 00f52226d8fa1243534fc07bb179f83471b8d8b4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 3 Mar 2024 08:02:31 +0100 Subject: [PATCH 194/556] execute all account state managments on ping thread --- .../conversations/services/XmppConnectionService.java | 10 ++++++++-- .../eu/siacs/conversations/utils/WakeLockHelper.java | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f120c4c54..887ffe937 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -83,6 +83,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -879,7 +880,12 @@ public class XmppConnectionService extends Service { } return START_NOT_STICKY; } - manageAccountConnectionStates(action, intent == null ? null : intent.getExtras()); + final var extras = intent == null ? null : intent.getExtras(); + try { + internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras)); + } catch (final RejectedExecutionException e) { + Log.e(Config.LOGTAG, "can not schedule connection states manager"); + } if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) { expireOldMessages(); } @@ -959,7 +965,7 @@ public class XmppConnectionService extends Service { } } - private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet pingCandidates) { + private boolean processAccountState(final Account account, final boolean interactive, final boolean isUiAction, final boolean isAccountPushed, final HashSet pingCandidates) { if (!account.getStatus().isAttemptReconnect()) { return false; } diff --git a/src/main/java/eu/siacs/conversations/utils/WakeLockHelper.java b/src/main/java/eu/siacs/conversations/utils/WakeLockHelper.java index ce1631b03..9fb38ef86 100644 --- a/src/main/java/eu/siacs/conversations/utils/WakeLockHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/WakeLockHelper.java @@ -36,15 +36,15 @@ import eu.siacs.conversations.Config; public class WakeLockHelper { - public static void acquire(PowerManager.WakeLock wakeLock) { + public static void acquire(final PowerManager.WakeLock wakeLock) { try { wakeLock.acquire(2000); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to acquire wake lock", e); } } - public static void release(PowerManager.WakeLock wakeLock) { + public static void release(final PowerManager.WakeLock wakeLock) { if (wakeLock == null) { return; } @@ -52,7 +52,7 @@ public class WakeLockHelper { if (wakeLock.isHeld()) { wakeLock.release(); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to release wake lock", e); } } From b9a19dc6c7b20abb006c35d34d1673829d8df25a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 3 Mar 2024 20:14:27 +0100 Subject: [PATCH 195/556] do not terminate jingle ft session twice (after iq timeout) --- .../xmpp/jingle/JingleFileTransferConnection.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 632a8f034..845f91ecc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -843,6 +843,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (transport == null) { return; } + // TODO consider setting transport callback to null. requires transport to handle null callback + //transport.setTransportCallback(null); transport.terminate(); this.transport = null; } @@ -980,7 +982,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection public void onTransportSetupFailed() { final var transport = this.transport; if (transport == null) { - // this really is not supposed to happen + // this can happen on IQ timeouts + if (isTerminated()) { + return; + } sendSessionTerminate(Reason.FAILED_APPLICATION, null); return; } From 29978a0f2a127223260db1b37420bed9b5e97e9a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 4 Mar 2024 09:49:03 +0100 Subject: [PATCH 196/556] null check SurfaceTextureHelper --- .../eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java | 3 +++ 1 file changed, 3 insertions(+) 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 c1ff24521..89552fc18 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -57,6 +57,9 @@ class VideoSourceWrapper { final EglBase.Context eglBaseContext) { final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBaseContext); + if (surfaceTextureHelper == null) { + throw new IllegalStateException("Could not create SurfaceTextureHelper"); + } this.videoSource = peerConnectionFactory.createVideoSource(false); this.cameraVideoCapturer.initialize( surfaceTextureHelper, context, this.videoSource.getCapturerObserver()); From 6ea8a258025fdbe2b917d5e1a55925640ed1f300 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 4 Mar 2024 09:50:30 +0100 Subject: [PATCH 197/556] call client app in welcome screen --- src/conversations/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values/strings.xml b/src/conversations/res/values/strings.xml index 3e8a0526d..df76b0739 100644 --- a/src/conversations/res/values/strings.xml +++ b/src/conversations/res/values/strings.xml @@ -4,7 +4,7 @@ Use conversations.im Create new account Do you already have an XMPP account? This might be the case if you are already using a different XMPP client or have used Conversations before. If not you can create a new XMPP account right now.\nHint: Some email providers also provide XMPP accounts. - XMPP is a provider-independent instant messaging network. You can use this client with whichever XMPP server you choose.\nHowever, for your convenience we made it easy to create an account on conversations.im; a provider specifically suited for use with Conversations. + XMPP is a provider-independent instant messaging network. You can use this app with whichever XMPP server you choose.\nHowever, for your convenience we made it easy to create an account on conversations.im; a provider specifically suited for use with Conversations. You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address. Your server invitation From ff96570e4f00bf178efdeca2165721b3e0e42df8 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Sat, 24 Feb 2024 21:13:17 +0000 Subject: [PATCH 198/556] Translated using Weblate (Albanian) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-quicksy/sq/ --- src/quicksy/fastlane/metadata/android/sq/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quicksy/fastlane/metadata/android/sq/short_description.txt b/src/quicksy/fastlane/metadata/android/sq/short_description.txt index 05a7f2969..5848ee953 100644 --- a/src/quicksy/fastlane/metadata/android/sq/short_description.txt +++ b/src/quicksy/fastlane/metadata/android/sq/short_description.txt @@ -1 +1 @@ -Jabber/XMPP me Hyrje të Lehtë dhe Zbulim të Lehtë +Jabber/XMPP me Hyrje të Lehtë dhe Zbulim të Kollajtë From 6414894ffee242e29b3dee8a4981516531db7399 Mon Sep 17 00:00:00 2001 From: MasoudAbkenar Date: Sun, 3 Mar 2024 07:30:05 +0000 Subject: [PATCH 199/556] Translated using Weblate (Persian) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fa/ --- src/main/res/values-fa-rIR/strings.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index 0de5326b6..9eae38a8b 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -844,10 +844,9 @@ لطفاً شمارهٔ تلفن خود را وارد کنید. نوشته با %s هم‌رسانی شد لطفاً %s دیگر دوباره تلاش کنید - %1$s نیاز به دسترسی به فهرست مخاطبانتان دارد تا آن را با فهرست مخاطبان XMPP شما مقایسه کند. -\nبا این کار نام کامل و تصویر نمایهٔ مخاطبانتان نشان داده خواهد شد. + %1$s فهرست مخاطبان شما را به طور محلی، فقط روی همین دستگاه، بررسی می‌کند تا نام و تصویر نمایهٔ مخاطبان مشترک در XMPP را نشان دهد. \n -\n%1$s فهرست مخاطبان شما را فقط به طور آفلاین مقایسه می‌کند، بی‌آن‌که چیزی را به سرور بفرستد. +\nهیچ اطلاعاتی دربارهٔ فهرست مخاطبان شما به جایی فرستاده نمی‌شود! اعتمادنشده ‪xmpp.example.com‬ لرزش معادل بی‌صدا @@ -896,7 +895,7 @@ ثبت نام شکست خورد: رمز زیادی ضعیف است تلاش‌های بیش از حد این برنامه به شما پیامکی می‌فرستد (ممکن است برایتان هزینه داشته باشد) تا شمارهٔ تلفن شما را تأیید کند. کد کشور و شمارهٔ تلفن خود را وارد کنید: - همگام‌سازی با مخاطبان + یکپارچه‌سازی فهرست مخاطبان خطای اتصال تأییدیهٔ امنیتی قابل خواندن نبود پیش‌نویس: @@ -1048,4 +1047,11 @@ نام میزبان کپی نشانی وب به‌روزرسانی حساب ممکن نبود + مخاطب در دسترس نیست + به Quicksy خوش آمدید! + سیاست محرمانگی + یکپارچه‌سازی فهرست مخاطبان در دسترس نیست + این برنامه برای به‌کاربردن داده‌های شما نیازمند موافقت شماست + یکپارچه‌سازی تماس تلفنی در دسترس نیست! + اجازهٔ تماس تلفنی وجود ندارد \ No newline at end of file From df8ba6c4bc348d28ce30d72ff5f55ca74eaad23e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:48 +0000 Subject: [PATCH 200/556] Translated using Weblate (Bulgarian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/bg/ --- src/conversations/res/values-bg/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-bg/strings.xml b/src/conversations/res/values-bg/strings.xml index 92667523d..21f9c8d9a 100644 --- a/src/conversations/res/values-bg/strings.xml +++ b/src/conversations/res/values-bg/strings.xml @@ -5,7 +5,8 @@ Създаване не нов профил Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили.   - XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im — сървър, пригоден да работи най-добре с Conversations. + XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP. +\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im — сървър, пригоден да работи най-добре с Conversations. Получихте покана за %1$s. Ще Ви преведем през процеса на създаване на профил.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Получихте покана за %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на профил.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Вашата покана за сървъра From c3df579aeb1e06fd5c7ce11512f33dca6e227a07 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:48 +0000 Subject: [PATCH 201/556] Translated using Weblate (Danish) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/da/ --- src/conversations/res/values-da-rDK/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-da-rDK/strings.xml b/src/conversations/res/values-da-rDK/strings.xml index f79a92078..28f460eaf 100644 --- a/src/conversations/res/values-da-rDK/strings.xml +++ b/src/conversations/res/values-da-rDK/strings.xml @@ -4,7 +4,8 @@ Brug conversations.im Opret ny konto Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti. - XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations. + XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger. +\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations. Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. Din server invitation From 489e5cf41fc1a754664b55b76dfd9e5d49ec038e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:48 +0000 Subject: [PATCH 202/556] Translated using Weblate (German) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/de/ --- src/conversations/res/values-de/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-de/strings.xml b/src/conversations/res/values-de/strings.xml index 2fd0319a9..5881988ee 100644 --- a/src/conversations/res/values-de/strings.xml +++ b/src/conversations/res/values-de/strings.xml @@ -4,7 +4,8 @@ Benutze conversations.im Neues Konto erstellen Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an. - XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. + XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen. +\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Deine Einladung für den Server From e58fdaddf155f9fa1c1f2c7c2414efe760c62a82 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:48 +0000 Subject: [PATCH 203/556] Translated using Weblate (Greek) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/el/ --- src/conversations/res/values-el/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-el/strings.xml b/src/conversations/res/values-el/strings.xml index c64e3d68e..7dbc19056 100644 --- a/src/conversations/res/values-el/strings.xml +++ b/src/conversations/res/values-el/strings.xml @@ -4,7 +4,8 @@ Χρήση του conversations.im Δημιουργία νέου λογαριασμού Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP. - Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. + Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε. +\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. Έχετε προσκληθεί στο %1$s. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. Η πρόσκλησή σας στον διακομιστή From 9ed182c6d619a0572085416e3eb6e361c8f2061f Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 204/556] Translated using Weblate (Basque) Currently translated at 30.7% (4 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/eu/ --- src/conversations/res/values-eu/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-eu/strings.xml b/src/conversations/res/values-eu/strings.xml index bf9555311..a8efd240d 100644 --- a/src/conversations/res/values-eu/strings.xml +++ b/src/conversations/res/values-eu/strings.xml @@ -4,5 +4,6 @@ Erabili conversations.im Kontu berria sortu XMPP kontu bat badaukazu dagoeneko? Horrela izan daiteke beste XMPP aplikazio bat erabiltzen baduzu edo Conversations lehenago erabili baduzu. Bestela XMPP kontu berri bat sortu dezakezu oraintxe bertan.\nIradokizuna: email hornitzaile batzuek XMPP kontuak hornitzen dituzte ere. - XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu. - \ No newline at end of file + XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu. +\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu. + \ No newline at end of file From 5ed98b25a3d194e318d6e1d8880c506b93e0b3f1 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 205/556] Translated using Weblate (Finnish) Currently translated at 76.9% (10 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/fi/ --- src/conversations/res/values-fi/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-fi/strings.xml b/src/conversations/res/values-fi/strings.xml index 17c75a297..c416b2555 100644 --- a/src/conversations/res/values-fi/strings.xml +++ b/src/conversations/res/values-fi/strings.xml @@ -4,7 +4,8 @@ Käytä conversations.im:ää Luo uusi tili Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin. - XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. + XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa. +\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi. Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi. Kutsusi palvelimelle From cf134927dd330df9322e327457ff9843d388a569 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 206/556] Translated using Weblate (French) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/fr/ --- src/conversations/res/values-fr/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index f0c96726f..752d70041 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -4,7 +4,8 @@ Utiliser conversations.im Créer un nouveau compte Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. - XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. + XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix. +\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Votre invitation au serveur From 5d31d14ed7a367c60564f6c93ca8a1b24ef6070a Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 207/556] Translated using Weblate (Galician) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/gl/ --- src/conversations/res/values-gl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 2becd8bea..5d9f097db 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -4,7 +4,8 @@ Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. - XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. + XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección. +\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. O convite do teu servidor From e77eacc668771f68815a73dac241e54cfcc24f33 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 208/556] Translated using Weblate (Croatian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/hr/ --- src/conversations/res/values-hr/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-hr/strings.xml b/src/conversations/res/values-hr/strings.xml index 093c6d0b9..21b0823be 100644 --- a/src/conversations/res/values-hr/strings.xml +++ b/src/conversations/res/values-hr/strings.xml @@ -4,7 +4,8 @@ Koristite conversations.im Napravi novi račun Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune. - XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations. + XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete. +\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations. Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu. Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu. Vaša pozivnica za poslužitelj From aa28472a218f46b7d8793c55510381c70cf8ebdb Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 209/556] Translated using Weblate (Hungarian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/hu/ --- src/conversations/res/values-hu/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-hu/strings.xml b/src/conversations/res/values-hu/strings.xml index f4c180889..272809007 100644 --- a/src/conversations/res/values-hu/strings.xml +++ b/src/conversations/res/values-hu/strings.xml @@ -4,7 +4,8 @@ A conversations.im használata Új fiók létrehozása Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat. - Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. + Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja. +\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Az Ön kiszolgálómeghívása From 14ac2a7be02d7a4f5a1d9279bc5fce8561e3921f Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 210/556] Translated using Weblate (Japanese) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ja/ --- src/conversations/res/values-ja/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-ja/strings.xml b/src/conversations/res/values-ja/strings.xml index 2d240bedc..287ee093b 100644 --- a/src/conversations/res/values-ja/strings.xml +++ b/src/conversations/res/values-ja/strings.xml @@ -4,7 +4,8 @@ conversations.im を利用する 新規アカウントを作成 XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。 - XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。 + XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。 +\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。 %1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 %1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 サーバーの招待 From bad24ffaccab519cfea4c916600f63ae83ef8f7e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 211/556] Translated using Weblate (Dutch) Currently translated at 84.6% (11 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/nl/ --- src/conversations/res/values-nl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index bc7dbc2fa..366506e84 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -4,7 +4,8 @@ Conversations.im gebruiken Nieuwe account registreren Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan. - XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations. + XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server. +\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations. Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je server uitnodiging From 282126862f7c9b2d2326c747768e73ddd6da5cff Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 212/556] Translated using Weblate (Polish) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/pl/ --- src/conversations/res/values-pl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index f3771aed2..585fe01b3 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -4,7 +4,8 @@ Użyj conversations.im Utwórz nowe konto Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. - XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. + XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP. +\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP. Zaproszenie twojego serwera From c5d396706c10100a8f9ac0fb89f633b0dc2f3f83 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:49 +0000 Subject: [PATCH 213/556] Translated using Weblate (Portuguese (Brazil)) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/pt_BR/ --- src/conversations/res/values-pt-rBR/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-pt-rBR/strings.xml b/src/conversations/res/values-pt-rBR/strings.xml index 0a4b54191..210d814e1 100644 --- a/src/conversations/res/values-pt-rBR/strings.xml +++ b/src/conversations/res/values-pt-rBR/strings.xml @@ -4,7 +4,8 @@ Usar o conversations.im Criar uma nova conta Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP. - O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations. + O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher. +\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations. Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Seu convite do servidor From b896504bb2b0bd1649ae6c62ae64fdee255e578c Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 214/556] Translated using Weblate (Romanian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ro/ --- src/conversations/res/values-ro-rRO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-ro-rRO/strings.xml b/src/conversations/res/values-ro-rRO/strings.xml index baefb00c6..1c49b1418 100644 --- a/src/conversations/res/values-ro-rRO/strings.xml +++ b/src/conversations/res/values-ro-rRO/strings.xml @@ -4,7 +4,8 @@ Folosește conversations.im Creează un cont nou Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP. - XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations. + XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți. +\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations. Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Invitația serverului dumneavoastră From 1c0334f796079aaa2e5207de066f98f03e192aec Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 215/556] Translated using Weblate (Russian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ru/ --- src/conversations/res/values-ru/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index 20b99a7b3..2cf7426c8 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -5,7 +5,8 @@ Создать новый аккаунт У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. \nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. - XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. + XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор. +\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. \nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. From 3450a0d5231aa39e4297cacab0c6a2212977d0e3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 216/556] Translated using Weblate (Slovak) Currently translated at 76.9% (10 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/sk/ --- src/conversations/res/values-sk/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-sk/strings.xml b/src/conversations/res/values-sk/strings.xml index e280344c4..58d638e37 100644 --- a/src/conversations/res/values-sk/strings.xml +++ b/src/conversations/res/values-sk/strings.xml @@ -4,7 +4,8 @@ Použiť conversations.im Vytvoriť nové konto Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá. - XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im; poskytovateľ špeciálne vhodný na používanie s Conversations. + XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete.. +\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im; poskytovateľ špeciálne vhodný na používanie s Conversations. Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu. From 8b52e0ac5cd77c489e0981d747eb8c16803b1dfd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 217/556] Translated using Weblate (Serbian) Currently translated at 38.4% (5 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/sr/ --- src/conversations/res/values-sr/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-sr/strings.xml b/src/conversations/res/values-sr/strings.xml index e668ed7e6..3bbef8725 100644 --- a/src/conversations/res/values-sr/strings.xml +++ b/src/conversations/res/values-sr/strings.xml @@ -4,6 +4,7 @@ Користи conversations.im Направи нови налог Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге. - ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију + ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору. +\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију Ваша серверска позивница - \ No newline at end of file + \ No newline at end of file From 6328773c6f8cc0a04d5fccc1a92fb6ddc3f23a4a Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 218/556] Translated using Weblate (Silesian) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/szl/ --- src/conversations/res/values-szl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-szl/strings.xml b/src/conversations/res/values-szl/strings.xml index 6e0134d06..bc26248a5 100644 --- a/src/conversations/res/values-szl/strings.xml +++ b/src/conversations/res/values-szl/strings.xml @@ -4,7 +4,8 @@ Użyj conversations.im Stwōrz nowe kōnto Mosz już kōnto XMPP? Tak może być, jeźli już używosz inkszego klijynta XMPP aboś używoł abo używała wcześnij Conversations. Jak niy, to możesz stworzić teroz nowe kōnto XMPP.\nDorada: Niykerzi liferańcio emaili dowajōm tyż kōnta XMPP. - XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz.\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations. + XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz. +\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations. Mosz zaproszynie na %1$s. Pokludzymy cie bez proces tworzynio kōnta.\nPo wybraniu %1$s za liferanta, poradzisz kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. Mosz zaproszynie na %1$s. Miano ôd używocza już je do ciebie wybrane. Pokludzymy cie bez proces tworzynio kōnta.\nBydzie szło kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. Twoje zaproszynie na serwer From 973431755f443ff433d72783f9bea4774b680817 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 3 Mar 2024 19:26:50 +0000 Subject: [PATCH 219/556] Translated using Weblate (Turkish) Currently translated at 92.3% (12 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/tr/ --- src/conversations/res/values-tr-rTR/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-tr-rTR/strings.xml b/src/conversations/res/values-tr-rTR/strings.xml index 415bc89e0..db1b0ac99 100644 --- a/src/conversations/res/values-tr-rTR/strings.xml +++ b/src/conversations/res/values-tr-rTR/strings.xml @@ -4,7 +4,8 @@ conversations.im kullan Yeni hesap oluştur Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir. - XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık. + XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz. +\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık. %1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. %1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. Sunucu davetiyeniz From f80e8918e0951eb48b92375c9fd206d783a45366 Mon Sep 17 00:00:00 2001 From: ghose Date: Mon, 4 Mar 2024 05:09:53 +0000 Subject: [PATCH 220/556] Translated using Weblate (Galician) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/gl/ --- src/conversations/res/values-gl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 5d9f097db..0c49a7931 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -4,7 +4,7 @@ Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. - XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección. + XMPP é unha rede de mensaxería independente-do-provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección. \nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. From a3538f975381f7ef004b1bdbf494f5cb8efa6b63 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 4 Mar 2024 13:49:57 +0000 Subject: [PATCH 221/556] Translated using Weblate (Spanish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/es/ --- src/conversations/res/values-es/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-es/strings.xml b/src/conversations/res/values-es/strings.xml index 80958fadc..53d97d6e9 100644 --- a/src/conversations/res/values-es/strings.xml +++ b/src/conversations/res/values-es/strings.xml @@ -4,8 +4,8 @@ Usa conversations.im Crear nueva cuenta ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP. - XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas. -\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations. + XMPP es una red de mensajería instantánea independiente del proveedor. Puedes utilizar esta aplicación con cualquier servidor XMPP que elijas. +\nSin embargo, para tu comodidad, te facilitamos la creación de una cuenta en conversations.im, un proveedor específicamente adaptado para su uso con Conversations. Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu invitación al servidor From 4587b7e818fc8dc33fafcdffab8354c599a911ef Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Mon, 4 Mar 2024 09:27:57 +0000 Subject: [PATCH 222/556] Translated using Weblate (Romanian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ro/ --- src/conversations/res/values-ro-rRO/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-ro-rRO/strings.xml b/src/conversations/res/values-ro-rRO/strings.xml index 1c49b1418..90b8763a9 100644 --- a/src/conversations/res/values-ro-rRO/strings.xml +++ b/src/conversations/res/values-ro-rRO/strings.xml @@ -4,7 +4,7 @@ Folosește conversations.im Creează un cont nou Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP. - XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți. + XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați această aplicație cu orice server XMPP doriți. \nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations. Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. From b4fd767429b1cfab5a77d725844d98822b77dc55 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Mon, 4 Mar 2024 09:24:23 +0000 Subject: [PATCH 223/556] Translated using Weblate (Albanian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/sq/ --- src/conversations/res/values-sq/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-sq/strings.xml b/src/conversations/res/values-sq/strings.xml index 1e3f34b5b..b87d55c82 100644 --- a/src/conversations/res/values-sq/strings.xml +++ b/src/conversations/res/values-sq/strings.xml @@ -1,6 +1,6 @@ - XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë klient mund ta përdorni me cilindo shërbyes XMPP që zgjidhni. + XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë aplikacion mund ta përdorni me cilindo shërbyes XMPP që zgjidhni. \nMegjithatë, për lehtësi, e kemi bërë të kollajshme të krijohet një llogari te conversations.im, një shërbim posaçërisht i përshtatshëm për përdorim me Conversations. Jeni ftuar te %1$s. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie. \nKur zgjidhet %1$s si shërbim, do të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP. From 5993328cde849ba5de3e2350239214f33862458b Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 4 Mar 2024 13:51:33 +0000 Subject: [PATCH 224/556] Translated using Weblate (Spanish) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-quicksy/es/ --- .../metadata/android/es-ES/full_description.txt | 14 ++++++++++++++ .../metadata/android/es-ES/short_description.txt | 1 + 2 files changed, 15 insertions(+) create mode 100644 src/quicksy/fastlane/metadata/android/es-ES/full_description.txt create mode 100644 src/quicksy/fastlane/metadata/android/es-ES/short_description.txt diff --git a/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt b/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 000000000..7b75633e8 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,14 @@ +Quicksy es un programa que se ejecuta en el popular cliente Jabber/XMPP, con descubrimiento de contactos automatizado. + +Regístrese con su número de teléfono y Quicksy, según los números de teléfono de su agenda, sugerirá automáticamente contactos potenciales. + +En esencia, Quicksy es un cliente Jabber completo que le permite comunicarse con cualquier usuario en cualquier servidor público. De manera similar, se puede contactar a los usuarios de Quicksy desde el extranjero simplemente agregando +phonenumber@quicksy.im a su lista de contactos. + +Elimine la sincronización de contactos; la interfaz de usuario se deja intencionalmente lo más cerca posible de Conversations. Esto permite a los usuarios migrar, si lo desean, de Quicksy a Conversations sin tener que volver a aprender cómo funciona la aplicación. + +Los contactos sugeridos consisten en otros usuarios de Quicksy y usuarios habituales de Jabber/XMPP que han proporcionado su ID de Jabber a la Lista de Quicksy (https://quicksy.im/#get-listed). + +NOTA: Para proporcionar (https://quicksy.im/enter/) su ID de Jabber a la Lista +Quicksy requiere una tarifa de registro aplicable única. + +Para más detalles, lea la Política de Privacidad (https://quicksy.im/#privacy). diff --git a/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt b/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 000000000..e988bbf91 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Jabber/XMPP fácil de ingresar y fácil de descubrir From 08ec15e9e8af8ca66489dc719cfcaeba8c0a9365 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 4 Mar 2024 21:42:46 +0000 Subject: [PATCH 225/556] Translated using Weblate (German) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/de/ --- src/conversations/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-de/strings.xml b/src/conversations/res/values-de/strings.xml index 5881988ee..f3a547897 100644 --- a/src/conversations/res/values-de/strings.xml +++ b/src/conversations/res/values-de/strings.xml @@ -4,7 +4,7 @@ Benutze conversations.im Neues Konto erstellen Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an. - XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen. + XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diese App mit jedem beliebigen XMPP-Server nutzen. \nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. From 6ecc481eca762c60287c6dbeb460965509545ec8 Mon Sep 17 00:00:00 2001 From: ghose Date: Tue, 5 Mar 2024 05:14:42 +0000 Subject: [PATCH 226/556] Translated using Weblate (Galician) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/gl/ --- src/conversations/res/values-gl/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 0c49a7931..c36e20c55 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -4,8 +4,8 @@ Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. - XMPP é unha rede de mensaxería independente-do-provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección. -\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. + XMPP é unha rede de mensaxería independente-do-provedor. Podes utilizar esta app con calquera provedor XMPP da túa elección. +\nEmporiso, pola tua comenencia, fixemos que fose doado crear unha conta en conversations.im; un provedor moi axeitado para utilizar con Conversations. Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. O convite do teu servidor From 20e1f542779b1c9027449e4e9875814e2f0fa05a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 5 Mar 2024 13:08:43 +0100 Subject: [PATCH 227/556] play ringback sound on android 6/7 --- .../services/AppRTCAudioManager.java | 28 +++++++++++++++++++ .../services/CallIntegration.java | 10 +++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index 2cd3ac346..d9eaeee48 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -16,6 +16,7 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioDeviceInfo; import android.media.AudioManager; +import android.media.ToneGenerator; import android.util.Log; import androidx.annotation.Nullable; @@ -25,11 +26,14 @@ import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import org.webrtc.ThreadUtils; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; /** AppRTCAudioManager manages all audio related parts of the AppRTC demo. */ public class AppRTCAudioManager { @@ -66,6 +70,7 @@ public class AppRTCAudioManager { private final BroadcastReceiver wiredHeadsetReceiver; // Callback method for changes in audio focus. @Nullable private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; + private ScheduledFuture ringBackFuture; public AppRTCAudioManager(final Context context) { apprtcContext = context; @@ -454,6 +459,29 @@ public class AppRTCAudioManager { ContextCompat.getMainExecutor(apprtcContext).execute(runnable); } + public void startRingBack() { + this.ringBackFuture = + JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate( + () -> { + final var toneGenerator = + new ToneGenerator( + AudioManager.STREAM_MUSIC, + CallIntegration.DEFAULT_VOLUME); + toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); + }, + 0, + 3, + TimeUnit.SECONDS); + } + + public void stopRingBack() { + final var future = this.ringBackFuture; + if (future == null || future.isDone()) { + return; + } + future.cancel(true); + } + /** AudioManager state. */ public enum AudioManagerState { UNINITIALIZED, diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 378dd6277..0074d7079 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CallIntegration extends Connection { - private static final int DEFAULT_VOLUME = 80; + public static final int DEFAULT_VOLUME = 80; private final Context context; @@ -309,7 +309,13 @@ public class CallIntegration extends Connection { @Override public void onStateChanged(final int state) { Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); - // TODO devices before selfManaged() will likely have to play their own ringback sound + if (notSelfManaged()) { + if (state == STATE_DIALING) { + requireAppRtcAudioManager().startRingBack(); + } else { + requireAppRtcAudioManager().stopRingBack(); + } + } if (state == STATE_ACTIVE) { playConnectedSound(); } else if (state == STATE_DISCONNECTED) { From 71763902f82743cbe9fa37d38551826531376fb1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 7 Mar 2024 11:38:21 +0100 Subject: [PATCH 228/556] do not use call integration on devices w/o telephony support --- .../services/CallIntegration.java | 32 ++++++++++++++----- .../CallIntegrationConnectionService.java | 4 +-- .../xmpp/jingle/JingleRtpConnection.java | 2 +- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 0074d7079..796368096 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import android.content.Context; +import android.content.pm.PackageManager; import android.media.AudioManager; import android.media.ToneGenerator; import android.net.Uri; @@ -50,7 +51,12 @@ public class CallIntegration extends Connection { public CallIntegration(final Context context) { this.context = context.getApplicationContext(); if (selfManaged()) { - setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + } else { + throw new AssertionError( + "Trying to set connection properties on unsupported version"); + } this.appRTCAudioManager = null; } else { this.appRTCAudioManager = new AppRTCAudioManager(context); @@ -151,7 +157,11 @@ public class CallIntegration extends Connection { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { setAudioDeviceUpsideDownCake(audioDevice); } else if (selfManaged()) { - setAudioDeviceOreo(audioDevice); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setAudioDeviceOreo(audioDevice); + } else { + throw new AssertionError("Trying to set audio devices on unsupported version"); + } } else { setAudioDeviceFallback(audioDevice); } @@ -309,7 +319,7 @@ public class CallIntegration extends Connection { @Override public void onStateChanged(final int state) { Log.d(Config.LOGTAG, "onStateChanged(" + state + ")"); - if (notSelfManaged()) { + if (notSelfManaged(context)) { if (state == STATE_DIALING) { requireAppRtcAudioManager().startRingBack(); } else { @@ -432,18 +442,24 @@ public class CallIntegration extends Connection { callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); } - public static boolean selfManaged() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + private boolean selfManaged() { + return selfManaged(context); } - public static boolean notSelfManaged() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.O; + public static boolean selfManaged(final Context context) { + final var packageManager = context.getPackageManager(); + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + } + + public static boolean notSelfManaged(final Context context) { + return !selfManaged(context); } public void setInitialAudioDevice(final AudioDevice audioDevice) { Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")"); this.initialAudioDevice = audioDevice; - if (CallIntegration.selfManaged()) { + if (selfManaged()) { // once the 'CallIntegration' gets added to the system we receive calls to update audio // state return; diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 01602dc70..ce4f0eb85 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -314,7 +314,7 @@ public class CallIntegrationConnectionService extends ConnectionService { final Account account, final Jid with, final Set media) { - if (CallIntegration.selfManaged()) { + if (CallIntegration.selfManaged(service)) { final var extras = new Bundle(); extras.putParcelable( TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(service, account)); @@ -349,7 +349,7 @@ public class CallIntegrationConnectionService extends ConnectionService { public static void addNewIncomingCall( final Context context, final AbstractJingleConnection.Id id) { - if (CallIntegration.notSelfManaged()) { + if (CallIntegration.notSelfManaged(context)) { Log.d( Config.LOGTAG, "not adding incoming call to TelecomManager on Android " diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e22d6574e..bd069fa8b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1692,7 +1692,7 @@ public class JingleRtpConnection extends AbstractJingleConnection ringingTimeoutFuture = jingleConnectionManager.schedule( this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); - if (CallIntegration.selfManaged()) { + if (CallIntegration.selfManaged(xmppConnectionService)) { return; } xmppConnectionService.getNotificationService().startRinging(id, getMedia()); From 55fd7157c810cd93e3c8698419eb7b0b5c27ae1f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 7 Mar 2024 11:39:16 +0100 Subject: [PATCH 229/556] version bump to 2.14.0-beta.2 --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/en-US/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/es-ES/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/gl-ES/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/sq/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/uk/changelogs/{4209504.txt => 4209804.txt} | 0 .../android/zh-CN/changelogs/{4209504.txt => 4209804.txt} | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/es-ES/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/gl-ES/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/sq/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4209504.txt => 4209804.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4209504.txt => 4209804.txt} (100%) diff --git a/build.gradle b/build.gradle index 96f35d13d..f04284345 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42095 - versionName "2.14.0-beta" + versionCode 42098 + versionName "2.14.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4209504.txt b/fastlane/metadata/android/de-DE/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4209504.txt rename to fastlane/metadata/android/de-DE/changelogs/4209804.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4209504.txt b/fastlane/metadata/android/en-US/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4209504.txt rename to fastlane/metadata/android/en-US/changelogs/4209804.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4209504.txt b/fastlane/metadata/android/es-ES/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/es-ES/changelogs/4209504.txt rename to fastlane/metadata/android/es-ES/changelogs/4209804.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209504.txt b/fastlane/metadata/android/gl-ES/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/gl-ES/changelogs/4209504.txt rename to fastlane/metadata/android/gl-ES/changelogs/4209804.txt diff --git a/fastlane/metadata/android/sq/changelogs/4209504.txt b/fastlane/metadata/android/sq/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/sq/changelogs/4209504.txt rename to fastlane/metadata/android/sq/changelogs/4209804.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209504.txt b/fastlane/metadata/android/uk/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4209504.txt rename to fastlane/metadata/android/uk/changelogs/4209804.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4209504.txt b/fastlane/metadata/android/zh-CN/changelogs/4209804.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4209504.txt rename to fastlane/metadata/android/zh-CN/changelogs/4209804.txt From 5cca842e6621818f81bf9ff4009fa7ed72d7cb6f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 7 Mar 2024 13:37:40 +0100 Subject: [PATCH 230/556] fix return to call --- .../eu/siacs/conversations/ui/ConversationFragment.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index c97aea645..763bc3760 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1560,17 +1560,18 @@ public class ConversationFragment extends XmppFragment RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); - if (id instanceof AbstractJingleConnection.Id) { + if (id instanceof AbstractJingleConnection) { intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.getSessionId()); - } else if (id instanceof JingleConnectionManager.RtpSessionProposal) { - if (((JingleConnectionManager.RtpSessionProposal) id).media.contains(Media.VIDEO)) { + startActivity(intent); + } else if (id instanceof JingleConnectionManager.RtpSessionProposal proposal) { + if (proposal.media.contains(Media.VIDEO)) { intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } else { intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); } + startActivity(intent); } - startActivity(intent); } } From 781741b43af2f617585a868cc9fe3109362b480f Mon Sep 17 00:00:00 2001 From: Hund Date: Fri, 8 Mar 2024 10:03:21 +0000 Subject: [PATCH 231/556] Translated using Weblate (Swedish) Currently translated at 98.9% (975 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sv/ --- src/main/res/values-sv/strings.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 5bdd6aac1..84b259d18 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -329,8 +329,8 @@ Kopierade XMPP-adress till urklipp Kopierade felmeddelandet till urklipp webbadress - Scanna 2D-streckkod - Visa 2D-streckkod + Scanna QR-kod + Visa QR-kod Visa blockeringslista Kontodetaljer Bekräfta @@ -524,7 +524,7 @@ Text delades med %s Bevilja %1$s åtkomst till extern lagring Bevilja %1$s åtkomst till kameran - Synkronisera med kontakter + Kontaktlistintegration %1$s vill ha behörighet att komma åt din adressbok för att matcha den med din XMPP-kontaktlista. \nDetta kommer att visa dina kontakters fullständiga namn och visningsbilder. \n @@ -1045,4 +1045,5 @@ Funktionen Channel Discovery, använder en tredjepartstjänst som heter <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Om du använder den här funktionen, överförs din IP-adress och din söktermer till den tjänsten. Se deras <a href=https://search.jabber.network/privacy>sekretesspolicy</a> för mer information. Försök inte att återställa säkerhetskopior som du inte har skapat själv! Det gick inte att ta bort kontot från servern + Quicksy ber om ditt samtycke för att använda dina uppgifter \ No newline at end of file From a3cf788e0d0a0114c1ce6842f2c6edab98f8534e Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Thu, 7 Mar 2024 09:50:59 +0000 Subject: [PATCH 232/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/zh_Hans/ --- src/conversations/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 34cf35734..b16b819f7 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -5,7 +5,7 @@ 创建新账号 您已经有 XMPP 账号了吗?如果您之前使用过 Conversations 或其他 XMPP 客户端,那么您已经有账号了。如果没有,您可以立即创建一个。 \n提示:一些电子邮件服务也提供 XMPP 账号。 - XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此客户端。 + XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此应用。 \n不过,您可以轻松地在 conversations.im 上创建账号;特别适合与 Conversations 使用的提供者。 您已受邀加入 %1$s。我们将指导您创建账号。 \n当选择 %1$s 作为提供者时,向其他 XMPP 用户提供您的完整地址,就能和对方交流。 From 88030c8b8b024280a5884b9888e14d6ba18cb627 Mon Sep 17 00:00:00 2001 From: petitpois Date: Fri, 8 Mar 2024 21:58:04 +0000 Subject: [PATCH 233/556] Translated using Weblate (French) Currently translated at 99.3% (979 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index fcd0cc811..9dea12601 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -319,8 +319,8 @@ Adresse XMPP copiée dans le presse-papiers Message d\'erreur copié dans le presse-papier adresse internet - Scanner le code-barres 2D - Montrer le code-barres 2D + Scanner le QR code + Montrer le QR code Afficher la liste des contacts bloqués Détails du compte Confirmer @@ -534,7 +534,7 @@ Ce champ est requis Corriger le message Envoyer le message corrigé - Vous avez déjà validé l\'empreinte de cette personne pour accorder votre confiance. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe. + Vous avez déjà fait confiance à l\'empreinte de cette personne pour accorder votre confiance. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe. Vous avez désactivé ce compte Erreur de sécurité : accès invalide au fichier ! Aucune application disponible pour partager l\'URI @@ -615,7 +615,7 @@ Faire automatiquement confiance aux nouveaux appareils des contacts qui n\'ont pas été vérifiés auparavant mais demander une confirmation manuelle à chaque fois qu\'un contact vérifié auparavant utilise un nouvel appareil. Les clés OMEMO ont fait l\'objet d\'une confiance aveugle, cela signifie qu\'il pourrait s\'agir de quelqu\'un d\'autre ou que quelqu\'un aurait pu intercepter l\'échange. Non approuvée - Code-barres 2D invalide + QR code invalide Vide le dossier de cache (utilisé par l\'appplication caméra) Vider le cache Vider le stockage privé @@ -699,7 +699,7 @@ Désirez-vous quand-même vous connecter ? Détails du certificat : Une fois - La lecture d\'un QR Code nécessite l\'accès à l\'appareil photo + La lecture d\'un QR code nécessite l\'accès à l\'appareil photo Faire défiler l\'écran jusqu\'en bas Faire défiler l\'écran jusqu\'en bas après avoir envoyé un message Modifier le message de l\'état @@ -1036,4 +1036,17 @@ Livre audio Distributeur UnifiedPush Ne tentez pas de restaurer des sauvegardes que vous n\'avez pas créées vous-même ! + Signaler un spam + Politique de confidentialité + Quicksy vous demande votre consentement pour utiliser vos données + Signaler un spam et bloquer son auteur + Déconnecté + S\'identifier + Vous vous êtes déconnecté⸱e de ce compte + Bienvenue sur Quicksy ! + Reconnexion sur un autre hôte + Pas d\'autorisation pour passer un appel téléphonique + Contact non disponible + Cacher la notification + Se déconnecter \ No newline at end of file From 6bb99d9da601478c3bcb14482b759013d9d46d31 Mon Sep 17 00:00:00 2001 From: petitpois Date: Fri, 8 Mar 2024 21:34:40 +0000 Subject: [PATCH 234/556] Translated using Weblate (French) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/fr/ --- src/conversations/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index 752d70041..ba7122979 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -4,7 +4,7 @@ Utiliser conversations.im Créer un nouveau compte Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. - XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix. + XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser cette application avec n’importe quel serveur XMPP de votre choix. \nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. From 4264764d41c857c26e2c14487e3538aba3d5034d Mon Sep 17 00:00:00 2001 From: petitpois Date: Fri, 8 Mar 2024 22:18:39 +0000 Subject: [PATCH 235/556] Translated using Weblate (French) Currently translated at 50.0% (1 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/fr/ --- .../fastlane/metadata/android/fr-FR/short_description.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/conversations/fastlane/metadata/android/fr-FR/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt b/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 000000000..b4ae66d63 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Messagerie instantanée XMPP chiffrée, facile à utiliser avec votre appareil mobile From ea78617ee1f06a25d1b3bcdbbec2ffddf3c20e1d Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sat, 9 Mar 2024 13:27:59 +0000 Subject: [PATCH 236/556] Translated using Weblate (Polish) Currently translated at 99.6% (982 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index c567574f3..11e31ccdb 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -324,7 +324,7 @@ Skopiowano adres XMPP do schowka Skopiowano komunikat błędu do schowka adres URL - Zeskanuj kod + Zeskanuj kod QR Pokaż kod QR Wyświetl listę banów Szczegóły konta @@ -523,11 +523,10 @@ Tekst udostępniony %s Pozwól %1$s na dostęp do zewnętrznego magazynu Pozwól %1$s na dostępu do aparatu - Synchronizuj z kontaktami - %1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP. -\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów. + Integracja listy kontaktów + %1$s przetwarza twoje kontakty lokalnie, na twoim urządzeniu, by pokazać nazwę i awatar profilu zgodny z kontaktami z XMPP. \n -\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer. +\nŻaden kontakt nie opuści twojego urządzenia! Powiadom o wszystkich wiadomościach Powiadamiaj tylko w przypadku wzmianki o mnie Powiadomienia wyłączone @@ -629,7 +628,7 @@ Automatycznie ufaj wszystkim nowym urządzeniom kontaktów, którzy nie zostali zweryfikowani wcześniej i poproś o ręczne potwierdzenie za każdym razem, kiedy zweryfikowany kontakt dodaje nowe urządzenie. Ślepo zaufane klucze OMEMO, to jest mogą należeć do kogoś innego lub ktoś może się podszywać. Niezaufane - Nieprawidłowy kod kreskowy 2D + Nieprawidłowy kod QR Wyczyść cache (używane przez aparat) Wyczyść cache Wyczyść prywatny magazyn @@ -1052,10 +1051,14 @@ Wylogowano się z tego konta Zaloguj się Ukryj powiadomienie - Twój kontakt korzysta z niezweryfikowanych urządzeń. Zeskanuj ich kod kreskowy 2D, aby przeprowadzić weryfikację i uniemożliwić aktywne ataki MITM. + Twój kontakt korzysta z niezweryfikowanych urządzeń. Zeskanuj ich kod QR, aby przeprowadzić weryfikację i uniemożliwić aktywne ataki MITM. Zgłoś spam i zablokuj nadawcę Wyloguj się Wylogowano - Używasz z niezweryfikowanych urządzeń. Zeskanuj kod kreskowy 2D na innych urządzeniach, aby przeprowadzić weryfikację i uniemożliwić aktywne ataki MITM. + Korzystasz z niezweryfikowanych urządzeń. Zeskanuj kod QR na innych urządzeniach, aby przeprowadzić weryfikację i uniemożliwić aktywne ataki MITM. Zgłoś spam + Polityka prywatności + Witamy w Quicksy! + Kontakt nie jest dostępny + Integracja listy kontaktów nie jest dostępna \ No newline at end of file From bc3ccfb1be87e217fac13329c7a7d5df59924060 Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sat, 9 Mar 2024 13:30:31 +0000 Subject: [PATCH 237/556] Translated using Weblate (Polish) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/pl/ --- src/conversations/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index 585fe01b3..62a7f4104 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -4,7 +4,7 @@ Użyj conversations.im Utwórz nowe konto Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. - XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP. + XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tej aplikacji z dowolnym serwerem XMPP. \nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP. From e5cffa11befeef6b0c5515d749b3e7290423f48a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Mar 2024 17:38:56 +0100 Subject: [PATCH 238/556] fix stanza counting error after inline SM enable --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 9141e1c70..88f740d5b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -935,7 +935,8 @@ public class XmppConnection implements Runnable { private void resetOutboundStanzaQueue() { synchronized (this.mStanzaQueue) { - final List intermediateStanzas = new ArrayList<>(); + final ImmutableList.Builder intermediateStanzasBuilder = + new ImmutableList.Builder<>(); if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -946,12 +947,13 @@ public class XmppConnection implements Runnable { for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) { final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i); if (stanza != null) { - intermediateStanzas.add(stanza); + intermediateStanzasBuilder.add(stanza); } } this.mStanzaQueue.clear(); + final var intermediateStanzas = intermediateStanzasBuilder.build(); for (int i = 0; i < intermediateStanzas.size(); ++i) { - this.mStanzaQueue.put(i, intermediateStanzas.get(i)); + this.mStanzaQueue.append(i + 1, intermediateStanzas.get(i)); } this.stanzasSent = intermediateStanzas.size(); if (Config.EXTENDED_SM_LOGGING) { From 9ad5b68d572f1dd6789dc1a02d9d7ab27a34f8ae Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Mar 2024 17:40:41 +0100 Subject: [PATCH 239/556] do not attempt unique/exporter channel binding on non conscrypt sockets --- .../crypto/sasl/ChannelBinding.java | 29 ++++------ .../crypto/sasl/ChannelBindingMechanism.java | 17 ++++-- .../siacs/conversations/utils/SSLSockets.java | 56 +++++++++---------- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 2eb5e39fb..3cbaaeeb9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -10,15 +10,15 @@ import com.google.common.collect.BiMap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableBiMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + public enum ChannelBinding { NONE, TLS_EXPORTER, @@ -104,22 +104,17 @@ public enum ChannelBinding { } private static String shortName(final ChannelBinding channelBinding) { - switch (channelBinding) { - case TLS_UNIQUE: - return "UNIQ"; - case TLS_EXPORTER: - return "EXPR"; - case TLS_SERVER_END_POINT: - return "ENDP"; - case NONE: - return "NONE"; - default: - throw new AssertionError("Missing short name for " + channelBinding); - } + return switch (channelBinding) { + case TLS_UNIQUE -> "UNIQ"; + case TLS_EXPORTER -> "EXPR"; + case TLS_SERVER_END_POINT -> "ENDP"; + case NONE -> "NONE"; + default -> throw new AssertionError("Missing short name for " + channelBinding); + }; } public static int priority(final ChannelBinding channelBinding) { - if (Arrays.asList(TLS_EXPORTER,TLS_UNIQUE).contains(channelBinding)) { + if (Arrays.asList(TLS_EXPORTER, TLS_UNIQUE).contains(channelBinding)) { return 2; } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { return 1; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java index 7343eb86e..6c763a382 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -20,12 +20,18 @@ public interface ChannelBindingMechanism { ChannelBinding getChannelBinding(); - static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding) + static byte[] getChannelBindingData( + final SSLSocket sslSocket, final ChannelBinding channelBinding) throws SaslMechanism.AuthenticationException { if (sslSocket == null) { - throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket"); + throw new SaslMechanism.AuthenticationException( + "Channel binding attempt on non secure socket"); } if (channelBinding == ChannelBinding.TLS_EXPORTER) { + if (!Conscrypt.isConscrypt(sslSocket)) { + throw new SaslMechanism.AuthenticationException( + "Channel binding attempt on non supporting socket"); + } final byte[] keyingMaterial; try { keyingMaterial = @@ -39,6 +45,10 @@ public interface ChannelBindingMechanism { } return keyingMaterial; } else if (channelBinding == ChannelBinding.TLS_UNIQUE) { + if (!Conscrypt.isConscrypt(sslSocket)) { + throw new SaslMechanism.AuthenticationException( + "Channel binding attempt on non supporting socket"); + } final byte[] unique = Conscrypt.getTlsUnique(sslSocket); if (unique == null) { throw new SaslMechanism.AuthenticationException( @@ -99,8 +109,7 @@ public interface ChannelBindingMechanism { } static int getPriority(final SaslMechanism mechanism) { - if (mechanism instanceof ChannelBindingMechanism) { - final ChannelBindingMechanism channelBindingMechanism = (ChannelBindingMechanism) mechanism; + if (mechanism instanceof ChannelBindingMechanism channelBindingMechanism) { return ChannelBinding.priority(channelBindingMechanism.getChannelBinding()); } else { return 0; diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSockets.java b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java index ae853bea8..4a8680e20 100644 --- a/src/main/java/eu/siacs/conversations/utils/SSLSockets.java +++ b/src/main/java/eu/siacs/conversations/utils/SSLSockets.java @@ -7,6 +7,9 @@ import androidx.annotation.RequiresApi; import com.google.common.base.Strings; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; + import org.conscrypt.Conscrypt; import java.lang.reflect.Method; @@ -24,22 +27,19 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; - public class SSLSockets { public static void setSecurity(final SSLSocket sslSocket) { final String[] supportProtocols; - final Collection supportedProtocols = new LinkedList<>( - Arrays.asList(sslSocket.getSupportedProtocols())); + final Collection supportedProtocols = + new LinkedList<>(Arrays.asList(sslSocket.getSupportedProtocols())); supportedProtocols.remove("SSLv3"); supportProtocols = supportedProtocols.toArray(new String[0]); sslSocket.setEnabledProtocols(supportProtocols); - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sslSocket.getSupportedCipherSuites()); + final String[] cipherSuites = + CryptoHelper.getOrderedCipherSuites(sslSocket.getSupportedCipherSuites()); if (cipherSuites.length > 0) { sslSocket.setEnabledCipherSuites(cipherSuites); } @@ -70,7 +70,8 @@ public class SSLSockets { socket.setSSLParameters(parameters); } - private static void setApplicationProtocolReflection(final SSLSocket socket, final String protocol) { + private static void setApplicationProtocolReflection( + final SSLSocket socket, final String protocol) { try { final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class); // the concatenation of 8-bit, length prefixed protocol names, just one in our case... @@ -78,16 +79,17 @@ public class SSLSockets { final byte[] protocolUTF8Bytes = protocol.getBytes(StandardCharsets.UTF_8); final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1]; lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow - System.arraycopy(protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length); - method.invoke(socket, new Object[]{lengthPrefixedProtocols}); + System.arraycopy( + protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length); + method.invoke(socket, new Object[] {lengthPrefixedProtocols}); } catch (Throwable e) { - Log.e(Config.LOGTAG,"unable to set ALPN on socket",e); + Log.e(Config.LOGTAG, "unable to set ALPN on socket", e); } } public static void setApplicationProtocol(final SSLSocket socket, final String protocol) { if (Conscrypt.isConscrypt(socket)) { - Conscrypt.setApplicationProtocols(socket, new String[]{protocol}); + Conscrypt.setApplicationProtocols(socket, new String[] {protocol}); } else { setApplicationProtocolReflection(socket, protocol); } @@ -113,9 +115,12 @@ public class SSLSockets { } public static Version version(final Socket socket) { - if (socket instanceof SSLSocket) { - final SSLSocket sslSocket = (SSLSocket) socket; - return Version.of(sslSocket.getSession().getProtocol()); + if (socket instanceof SSLSocket sslSocket) { + if (Conscrypt.isConscrypt(sslSocket)) { + return Version.of(sslSocket.getSession().getProtocol()); + } else { + return Version.TLS_UNSUPPORTED_VERSION; + } } else { return Version.NONE; } @@ -126,22 +131,17 @@ public class SSLSockets { TLS_1_1, TLS_1_2, TLS_1_3, - UNKNOWN, + TLS_UNSUPPORTED_VERSION, NONE; private static Version of(final String protocol) { - switch (Strings.nullToEmpty(protocol)) { - case "TLSv1": - return TLS_1_0; - case "TLSv1.1": - return TLS_1_1; - case "TLSv1.2": - return TLS_1_2; - case "TLSv1.3": - return TLS_1_3; - default: - return UNKNOWN; - } + return switch (Strings.nullToEmpty(protocol)) { + case "TLSv1" -> TLS_1_0; + case "TLSv1.1" -> TLS_1_1; + case "TLSv1.2" -> TLS_1_2; + case "TLSv1.3" -> TLS_1_3; + default -> TLS_UNSUPPORTED_VERSION; + }; } } } From 1bf1411e11965d0c5f6c96b53118c99db3661cf3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 10 Mar 2024 18:58:08 +0100 Subject: [PATCH 240/556] add FAST to doap file --- conversations.doap | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/conversations.doap b/conversations.doap index 4d492f9b3..486baed40 100644 --- a/conversations.doap +++ b/conversations.doap @@ -474,12 +474,19 @@ 0.1.0 + + + + complete + 0.1.0 + + - 2.9.13 - 2021-05-03 - + 2.13.4 + 2024-02-20 + From 20935f2271197b0b19fb180fe685f83623b85655 Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sun, 10 Mar 2024 14:51:44 +0000 Subject: [PATCH 241/556] Translated using Weblate (Polish) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 11e31ccdb..a8e690e76 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1061,4 +1061,7 @@ Witamy w Quicksy! Kontakt nie jest dostępny Integracja listy kontaktów nie jest dostępna + Quicksy pyta o pozwolenie na użycie twoich danych + Brak pozwolenia na wypisanie numeru telefonu + Integracja połączeń nie jest dostępna! \ No newline at end of file From d242aaf92050cdb6cc4c82c1c9dd0de73710430d Mon Sep 17 00:00:00 2001 From: v1s7 Date: Sun, 10 Mar 2024 07:44:31 +0000 Subject: [PATCH 242/556] Translated using Weblate (Russian) Currently translated at 100.0% (985 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index fe5e5bc2d..4c18fb1df 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -1057,4 +1057,7 @@ Интеграция списка контактов недоступна Quicksy запрашивает ваше согласие на использование ваших данных Добро пожаловать в Quicksy! + Контакт недоступен + Интеграция вызовов недоступна! + Нет разрешения на телефонный звонок \ No newline at end of file From f4d8601d010ec6540f18307fc37336ebecbebeaf Mon Sep 17 00:00:00 2001 From: Hund Date: Sun, 10 Mar 2024 19:56:03 +0000 Subject: [PATCH 243/556] Translated using Weblate (Swedish) Currently translated at 99.5% (981 of 985 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sv/ --- src/main/res/values-sv/strings.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 84b259d18..30f202d1c 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -1026,7 +1026,7 @@ Logga in Dölj notis Återanslut på annan värd - Din kontakt använder overifierade enheter. Skanna deras 2D-streckkod för att utföra en verifiering och för att förhindra aktiva MITM-attacker. + Din kontakt använder overifierade enheter. Skanna deras QR-kod för att utföra en verifiering och för att förhindra aktiva MITM-attacker. Ta bort kontot från servern Rapportera spam och blockera spammaren Inkommande samtal (%s) · %s @@ -1037,7 +1037,7 @@ Neka Utgående samtal (%s) · %s Utloggad - Du använder overifierade enheter. Skanna 2D-streckkoden på dina andra enheter, för att utföra en verifiering och för att förhindra aktiva MITM-attacker. + Du använder overifierade enheter. Skanna QR-kod på dina andra enheter, för att utföra en verifiering och för att förhindra aktiva MITM-attacker. Utgående samtal · %s Spara som gruppchatt Ljudbok @@ -1046,4 +1046,8 @@ Försök inte att återställa säkerhetskopior som du inte har skapat själv! Det gick inte att ta bort kontot från servern Quicksy ber om ditt samtycke för att använda dina uppgifter + Integritetspolicy + Integrering av kontaktlista är inte tillgängligt + Samtalsintegration inte tillgänglig! + Kontakten är inte tillgänglig \ No newline at end of file From 0f50f711765affd3bac5cbcf08c49167ce00f8cb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 12 Mar 2024 09:22:26 +0100 Subject: [PATCH 244/556] prevent deletion of bookmark w/o also closing conversation --- .../ui/ConferenceDetailsActivity.java | 30 ++++--------------- .../ui/StartConversationActivity.java | 26 ++++++++++++---- src/main/res/menu/muc_details.xml | 5 ---- src/main/res/values-ar/strings.xml | 1 - src/main/res/values-bg/strings.xml | 1 - src/main/res/values-ca/strings.xml | 1 - src/main/res/values-cs/strings.xml | 2 -- src/main/res/values-da-rDK/strings.xml | 2 -- src/main/res/values-de/strings.xml | 2 -- src/main/res/values-el/strings.xml | 1 - src/main/res/values-es/strings.xml | 2 -- src/main/res/values-fa-rIR/strings.xml | 2 -- src/main/res/values-fi/strings.xml | 2 -- src/main/res/values-fr/strings.xml | 2 -- src/main/res/values-gl/strings.xml | 2 -- src/main/res/values-hr/strings.xml | 1 - src/main/res/values-hu/strings.xml | 1 - src/main/res/values-id/strings.xml | 1 - src/main/res/values-it/strings.xml | 2 -- src/main/res/values-ja/strings.xml | 2 -- src/main/res/values-nl/strings.xml | 1 - src/main/res/values-pl/strings.xml | 2 -- src/main/res/values-pt-rBR/strings.xml | 1 - src/main/res/values-ro-rRO/strings.xml | 2 -- src/main/res/values-ru/strings.xml | 2 -- src/main/res/values-sk/strings.xml | 1 - src/main/res/values-sq-rAL/strings.xml | 2 -- src/main/res/values-sr/strings.xml | 1 - src/main/res/values-sv/strings.xml | 2 -- src/main/res/values-szl/strings.xml | 1 - src/main/res/values-tr-rTR/strings.xml | 1 - src/main/res/values-uk/strings.xml | 2 -- src/main/res/values-vi/strings.xml | 2 -- src/main/res/values-zh-rCN/strings.xml | 2 -- src/main/res/values-zh-rTW/strings.xml | 2 -- src/main/res/values/strings.xml | 4 ++- 36 files changed, 29 insertions(+), 87 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index f5fb9c7f5..47debf9c1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -243,9 +243,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers case R.id.action_save_as_bookmark: saveAsBookmark(); break; - case R.id.action_delete_bookmark: - deleteBookmark(); - break; case R.id.action_destroy_room: destroyRoom(); break; @@ -345,28 +342,21 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark); - MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark); - MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); - MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room); + public boolean onPrepareOptionsMenu(final Menu menu) { + final MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark); + final MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); + final MenuItem menuItemDestroyRoom = menu.findItem(R.id.action_destroy_room); menuItemAdvancedMode.setChecked(mAdvancedMode); if (mConversation == null) { return true; } - if (mConversation.getBookmark() != null) { - menuItemSaveBookmark.setVisible(false); - menuItemDeleteBookmark.setVisible(true); - } else { - menuItemDeleteBookmark.setVisible(false); - menuItemSaveBookmark.setVisible(true); - } + menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null); menuItemDestroyRoom.setVisible(mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)); return true; } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); getMenuInflater().inflate(R.menu.muc_details, menu); final MenuItem share = menu.findItem(R.id.action_share); @@ -392,14 +382,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName()); } - protected void deleteBookmark() { - final Account account = mConversation.getAccount(); - final Bookmark bookmark = mConversation.getBookmark(); - bookmark.setConversation(null); - xmppConnectionService.deleteBookmark(account, bookmark); - updateView(); - } - protected void destroyRoom() { final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); AlertDialog.Builder builder = new AlertDialog.Builder(this); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 8b94dd440..1e54f2205 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -502,17 +502,25 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void deleteConference() { - int position = conference_context_id; + final int position = conference_context_id; final Bookmark bookmark = (Bookmark) conferences.get(position); - - AlertDialog.Builder builder = new AlertDialog.Builder(this); + final var conversation = bookmark.getConversation(); + final boolean hasConversation = conversation != null; + final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_bookmark); - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_text, bookmark.getJid().toEscapedString())); - builder.setPositiveButton(R.string.delete, (dialog, which) -> { + if (hasConversation) { + builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString())); + } else { + builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString())); + } + builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> { bookmark.setConversation(null); final Account account = bookmark.getAccount(); xmppConnectionService.deleteBookmark(account, bookmark); + if (conversation != null) { + xmppConnectionService.archiveConversation(conversation); + } filter(mSearchEditText.getText().toString()); }); builder.create().show(); @@ -1245,7 +1253,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { + public void onCreateContextMenu(@NonNull final ContextMenu menu, @NonNull final View v, final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); final StartConversationActivity activity = (StartConversationActivity) getActivity(); if (activity == null) { @@ -1258,6 +1266,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final Bookmark bookmark = (Bookmark) activity.conferences.get(acmi.position); final Conversation conversation = bookmark.getConversation(); final MenuItem share = menu.findItem(R.id.context_share_uri); + final MenuItem delete = menu.findItem(R.id.context_delete_conference); + if (conversation != null) { + delete.setTitle(R.string.delete_and_close); + } else { + delete.setTitle(R.string.delete_bookmark); + } share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous()); } else if (mResContextMenu == R.menu.contact_context) { activity.contact_context_id = acmi.position; diff --git a/src/main/res/menu/muc_details.xml b/src/main/res/menu/muc_details.xml index 16063b2b1..4bd5fa4ee 100644 --- a/src/main/res/menu/muc_details.xml +++ b/src/main/res/menu/muc_details.xml @@ -26,11 +26,6 @@ android:orderInCategory="80" app:showAsAction="never" android:title="@string/save_as_bookmark"/> - الغاء حجب جميع جهات الإتصال من %s? جهة الاتصال محجوبه محجوب - هل تريد حذف %sمن قائمة المفضلة؟ المحادثات مع هذا المفضل لن تحذف. تسجيل حساب جديد في سيرفر تغيير كلمة المرور في سيرفر مشاركة مع diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 910b10fc2..3778e236e 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -50,7 +50,6 @@ Деблокиране на всички контакти от %s? Контактът е блокиран Блокиран - Искате ли да премахнете отметката за %s? Разговорите, свързани с тази отметка, няма да бъдат премахнати. Регистриране на нов профил на сървъра Промяна на паролата в сървъра Споделяне с… diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index d57c9a0cc..15859ec69 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -50,7 +50,6 @@ Voleu desbloquejar tots el contactes de %s? Contacte bloquejat Bloquejats - Vols esborrar %s com a marcador? Les converses amb aquest marcador no seran eliminades. Registra un compte nou al servidor Canvia la contrasenya al servidor Comparteix amb… diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 18e2941cd..62ec6018e 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -52,7 +52,6 @@ Odblokovat všechny kontakty z %s? Kontakt zablokován Zablokovaný - Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny. Registrovat nový účet na serveru Změnit heslo na serveru Sdílet s… @@ -993,7 +992,6 @@ Zmeškané hovory Uložit jako skupinový chat Tento skupinový chat jste opustili z technických důvodů - Vyhledávání kanálů používá službu třetí strany jménem <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Používání této služby odešle vaši IP adresu a vyhledávaný termín této službě. Pro více informací konzultujte jejich <a href=https://search.jabber.network/privacy>Zásady ochrany osobních údajů</a>. Znovu spojuji hovor Slepě důvěřované klíče OMEMO, které mohou být někdo jiný nebo někým zasaženy. Příchozí hovor ·(%s) · %s diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index a7a534e1c..29d0e054e 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -50,7 +50,6 @@ Frigiv alle kontakter fra %s? Kontakt blokeret Blokeret - Vil du gerne slette %s som et bogmærke? Samtaler med dette bogmærke vil ikke blive slettet. Register ny konto på server Ændr adgangskode på server Del med… @@ -1003,7 +1002,6 @@ UnifiedPush-distributør Udgående opkald · %s Udgående opkald (%s) · %s - Kanalopdagelse bruger en tredjepartstjeneste kaldet <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Brug af denne funktion vil overføre din IP-adresse og søgetermer til den service. Se deres <a href=https://search.jabber.network/privacy>privatlivspolitik</a> for at få flere oplysninger. Indkommende opkald (%s) · %s Fjern konto fra server En brugervalgt push-server til at videresende push-meddelelser via XMPP til din enhed. diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index ebedabda4..e0edf2989 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -50,7 +50,6 @@ Alle Kontakte von %s entsperren? Kontakt gesperrt Gesperrt - Möchtest du %s als Lesezeichen entfernen? Unterhaltungen mit diesem Lesezeichen werden dabei nicht entfernt. Neues Konto auf Server erstellen Passwort ändern Teilen mit… @@ -1010,7 +1009,6 @@ Gruppenchats Als Gruppenchat speichern Gruppenchats durchsuchen - Die Channelsuche verwendet einen Drittanbieterservice namens <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der <a href=https://search.jabber.network/privacy>Datenschutzerklärung</a>. Versuche nicht, Backups wiederherzustellen, die du nicht selbst erstellt hast! Du versuchst, ein veraltetes Sicherungsdateiformat zu importieren Hörbuch diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index aebf7e03a..771dbb28d 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -50,7 +50,6 @@ Άρση αποκλεισμού όλων των επαφών από το %s; Η επαφή αποκλείστηκε Αποκλεισμένος - Θέλετε να αφαιρέσετε το %s από σελιδοδείκτη; Οι συζητήσεις που σχετίζονται με αυτόν τον σελιδοδείκτη δεν θα αφαιρεθούν. Εγγραφή νέου λογαριασμού στον διακομιστή Αλλαγή συνθηματικού στον διακομιστή Διαμοιρασμός με... diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 16e440bb6..cf96d604e 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -51,7 +51,6 @@ ¿Desbloquear todos los contatos de %s? Contacto bloqueado Bloqueado - ¿Quieres eliminar %s de tus marcadores? Las conversaciones con este marcador no serán eliminadas. Registrar nueva cuenta en servidor Cambiar contraseña en servidor Compartir con… @@ -1023,7 +1022,6 @@ No se pudo eliminar la cuenta del servidor Chats en grupo Buscar un grupo de chats - La búsqueda de canales utiliza un servicio de terceros denominado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Si utiliza esta función, tu dirección IP y la búsqueda de términos serán transferidos a este servicio. Para obtener más información, consulta la <a href=https://search.jabber.network/privacy>Política de privacidad</a>. Guardar como un chat en grupo ¡No intentes restaurar las copias de seguridad que no creaste tu mismo! Estás intentando importar un formato de copia de seguridad obsoleto diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index 9eae38a8b..a29c7ca72 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -582,7 +582,6 @@ XEP-0198: Stream Management همهٔ گفتگوها قطع اعلان‌ها - آیا می‌خواهید نشانک‌گذاری %s را بردارید؟ گفتگوها با این نشانک پاک نخواهند شد. بازگشت به تماس جاری آخرین بار یک دقیقه پیش تغییر نسبت %s ممکن نبود @@ -983,7 +982,6 @@ شما این حساب را غیرفعال کرده‌اید اتصال مخاطب می‌خواهد وضعیت وصل‌بودن شما را بداند - کاوش کانال‌ها یک سرویس خارجی به نام <a href=https://search.jabber.network>search.jabber.network</a> را به‌کار می‌برد.<br><br>به‌کاربردن این ویژگی نشانی IP و کلمات مورد جستجوی شما را به این سرویس می‌فرستد. برای اطلاعات بیشتر <a href=https://search.jabber.network/privacy>سیاست محرمانگی</a> آن‌ها را ببینید. کدی که وارد کردید نادرست است. شماره تلفن خودکار diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index c5f7f4172..61bf523f5 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -49,7 +49,6 @@ Perutaanko kaikkien verkkotunnuksen %s käyttäjien esto? Yhteystieto estetty Estetty - Poistetaanko %s kirjanmerkeistä? Mitään keskustelujasi sen kanssa ei poisteta. Rekisteröi uusi tili palvelimella Vaihda salasanaa palvelimelle Aloita keskustelu @@ -916,7 +915,6 @@ Ryhmäkeskustelut Etsi ryhmäkeskusteluista Suoraan hakuun - Kanavien löytö käyttää kolmannen osapuolen palvelua nimeltä <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Tämän ominaisuuden käyttö lähettää IP-osoitteesi ja hakusanasi palvelulle. Lue lisää heidän <a href=https://search.jabber.network/privacy>yksityisyyskäytännöstään</a> (englanniksi). Hae viesteistä Olet kirjautunut ulos tältä tililtä Vastaamattomat puhelut diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 9dea12601..720c72a82 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -51,7 +51,6 @@ Débloquer tous les contacts de %s ? Contact bloqué Bloqué - Voulez-vous retirer %s des favoris ? La conversation associée à ce favori ne sera pas supprimée. Créer un nouveau compte sur le serveur Changer de mot de passe sur le serveur Partager avec… @@ -994,7 +993,6 @@ \n \n%1$s ne lira que votre carnet d\'adresses et le comparera localement sans rien télécharger sur votre serveur. ficher multimédia - La découverte des canaux utilise un service tiers appelé <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>L\'utilisation de cette fonction transmet votre adresse IP et votre recherche à ce service. Voir leur <a href=https://search.jabber.network/privacy>Politique de gestion de la vie privée</a> pour plus d\'informations. Rechercher des groupes Appel sortant (%s) · %s Votre système d\'exploitation empêche %1$s d\'accéder à l\'internet lorsqu\'il est en arrière-plan. Pour être notifié de nouveaux messages, vous devez permettre à %1$s d\'accéder à Internet lorsque l\'économiseur de données est activé. diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index e517ecd31..ff51f3010 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -50,7 +50,6 @@ Desbloquear todos os contactos desde 1%s? Contacto bloqueado Bloqueado - Desexas eliminar o marcador %s? As conversas deste marcador non se eliminarán. Rexistrar nova conta no servidor Cambiar o contrasinal no servidor Compartir con… @@ -1013,7 +1012,6 @@ Gardar como conversa en grupo Buscar conversas en grupo Conversas en grupo - O descubrimento de canles usa un servizo externo chamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Ao usar esta ferramenta transmitirás o teu enderezo IP e termos de busca a ese servizo. Le a súa <a href=https://search.jabber.network/privacy>Política de Privacidade</a> para saber máis. Non intentes restablecer unha copia de apoio que non tiveses creado ti! Estás intentando importar un ficheiro de apoio co formato antigo Audiolibro diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml index 2ee852279..37dec2d45 100644 --- a/src/main/res/values-hr/strings.xml +++ b/src/main/res/values-hr/strings.xml @@ -56,7 +56,6 @@ Deblokirati sve kontakte iz %s? Kontakt blokiran Blokiran - Želite li ukloniti %s kao oznaku? Razgovori s ovom knjižnom oznakom neće biti uklonjeni. Registrirajte novi račun na poslužitelju Promjena lozinke na poslužitelju Podijeli s… diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 87b1e000f..72ebc8e40 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -50,7 +50,6 @@ %s összes partnerének tiltását feloldja? Partner tiltva Tiltva - Szeretné eltávolítani ezt a könyvjelzőkből: %s? Ezzel a könyvjelzővel megjelölt beszélgetései nem lesznek eltávolítva. Új fiók regisztrálása a kiszolgálón Jelszó megváltoztatása a kiszolgálón Megosztás ezzel… diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 21022e927..16ee09fbc 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -49,7 +49,6 @@ Batalkan blokir semua kontak dari %s? Kontak terblokir Diblok - Apakah Anda ingin menghapus %s bookmark ini? Percakapan di bookmark ini tidak akan dihapus. Daftarkan akun baru di server Ganti password di server Bagikan dengan... diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index de49a1aff..555c3cf89 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -51,7 +51,6 @@ Sbloccare tutti i contatti da %s? Contatto bloccato Bloccato - Vuoi rimuovere %s dai segnalibri? Le conversazioni con questo segnalibro non verranno rimosse. Registra un nuovo profilo sul server Cambia la password sul server Condividi con… @@ -1025,7 +1024,6 @@ Chat di gruppo Salva come chat di gruppo Cerca chat di gruppo - La scoperta dei canali usa un servizio di terze parti chiamato <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>L\'uso di questa funzione invierà il tuo indirizzo IP e i termini di ricerca a quel servizio. Vedi la sua <a href=https://search.jabber.network/privacy>informativa sulla privacy</a> per maggiori informazioni. Non tentare di ripristinare dei backup che non hai creato te stesso! Stai tentando di importare un formato di file di backup obsoleto Audiolibro diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index d25240881..5e3456cf3 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -49,7 +49,6 @@ %sのすべての連絡先のブロックを解除しますか? 連絡先をブロックしました ブロックしました - %s のブックマークを削除しますか? このブックマークとの会話は削除されません。 サーバーに新規アカウントを登録 サーバーのパスワードを変更 …で共有 @@ -987,7 +986,6 @@ ログアウトしました 発信通話 · %s オーディオブック - 談話室の探索は<a href=https://search.jabber.network>search.jabber.network</a>というサービスを利用します.<br><br>利用するとIPアドレスと検索語はそのサービスに送信されます。詳細についてはそのサービスの<a href=https://search.jabber.network/privacy>個人情報保護方針</a>を参照してください。 自分で保存したバックアップしか復元しないでください! XMPP経由でPushメッセージを端末に転送するユーザー指定のPushサーバー。 ログイン diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 4ccdcdfde..29822d81f 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -50,7 +50,6 @@ Alle contacten van %s deblokkeren? Contact geblokkeerd Geblokkeerd - Wil je %s als bladwijzer verwijderen? De gesprekken met deze bladwijzer zullen niet worden verwijderd. Nieuwe account op server registreren Wachtwoord op server veranderen Delen met… diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index a8e690e76..062d3c868 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -52,7 +52,6 @@ Odblokować wszystkie kontakty z %s? Kontakt zablokowany Zablokowane - Czy chcesz usunąć zakładkę %s? Rozmowy z tą zakładką nie zostaną usunięte. Zarejestruj nowe konto na serwerze Zmień hasło na serwerze Udostępnij… @@ -1041,7 +1040,6 @@ Usuń konto z serwera Nie można usunąć konta z serwera Wyszukaj czatów grupowych - Wykrywanie kanałów korzysta z usługi innego podmiotu o nazwie <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Użycie tej funkcji spowoduje przesłanie adresu IP i wyszukiwanych terminów do tej usługi. Zobacz ich <a href=https://search.jabber.network/privacy>Politykę prywatności</a>, aby uzyskać więcej informacji. Czaty grupowe Zapisz jako czat grupowy Nie próbuj przywracać kopii zapasowych, których nie utworzono samodzielnie! diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 7b823803c..f34bb847c 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -51,7 +51,6 @@ Desbloquear todos os contatos de %s? Contato bloqueado Bloqueado - Você deseja remover %s dos favoritos? As conversas associadas a esse favorito não serão removidas. Registrar uma nova conta no servidor Alterar a senha no servidor Compartilhar com... diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 48a2f61a0..7fe270541 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -51,7 +51,6 @@ Deblochează toate contactele de la %s? Contact blocat Blocat - Ați dori să ștergeți %s din semne de carte? Conversațiile asociate cu acest semn de carte nu vor fi șterse. Înregistrează un cont nou pe server Schimbă parola pe server Partajează cu… @@ -1027,7 +1026,6 @@ Nu s-a putut șterge contul de pe server Șterge contul de pe server Discuții de grup - Descoperirea de canale publice folosește un serviciu terț numit <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Folosind această funcție se va transmite adresa dumneavoastră IP și cuvintele căutate către acest serviciu. Pentru mai multe informații citiți <a href=https://search.jabber.network/privacy>Politica de confidențialitate</a> a serviciului. Salvare ca discuție de grup Caută discuții de grup Nu încercați să restaurați copii de rezervă pe care nu le-ați creat personal! diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 4c18fb1df..4aec64650 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -52,7 +52,6 @@ Разблокировать всех пользователей домена %s? Контакт заблокирован Заблокирован - Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены. Создать новую учётную запись на сервере Изменить пароль на сервере Поделиться с… @@ -1025,7 +1024,6 @@ Звонки выключены, пока используется Tor Учётная запись для получения пуш-уведомлений. Выбираемый пользователем сервер для перенаправления уведомлений на Ваше устройство. - Обзор каналов использует сторонний сервис <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Эта функция передаст Ваш IP-адрес и ваш поисковый запрос этому сервису. Ознакомьтесь с их <a href=https://search.jabber.network/privacy>Политикой приватности</a> для получения подробностей. Нет (неактивно) Вы собираетесь проверить ключи OMEMO своей учетной записи. Это безопасно только в том случае, если вы перешли по этой ссылке из надежного источника, где только вы могли опубликовать эту ссылку. Не пытайтесь восстановить резервные копии, которые не были созданы вами! diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 410beca20..bfa4210ae 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -59,7 +59,6 @@ Odblokovať všetky kontakty od %s? Kontakt zablokovaný Zablokovaný - Chcete vymazať %sako záložku? Rozhovory s touto záložkou nebudú zmazané. Registrovať nový účet na serveri Zmeniť heslo na serveri Zdieľať s diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index ae11c901b..5de0a527b 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -840,7 +840,6 @@ Do të donit të hiqet %s prej listës tuaj të kontakteve\? Bisedat me këtë kontakt s’do të hiqen. Do të donit t’i bllokohet %s dërgimi i mesazheve për ju\? Do të donit të zhbllokohet %s dhe të lejohet t’ju dërgojë mesazhe\? - Do të donit të hiqet %s si faqerojtës\? Bisedat me këtë faqerojtës s’do të hiqen. Përdorimi i llogarisë tuaj XMPP për të dërguar “stack traces” ndihmon zhvillimin e pandërprerë të %1$s. Doni të fshihen krejt mesazhet te kjo bisedë\? \n @@ -1041,5 +1040,4 @@ S’ka integrim thirrjesh! Pa leje për bërje thirrjesh U kalua në version të mëparshëm SASL-i - Pikasje kanalesh përdor një shërbim palë të tretë të quajtur <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Përdorim i kësaj veçorie do t’i transmetojë atij shërbimi adresën tuaj IP dhe terma kërkimesh. Për më tepër hollësi, shihni <a href=https://search.jabber.network/privacy>Rregullat e tyre mbi Privatësinë</a>. \ No newline at end of file diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 925c98ffe..82ed6360a 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -51,7 +51,6 @@ Одблокирати све контакте од %s? Контакт блокиран Блокиран - Да ли желите да уклоните %s са обележивача? Преписке са овим контактом неће бити уклоњене. Региструј нови налог на серверу Промени лозинку на серверу Подели помоћу… diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 30f202d1c..dc98cfe6a 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -50,7 +50,6 @@ Avblockera alla kontakter från %s? Kontakt blockerad Blockerad - Vill du ta bort %s som ett bokmärke\? Konversationer med detta bokmärke kommer inte att tas bort. Registrera ett nytt konto på servern Byt lösenord på servern Dela med… @@ -1042,7 +1041,6 @@ Spara som gruppchatt Ljudbok Rapportera spam - Funktionen Channel Discovery, använder en tredjepartstjänst som heter <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Om du använder den här funktionen, överförs din IP-adress och din söktermer till den tjänsten. Se deras <a href=https://search.jabber.network/privacy>sekretesspolicy</a> för mer information. Försök inte att återställa säkerhetskopior som du inte har skapat själv! Det gick inte att ta bort kontot från servern Quicksy ber om ditt samtycke för att använda dina uppgifter diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index 266a41688..71f2abdd3 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -51,7 +51,6 @@ Ôdblokować wszyjske kōntakty ze %s\? Kōntakt zablokowany Zablokowane - Chcesz wymazać zokłodka %s\? Godki z niōm niy bydōm wymazane. Zaregistruj nowe kōnto na serwerze Umiyń hasło na serwerze Udostympnij… diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 511981d15..eec089f97 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -50,7 +50,6 @@ %s üzerinden gelen kişilerdeki engellemeyi kaldırmak istiyor musunuz? Kişi engellendi Engellendi - %s kişisini yer imlerinden çıkarmak ister misiniz? Bu yer imi ile kayıtlı konuşmalar silinmeyecektir. Sunucuda yeni bir hesap oluştur Sunucudaki şifreni değiştir Paylaş… diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 23186977e..53061aed4 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -45,7 +45,6 @@ Розблокувати всі контакти з %s\? Контакт заблоковано Заблоковано - Бажаєте вилучити %s із закладок\? Розмови, пов\'язані з цією закладкою, залишаться. Зареєструвати новий обліковий запис на сервері Змінити пароль Поділитися… @@ -975,7 +974,6 @@ Ця розмова Додати контакт, створити чи приєднатися до групи або знайти канали Обліковий запис XMPP - Пошук каналів використовує сторонній сервіс <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Використання цієї функції передає Вашу IP-адресу та пошукові запити цьому сервісу. Перегляньте їхню <a href=https://search.jabber.network/privacy>Політику конфіденційності</a>, щоб отримати більше інформації. Вихідний виклик · %s %1$s використовує <b>OpenKeychain</b> для шифрування/дешифрування повідомлень і керування публічними ключами.<br><br>OpenKeychain поширюється на умовах ліцензії GPLv3+ і доступний для завантаження на F-Droid та Google Play.<br><br><small>(Після встановлення необхідно перезапустити %1$s.)</small> Додатково diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index dad02f28d..9b0112df9 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -49,7 +49,6 @@ Bỏ chặn tất cả liên hệ từ %s? Đã chặn liên hệ Đã chặn - Bạn có muốn xoá dấu trang %s không? Các cuộc hội thoại với dấu trang này sẽ không bị xoá. Đăng ký tài khoản mới trên máy chủ Đổi mật k trên máy chủ Chia sẻ với… @@ -962,7 +961,6 @@ Tìm kiếm tin nhắn trong nhóm Tải về thất bại: Tệp tin không hợp lệ Cuộc gọi nhỡ - Khám phá kênh sử dụng một dịch vụ bên thứ ba gọi là <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>SỬ dụng chức năng này sẽ truyền địa chỉ IP của bạn và tìm kiếm điều khoản đến dịch vụ đó. Hãy đọc <a href=https://search.jabber.network/privacy>Chính sách quyền riêng tư</a> của họ để có thêm thông tin. Chuyển sang gọi video\? Đang kết nối lại cuộc gọi Cuộc gọi đi · %s diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index fe4787e67..835ae5dfe 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -49,7 +49,6 @@ 解除屏蔽来自 %s 的所有联系人? 联系人已屏蔽 已屏蔽 - 是否从书签中移除 %s?将不会移除与此书签相关的对话。 在服务器上注册新账号 在服务器上修改密码 分享至… @@ -1014,7 +1013,6 @@ 来电 (%s) · %s 去电 (%s) · %s 去电 · %s - 频道发现使用称为 <a href=https://search.jabber.network>search.jabber.network</a> 的第三方服务。<br><br>使用此功能会将您的 IP 地址和搜索词传输到此服务。请参阅其 <a href=https://search.jabber.network/privacy>隐私政策</a> 以获取更多信息。 无法从服务器删除账号 群聊 保存为群聊 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 67ff79f2b..5d955366d 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -49,7 +49,6 @@ 要解除封鎖來自 %s 的所有聯絡人嗎? 聯絡人已封鎖 已封鎖 - 要從書籤中移除 %s 嗎?與此書籤相關的會話將不會被移除。 在伺服器上註冊新帳戶 在伺服器上變更密碼 分享至… @@ -1017,7 +1016,6 @@ 從伺服器移除帳戶 無法從伺服器刪除帳戶 搜尋群組聊天 - 頻道探索使用一個名為 <a href=https://search.jabber.network>search.jabber.network</a> 的第三方服務,<br><br>使用此功能會將您的 IP 位址和搜尋詞彙傳送至此服務。更多資訊請參見他們的 <a href=https://search.jabber.network/privacy>隱私權政策</a>。 群組聊天 儲存為群組聊天 您已登出此帳戶 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 4f67b8a95..cee84bfd3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -51,7 +51,8 @@ Unblock all contacts from %s? Contact blocked Blocked - Would you like to remove %s as a bookmark? Conversations with this bookmark will not be removed. + Would you like to remove the bookmark for %s? + Would you like to remove the bookmark for %s and close the conversation? Register new account on server Change password on server Share with… @@ -1028,4 +1029,5 @@ Privacy policy Contact list integration is not available Call integration not available! + Delete & Close From a2c67a6e383226253dcc5749a6b8aaf10bd997a7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 12 Mar 2024 19:01:39 +0100 Subject: [PATCH 245/556] ensure that jingle ft transport is terminated --- .../jingle/JingleFileTransferConnection.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 845f91ecc..749a44e10 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -253,6 +253,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); + terminateTransport(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } @@ -534,6 +535,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (isTerminated()) { return; } + terminateTransport(); final Throwable rootCause = Throwables.getRootCause(throwable); Log.d(Config.LOGTAG, "unable to send session accept", rootCause); sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); @@ -603,6 +605,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); + terminateTransport(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } @@ -646,6 +649,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); + terminateTransport(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } @@ -708,6 +712,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } else if (transportInfo instanceof SocksByteStreamsTransportInfo.CandidateUsed candidateUsed) { if (!socksBytestreamsTransport.setCandidateUsed(candidateUsed.cid)) { + terminateTransport(); sendSessionTerminate( Reason.FAILED_TRANSPORT, String.format( @@ -734,6 +739,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); + terminateTransport(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } @@ -843,8 +849,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (transport == null) { return; } - // TODO consider setting transport callback to null. requires transport to handle null callback - //transport.setTransportCallback(null); + // TODO consider setting transport callback to null. requires transport to handle null + // callback + // transport.setTransportCallback(null); transport.terminate(); this.transport = null; } @@ -875,8 +882,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } @Override - public void onFailure(@NonNull Throwable throwable) { - onFileTransmissionFailed(throwable); + public void onFailure(@NonNull final Throwable throwable) { + // The state transition in here should be synchronized to not race with the + // state transition in receiveSessionTerminate + synchronized (JingleFileTransferConnection.this) { + onFileTransmissionFailed(throwable); + } } }, MoreExecutors.directExecutor()); From 028b3cff884897acd8873e4a7307f9ddd52ae129 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 12 Mar 2024 19:05:23 +0100 Subject: [PATCH 246/556] remove contacts integration again --- src/playstore/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml index 07ac01234..2f339c96f 100644 --- a/src/playstore/AndroidManifest.xml +++ b/src/playstore/AndroidManifest.xml @@ -3,7 +3,7 @@ - + From 2fa541f2dc5ef6cfb14b4ed6be361e18805b15b2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 12 Mar 2024 19:05:47 +0100 Subject: [PATCH 247/556] version bump to 2.13.5 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b0e622754..5f4ab3fd2 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 - versionCode 42094 - versionName "2.13.4" + versionCode 42099 + versionName "2.13.5" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 8f35242c35a3d49a136b231d6344c6d764070847 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 12 Mar 2024 19:17:55 +0100 Subject: [PATCH 248/556] version bump to 2.14.0-beta.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9594a4727..8b3316364 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42099 - versionName "2.14.0-beta.2" + versionCode 42100 + versionName "2.14.0-beta.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 4e72ef12f9056c22c5d18253571b5a2a2a26f4a9 Mon Sep 17 00:00:00 2001 From: Dirk Date: Tue, 12 Mar 2024 10:25:34 +0000 Subject: [PATCH 249/556] Translated using Weblate (German) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index e0edf2989..06e9f8283 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1029,4 +1029,8 @@ Anrufintegration nicht verfügbar! Keine Berechtigung für Telefonanrufe Kontakt ist nicht verfügbar + Möchtest du das Lesezeichen für %s entfernen und die Unterhaltung schließen? + Löschen & Schließen + Willst du das Lesezeichen für %s entfernen? + Die Channelsuche verwendet einen Drittanbieterservice namens <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der <a href=https://search.jabber.network/privacy>Datenschutzerklärung</a>. \ No newline at end of file From bab05a0dab2659705467365a03fd97ba60ed00f3 Mon Sep 17 00:00:00 2001 From: ghose Date: Wed, 13 Mar 2024 05:08:12 +0000 Subject: [PATCH 250/556] Translated using Weblate (Galician) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index ff51f3010..2cddf75b4 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1032,4 +1032,8 @@ Sen permiso para chamar por teléfono Non está dispoñible o servizo de chamadas! O contacto non está dispoñible + Queres eliminar o marcador para %s e pechar a conversa? + Queres eliminar o marcador para %s? + Eliminar e Pechar + O descubrimento de canles usa un servicio alleo chamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Ao usar esta ferramenta transmites o teu enderezo IP e os termos de busca a ese servizo. Le a súa <a href=https://search.jabber.network/privacy>Política de Privacidade</a> para saber máis. \ No newline at end of file From 7823ccb1a1c1f170338a2c4698e010b499caf826 Mon Sep 17 00:00:00 2001 From: Mako Date: Wed, 13 Mar 2024 03:52:47 +0000 Subject: [PATCH 251/556] Translated using Weblate (Japanese) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 5e3456cf3..a146ca409 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -198,7 +198,7 @@ XEP-0191: ブロッキング コマンド XEP-0237: 名簿バージョン管理 XEP-0198: ストリーム管理 - XEP-0215: 外部サービスの発見 + XEP-0215: 外部サービス探索 XEP-0163: PEP (アバター / OMEMO) XEP-0363: HTTP ファイルアップロード XEP-0357: プッシュ @@ -886,8 +886,8 @@ jabber.network ローカルサーバー - ほとんどのユーザーは、公開されている XMPP エコシステム全体からより良い提案を得るために、‘jabber.network’を選択するはずです。 - 談話室の発見方法 + ほとんどのユーザーは、公開 XMPP エコシステム全体からより適切な提案を得るために「jabber.network」を選択する必要があります。 + 談話室の探索方法 バックアップ アカウントを有効化してください 通話をする @@ -1011,4 +1011,11 @@ Quicksy へようこそ ! 連絡先は未検証のデバイスを使用しています。 QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 未検証のデバイスを使用しています。他のデバイスで QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 + 電話をかける権限がありません + %s のブックマークを削除して会話を閉じますか ? + %s のブックマークを削除しますか ? + 談話室の探索には <a href=https://search.jabber.network>search.jabber.network</a> というサードパーティのサービスを利用します。<br><br>この機能を使用すると、IP アドレスと検索語がこのサービスに送信されます。詳細は、<a href=https://search.jabber.network/privacy>プライバシーポリシー</a>をご覧ください。 + 通話の統合は利用できません。 + 連絡先は利用できません + 削除して閉じる \ No newline at end of file From 39340ad9563615ca4e7f0fcdb2e71e8217be31b3 Mon Sep 17 00:00:00 2001 From: Quini98 Date: Tue, 12 Mar 2024 15:50:59 +0000 Subject: [PATCH 252/556] Translated using Weblate (Dutch) Currently translated at 79.1% (781 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/nl/ --- src/main/res/values-nl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 29822d81f..d19629bc3 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -815,4 +815,5 @@ Kan video niet schakelen. Onversleuteld document Accountregistraties zijn niet ondersteund + Door crashrapportages te versturen help je de ontwikkeling \ No newline at end of file From 7190475325ad6fc67bb897296e9805e8356f0757 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Tue, 12 Mar 2024 19:07:24 +0000 Subject: [PATCH 253/556] Translated using Weblate (Ukrainian) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 53061aed4..10348d6ce 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -231,7 +231,7 @@ channel@conference.example.com/nick channel@conference.example.com Зберегти до закладок - Вилучити закладку + Видалити закладку Вилучити груповий чат Закрити канал Дійсно вилучити цей груповий чат\? @@ -1080,4 +1080,8 @@ Немає дозволу на телефонні дзвінки Контакт недоступний Інтеграція викликів недоступна! + Видалити і закрити + Пошук каналів використовує сторонній сервіс <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Використання цієї функції передає Вашу IP-адресу та пошукові запити цьому сервісу. Перегляньте їхню <a href=https://search.jabber.network/privacy>Політику конфіденційності</a>, щоб отримати більше інформації. + Бажаєте видалити закладку %s і закрити розмову? + Бажаєте видалити закладку %s? \ No newline at end of file From 40b8005e8db11076c11c29c1640ea8a1151255b0 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Tue, 12 Mar 2024 10:54:33 +0000 Subject: [PATCH 254/556] Translated using Weblate (Romanian) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 7fe270541..d0e849912 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1048,4 +1048,8 @@ Nu este permisă efectuarea de apeluri Integrarea apelurilor nu este disponibilă! Contactul nu este disponibil + Ați dori să ștergeți semnul de carte pentru %s și să închideți conversația? + Ați dori să ștergeți semnul de carte pentru %s? + Șterge și închide + Descoperirea de canale publice folosește un serviciu terț numit <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Folosind această funcție se va transmite adresa dumneavoastră IP și cuvintele căutate către acest serviciu. Pentru mai multe informații citiți <a href=https://search.jabber.network/privacy>Politica de confidențialitate</a> a serviciului. \ No newline at end of file From 5ff205315ef2a8e809c691338a25d2853cd4a5ae Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 12 Mar 2024 09:40:57 +0000 Subject: [PATCH 255/556] Translated using Weblate (Albanian) Currently translated at 98.7% (975 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index 5de0a527b..90915fbae 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -1040,4 +1040,8 @@ S’ka integrim thirrjesh! Pa leje për bërje thirrjesh U kalua në version të mëparshëm SASL-i + Pikasja e kanaleve përdor një shërbim palë të tretë të quajtur <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Përdorim i kësaj veçorie do t’i transmetojë atij shërbimi adresën tuaj IP dhe terma kërkimesh. Për më tepër hollësi, shihni <a href=https://search.jabber.network/privacy>Rregullat e tyre mbi Privatësinë</a>. + Doni të hiqet faqerojtësi për %s dhe të mbyllet biseda? + Fshije & Mbylle + Doni të hiqet faqerojtësi për %s? \ No newline at end of file From 3fd322ec9b1b9832c9181227c7f868f8e39bf1f1 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Tue, 12 Mar 2024 13:20:45 +0000 Subject: [PATCH 256/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 835ae5dfe..92b89a77f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1038,4 +1038,7 @@ 联系人不可用 没有拨打电话的权限 呼叫集成不可用! + 是否移除 %s 的书签并关闭对话? + 是否移除 %s 的书签? + 删除 & 关闭 \ No newline at end of file From 80437d20cfa594fe1244fb1b9ee4399b38578a19 Mon Sep 17 00:00:00 2001 From: melanotosjaunty Date: Tue, 12 Mar 2024 12:33:17 +0000 Subject: [PATCH 257/556] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 92b89a77f..62235d15f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1041,4 +1041,5 @@ 是否移除 %s 的书签并关闭对话? 是否移除 %s 的书签? 删除 & 关闭 + 频道发现使用称为 <a href=https://search.jabber.network>search.jabber.network</a> 的第三方服务。<br><br>使用此功能会将您的 IP 地址和搜索词传输到此服务。请参阅其 <a href=https://search.jabber.network/privacy>隐私政策</a> 以获取更多信息。 \ No newline at end of file From ea19e7d46c6b730da6ff794ebcea72a3ef55c492 Mon Sep 17 00:00:00 2001 From: Mako Date: Wed, 13 Mar 2024 06:05:05 +0000 Subject: [PATCH 258/556] Translated using Weblate (Japanese) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ja/ --- src/conversations/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-ja/strings.xml b/src/conversations/res/values-ja/strings.xml index 287ee093b..851af0d4a 100644 --- a/src/conversations/res/values-ja/strings.xml +++ b/src/conversations/res/values-ja/strings.xml @@ -4,7 +4,7 @@ conversations.im を利用する 新規アカウントを作成 XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。 - XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。 + XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このアプリを使用することができます。 \nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。 %1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 %1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 From baf4788220ecc0c52ea291fb9724a2f10cadbf4f Mon Sep 17 00:00:00 2001 From: Quini98 Date: Tue, 12 Mar 2024 15:46:41 +0000 Subject: [PATCH 259/556] Translated using Weblate (Dutch) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/nl/ --- src/conversations/res/values-nl/strings.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index 366506e84..30e90fe6d 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -4,13 +4,14 @@ Conversations.im gebruiken Nieuwe account registreren Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan. - XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server. + XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze applicatie gebruiken met eender welke XMPP-server. \nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations. Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. Je server uitnodiging - Tik op de delen knop om een uitnodiging te versturen naar %1$s + Tik op de delen knop om een uitnodiging te versturen naar %1$s. Als je contactpersoon in de buurt is, kan deze ook onderstaande code scannen om de uitnodiging te aanvaarden. - Deel de uitnodiging met ... + Deel de uitnodiging met… Vergezel %1$s en chat met mij: %2$s + Onjuist geformatteerde provisioningcode \ No newline at end of file From faf2088452e81a9dafec2a23c604650fb9e0f3c5 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 13 Mar 2024 13:43:11 +0000 Subject: [PATCH 260/556] Translated using Weblate (Spanish) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index cf96d604e..542185f64 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1043,4 +1043,8 @@ Sin permiso para llamar por teléfono Contacto no disponible ¡Sin integración de llamadas! + El observador de canales utiliza un servicio de terceros llamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>El uso de esta función transmitirá a ese servicio su dirección IP. y términos de búsqueda. Para obtener más detalles, consulte su <a href=https://search.jabber.network/privacy>Política de privacidad</a>. + Borrar y cerrar + ¿Quieres eliminar el marcador de %s ? + ¿Quieres eliminar el marcador de %s y cerrar la conversación? \ No newline at end of file From 72d194d8ffd2f3f56ca1e6c175426e2560b83a43 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 16 Mar 2024 13:54:47 +0100 Subject: [PATCH 261/556] treat delayed destruction call integration as busy --- .../services/CallIntegration.java | 15 +++++-- .../CallIntegrationConnectionService.java | 6 ++- .../xmpp/jingle/JingleConnectionManager.java | 40 ++++++++++++++----- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 796368096..1777e9fd0 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -43,6 +43,7 @@ public class CallIntegration extends Connection { private AudioDevice initialAudioDevice = null; private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false); private final AtomicBoolean delayedDestructionInitiated = new AtomicBoolean(false); + private final AtomicBoolean isDestroyed = new AtomicBoolean(false); private List availableEndpoints = Collections.emptyList(); @@ -363,7 +364,6 @@ public class CallIntegration extends Connection { final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, DEFAULT_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); - this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null)); } public void retracted() { @@ -389,7 +389,7 @@ public class CallIntegration extends Connection { JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule( () -> { this.setDisconnected(disconnectCause); - this.destroy(); + this.destroyCallIntegration(); }, delay, TimeUnit.MILLISECONDS); @@ -404,7 +404,7 @@ public class CallIntegration extends Connection { return; } this.setDisconnected(disconnectCause); - this.destroy(); + this.destroyCallIntegration(); Log.d(Config.LOGTAG, "destroyed!"); } @@ -472,6 +472,15 @@ public class CallIntegration extends Connection { audioManager.getAudioDevices())); } + private void destroyCallIntegration() { + super.destroy(); + this.isDestroyed.set(true); + } + + public boolean isDestroyed() { + return this.isDestroyed.get(); + } + /** AudioDevice is the names of possible audio devices that we currently support. */ public enum AudioDevice { NONE, diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index ce4f0eb85..d47a28a22 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -347,14 +347,14 @@ public class CallIntegrationConnectionService extends ConnectionService { } } - public static void addNewIncomingCall( + public static boolean addNewIncomingCall( final Context context, final AbstractJingleConnection.Id id) { if (CallIntegration.notSelfManaged(context)) { Log.d( Config.LOGTAG, "not adding incoming call to TelecomManager on Android " + Build.VERSION.RELEASE); - return; + return true; } final var phoneAccountHandle = CallIntegrationConnectionService.getHandle(context, id.account); @@ -373,7 +373,9 @@ public class CallIntegrationConnectionService extends ConnectionService { Config.LOGTAG, id.account.getJid().asBareJid() + ": call integration not available", e); + return false; } + return true; } public static class ServiceConnectionService { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 8541f6a44..0232478f5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -109,13 +109,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { + sessionEnded + ", stranger=" + stranger); - mXmppConnectionService.sendIqPacket( - account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - sessionTermination.setTo(id.with); - sessionTermination.setReason(Reason.BUSY, null); - mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + sendSessionTerminate(account, packet, id); if (busy || stranger) { writeLogMissedIncoming( account, @@ -136,7 +130,21 @@ public class JingleConnectionManager extends AbstractConnectionManager { connections.put(id, connection); if (connection instanceof JingleRtpConnection) { - CallIntegrationConnectionService.addNewIncomingCall(getXmppConnectionService(), id); + if (!CallIntegrationConnectionService.addNewIncomingCall( + mXmppConnectionService, id)) { + connections.remove(id); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not add incoming call"); + sendSessionTerminate(account, packet, id); + writeLogMissedIncoming( + account, + id.with, + id.sessionId, + null, + System.currentTimeMillis(), + false); + } } mXmppConnectionService.updateConversationUi(); @@ -147,14 +155,24 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + private void sendSessionTerminate(final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { + mXmppConnectionService.sendIqPacket( + account, request.generateResponse(IqPacket.TYPE.RESULT), null); + final JinglePacket sessionTermination = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + sessionTermination.setTo(id.with); + sessionTermination.setReason(Reason.BUSY, null); + mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + } + private boolean isUsingClearNet(final Account account) { return !account.isOnion() && !mXmppConnectionService.useTorToConnect(); } public boolean isBusy() { - for (AbstractJingleConnection connection : this.connections.values()) { - if (connection instanceof JingleRtpConnection) { - if (connection.isTerminated()) { + for (final AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleRtpConnection rtpConnection) { + if (connection.isTerminated() && rtpConnection.getCallIntegration().isDestroyed()) { continue; } return true; From 3d5d257707613aa4b35bc29ee22d614f4dc9b4c6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 16 Mar 2024 16:30:57 +0100 Subject: [PATCH 262/556] fix system feature detection for call integration --- .../conversations/services/CallIntegration.java | 11 +++++++++-- .../services/XmppConnectionService.java | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 1777e9fd0..67ea605f5 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -447,9 +447,16 @@ public class CallIntegration extends Connection { } public static boolean selfManaged(final Context context) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasSystemFeature(context); + } + + public static boolean hasSystemFeature(final Context context) { final var packageManager = context.getPackageManager(); - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM); + } else { + return packageManager.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE); + } } public static boolean notSelfManaged(final Context context) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 887ffe937..35b831eb4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1286,7 +1286,9 @@ public class XmppConnectionService extends Service { toggleSetProfilePictureActivity(hasEnabledAccounts); reconfigurePushDistributor(); - CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts); + if (CallIntegration.hasSystemFeature(this)) { + CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts); + } restoreFromDatabase(); @@ -2465,7 +2467,9 @@ public class XmppConnectionService extends Service { public void createAccount(final Account account) { account.initAccountServices(this); databaseBackend.createAccount(account); - CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); + if (CallIntegration.hasSystemFeature(this)) { + CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); + } this.accounts.add(account); this.reconnectAccountInBackground(account); updateAccountUi(); @@ -2589,7 +2593,9 @@ public class XmppConnectionService extends Service { toggleForegroundService(); syncEnabledAccountSetting(); mChannelDiscoveryService.cleanCache(); - CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); + if (CallIntegration.hasSystemFeature(this)) { + CallIntegrationConnectionService.togglePhoneAccountAsync(this, account); + } return true; } else { return false; From 5978494e60773e358143fb0509f1ea82819e80f5 Mon Sep 17 00:00:00 2001 From: fnetX Date: Thu, 14 Mar 2024 21:34:19 +0000 Subject: [PATCH 263/556] Translated using Weblate (French) Currently translated at 99.2% (980 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 720c72a82..936857932 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -513,7 +513,7 @@ Texte partagé avec %s Autoriser %1$s à accéder au stockage externe Autoriser %1$s à accéder à la caméra - Synchroniser avec contacts + Intégration de la liste de contacts Notifier pour tous les messages Notifier seulement en cas de mention Notifications désactivées @@ -1047,4 +1047,6 @@ Contact non disponible Cacher la notification Se déconnecter + L\'intégration de la liste de contacts n\'est pas disponible + Supprimer & Fermer \ No newline at end of file From bac62261f87b7fe408b29ae459a62edb7a04e801 Mon Sep 17 00:00:00 2001 From: locness3 Date: Sun, 17 Mar 2024 18:52:31 +0000 Subject: [PATCH 264/556] Translated using Weblate (French) Currently translated at 50.0% (1 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/fr/ --- .../android/fr-FR/full_description.txt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/conversations/fastlane/metadata/android/fr-FR/full_description.txt diff --git a/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt b/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..653bdec10 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,38 @@ +Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout-en-bout. + +Principes de conception : + +* Être le plus joli et simple d'utilisation possible, sans compromis sur la sécurité ou la vie privée. +* S'appuyer sur des protocoles existants, bien établis +* Ne pas nécessiter de compte Google ou spécifiquement le Google Cloud Messaging (GCM) +* Nécessiter le moins de permissions possible + +Fonctionnalités : +* Chiffrement de bout-en-bout avec au choix, OMEMO ou OpenPGP +* Envoi et réception d'images +* Appels audio et vidéo chiffrés (DTLS-SRTP) +* Interface utilisateur intuitive qui suit les directives de conception d'Android +* Images / avatars pour vos contacts +* Synchronisation avec des clients de bureau +* Conférences (avec prise en charge des marque-pages) +* Intégration avec le carnet d'adresses +* Plusieurs comptes / boîte de réception unifiée +* Impact très faible sur l'autonomie de la batterie + +Conversations facilite la création de compte sur le serveur gratuit conversations.im. Cependant Conversations fonctionne également avec n'importe quel autre serveur XMPP. De nombreux serveurs XMPP sont gérés par des bénévoles et gratuits. + +Fonctionnalités de XMPP : + +Conversations fonctionne avec n'importe quel serveur XMPP. Cependant XMPP est un protocole extensible. Ces extensions sont aussi standardisées dans ce que l'on appelle les XEP. Conversations en prend en charge quelques-unes pour rendre l'expérience utilisateur meilleure dans l'ensemble. Il y a des chances que votre serveur XMPP actuel ne les prenne pas en charge. Ainsi, pour tirer le meilleur parti de Conversations, vous devriez envisager soit de passer à un serveur XMPP qui le fait, ou encore mieux, gérer votre propre serveur XMPP pour vous et vos amis. + +Ces XEP sont actuellement : + +* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT). +* XEP-0163: Personal Eventing Protocol pour les avatars +* XEP-0191: Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts. +* XEP-0198: Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente. +* XEP-0280: Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation. +* XEP-0237: Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité. +* XEP-0313: Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne. +* XEP-0352: Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants. +* XEP-0363: HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur. From 3e333eb97285e71514e1870c02a9b2b03a1e10fa Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 19 Mar 2024 18:00:12 +0100 Subject: [PATCH 265/556] catch exception when checking phone lock state --- .../services/XmppConnectionService.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 35b831eb4..a28b2a484 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1138,23 +1138,23 @@ public class XmppConnectionService extends Service { } public boolean isScreenLocked() { - final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class); + final PowerManager powerManager = getSystemService(PowerManager.class); final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked(); - final boolean interactive = powerManager != null && powerManager.isInteractive(); + final boolean interactive; + try { + interactive = powerManager != null && powerManager.isInteractive(); + } catch (final Exception e) { + return false; + } return locked || !interactive; } private boolean isPhoneSilenced() { - final boolean notificationDnd; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final NotificationManager notificationManager = getSystemService(NotificationManager.class); - final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter(); - notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY; - } else { - notificationDnd = false; - } - final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + final NotificationManager notificationManager = getSystemService(NotificationManager.class); + final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter(); + final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY; + final AudioManager audioManager = getSystemService(AudioManager.class); final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode(); try { if (treatVibrateAsSilent()) { @@ -1162,7 +1162,7 @@ public class XmppConnectionService extends Service { } else { return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT; } - } catch (Throwable throwable) { + } catch (final Throwable throwable) { Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")"); return notificationDnd; } From 400cbd8eee5bf7822b2fda4580403fc98b18b0a3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 20 Mar 2024 07:26:20 +0100 Subject: [PATCH 266/556] channel discovery screen code clean up --- .../ui/ChannelDiscoveryActivity.java | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index 5cf9417e9..1b21cfb77 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -18,6 +18,7 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import com.google.common.base.Strings; @@ -130,7 +131,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } @Override - public boolean onMenuItemActionExpand(MenuItem item) { + public boolean onMenuItemActionExpand(@NonNull MenuItem item) { mSearchEditText.post(() -> { mSearchEditText.requestFocus(); final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @@ -140,7 +141,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } @Override - public boolean onMenuItemActionCollapse(MenuItem item) { + public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); mSearchEditText.setText(""); @@ -189,7 +190,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } @Override - public void onSaveInstanceState(Bundle savedInstanceState) { + public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); } @@ -248,40 +249,47 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O } @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(@NonNull MenuItem item) { final Room room = adapter.getCurrent(); - if (room != null) { - switch (item.getItemId()) { - case R.id.share_with: - StartConversationActivity.shareAsChannel(this, room.address); - return true; - case R.id.open_join_dialog: - final Intent intent = new Intent(this, StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra("force_dialog", true); - intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address))); - startActivity(intent); - return true; - } + if (room == null) { + return false; + } + final int itemId = item.getItemId(); + if (itemId == R.id.share_with) { + StartConversationActivity.shareAsChannel(this, room.address); + return true; + } else if (itemId == R.id.open_join_dialog) { + final Intent intent = new Intent(this, StartConversationActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra("force_dialog", true); + intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address))); + startActivity(intent); + return true; + } else { + return false; } - return false; } - public void joinChannelSearchResult(String selectedAccount, Room result) { - final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK); + public void joinChannelSearchResult(final String selectedAccount, final Room result) { + final Jid jid = + Config.DOMAIN_LOCK == null + ? Jid.ofEscaped(selectedAccount) + : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK); final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin); final Account account = xmppConnectionService.findAccountByJid(jid); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - if (!bookmark.autojoin() && syncAutoJoin) { - bookmark.setAutojoin(true); - xmppConnectionService.createBookmark(account, bookmark); - } - } else { - bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + account, result.getRoom(), true, true, true); + final var existingBookmark = conversation.getBookmark(); + if (existingBookmark == null) { + final var bookmark = new Bookmark(account, conversation.getJid().asBareJid()); bookmark.setAutojoin(syncAutoJoin); xmppConnectionService.createBookmark(account, bookmark); + } else { + if (!existingBookmark.autojoin() && syncAutoJoin) { + existingBookmark.setAutojoin(true); + xmppConnectionService.createBookmark(account, existingBookmark); + } } switchToConversation(conversation); } From 6b00e0b462c1cb315cb586bbb6119ce587d7eb75 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 20 Mar 2024 08:37:32 +0100 Subject: [PATCH 267/556] version bump to 2.14.0 --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/en-US/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/es-ES/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/gl-ES/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/sq/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/uk/changelogs/{4209804.txt => 4210104.txt} | 0 .../android/zh-CN/changelogs/{4209804.txt => 4210104.txt} | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/es-ES/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/gl-ES/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/sq/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4209804.txt => 4210104.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4209804.txt => 4210104.txt} (100%) diff --git a/build.gradle b/build.gradle index 8b3316364..734bf43eb 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42100 - versionName "2.14.0-beta.3" + versionCode 42101 + versionName "2.14.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4209804.txt b/fastlane/metadata/android/de-DE/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4209804.txt rename to fastlane/metadata/android/de-DE/changelogs/4210104.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4209804.txt b/fastlane/metadata/android/en-US/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4209804.txt rename to fastlane/metadata/android/en-US/changelogs/4210104.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/4209804.txt b/fastlane/metadata/android/es-ES/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/es-ES/changelogs/4209804.txt rename to fastlane/metadata/android/es-ES/changelogs/4210104.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4209804.txt b/fastlane/metadata/android/gl-ES/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/gl-ES/changelogs/4209804.txt rename to fastlane/metadata/android/gl-ES/changelogs/4210104.txt diff --git a/fastlane/metadata/android/sq/changelogs/4209804.txt b/fastlane/metadata/android/sq/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/sq/changelogs/4209804.txt rename to fastlane/metadata/android/sq/changelogs/4210104.txt diff --git a/fastlane/metadata/android/uk/changelogs/4209804.txt b/fastlane/metadata/android/uk/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4209804.txt rename to fastlane/metadata/android/uk/changelogs/4210104.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4209804.txt b/fastlane/metadata/android/zh-CN/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4209804.txt rename to fastlane/metadata/android/zh-CN/changelogs/4210104.txt From 6e560dc5dc93a0c4cea5a7ca7011575afe6e8d4b Mon Sep 17 00:00:00 2001 From: wiktor Date: Thu, 21 Mar 2024 07:58:06 +0000 Subject: [PATCH 268/556] Translated using Weblate (Polish) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 062d3c868..db8b08592 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1062,4 +1062,8 @@ Quicksy pyta o pozwolenie na użycie twoich danych Brak pozwolenia na wypisanie numeru telefonu Integracja połączeń nie jest dostępna! + Odkrywanie kanałów używa usługi firmy trzeciej <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Używanie tej funkcji spowoduje transmisję twojego adresu IP oraz kryteriów wyszukiwania. Sprawdź ich <a href=https://search.jabber.network/privacy>politykę prywatności</a> aby uzyskać więcej informacji. + Czy chcesz usunąć zakładkę dla %s? + Usuń i zamknij + Czy chcesz usunąć zakładkę dla %s i zamknąć rozmowę? \ No newline at end of file From ac4b588d2312b1a3e244583a57a9089c2104de58 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@users.noreply.translate.codeberg.org> Date: Wed, 20 Mar 2024 15:54:08 +0000 Subject: [PATCH 269/556] Translated using Weblate (Russian) Currently translated at 100.0% (987 of 987 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 4aec64650..fe2a4ba9e 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -1058,4 +1058,8 @@ Контакт недоступен Интеграция вызовов недоступна! Нет разрешения на телефонный звонок + Вы хотите удалить закладку для «%s»? + Закрыть и удалить + Обзор каналов использует сторонний сервис <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Эта функция передаст Ваш IP-адрес и ваш поисковый запрос этому сервису. Ознакомьтесь с его <a href=https://search.jabber.network/privacy>Политикой конфиденциальности</a> для получения подробностей. + Вы хотите удалить закладку для %s и закрыть беседу? \ No newline at end of file From 5fa42afcfc722465e90029f167ca3e85dfac03bd Mon Sep 17 00:00:00 2001 From: aei Date: Wed, 20 Mar 2024 08:52:53 +0000 Subject: [PATCH 270/556] Translated using Weblate (Arabic) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ar/ --- src/conversations/res/values-ar/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-ar/strings.xml b/src/conversations/res/values-ar/strings.xml index 6483bc9df..b0d52f8f7 100644 --- a/src/conversations/res/values-ar/strings.xml +++ b/src/conversations/res/values-ar/strings.xml @@ -5,7 +5,7 @@ أنشئ حسابًا جديدًا هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق Conversations سابقا. أو يمكنك صنع حساب XMPP جديد الآن. \nملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP. - XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا العميل مع أي خادم XMPP تختاره. + XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا التطبيق مع أي خادم XMPP تختاره. \nولكن من أجل راحتك ، فقد جعلنا من السهل إنشاء حساب على موقع chat. مزود مناسب بشكل خاص للاستخدام مع المحادثات. لقد تمت دعوتك إلى%1$s. سنوجهك خلال عملية إنشاء حساب. \nعند اختيار%1$s كموفر ، ستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك. From fe988765d57d4c0372e146d628a6532007a09860 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@users.noreply.translate.codeberg.org> Date: Wed, 20 Mar 2024 15:53:15 +0000 Subject: [PATCH 271/556] Translated using Weblate (Russian) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ru/ --- src/conversations/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index 2cf7426c8..a62334be8 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -5,7 +5,7 @@ Создать новый аккаунт У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. \nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. - XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор. + XMPP - это независимая сеть обмена сообщениями. Это приложение позволяет подключиться к любому XMPP-серверу на ваш выбор. \nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. \nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. From 3e06eab0fe77a76552996231631874c57d8207cd Mon Sep 17 00:00:00 2001 From: 0ko <0ko@users.noreply.translate.codeberg.org> Date: Wed, 20 Mar 2024 16:01:09 +0000 Subject: [PATCH 272/556] Translated using Weblate (Russian) Currently translated at 6.8% (4 of 58 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/ru/ --- fastlane/metadata/android/ru-RU/changelogs/4209004.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/4209204.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/4209404.txt | 1 + fastlane/metadata/android/ru-RU/changelogs/4209804.txt | 1 + 4 files changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4209004.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4209204.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4209404.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4209804.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4209004.txt b/fastlane/metadata/android/ru-RU/changelogs/4209004.txt new file mode 100644 index 000000000..1ceb3240a --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* незначительные исправления +* значительные изменения приветствия Quicksy diff --git a/fastlane/metadata/android/ru-RU/changelogs/4209204.txt b/fastlane/metadata/android/ru-RU/changelogs/4209204.txt new file mode 100644 index 000000000..fba3c9be5 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Упрощён доступ к Политике конфиденциальности в версии для Play Маркета (Quicksy и Conversations) +* Из версии Conversations для Play Маркета удалена интеграция с Контактами diff --git a/fastlane/metadata/android/ru-RU/changelogs/4209404.txt b/fastlane/metadata/android/ru-RU/changelogs/4209404.txt new file mode 100644 index 000000000..475ea3349 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4209404.txt @@ -0,0 +1 @@ +* Исправлены незначительные ухудшения, появившиеся в версии 2.13.1 diff --git a/fastlane/metadata/android/ru-RU/changelogs/4209804.txt b/fastlane/metadata/android/ru-RU/changelogs/4209804.txt new file mode 100644 index 000000000..3b2e3069b --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4209804.txt @@ -0,0 +1 @@ +* Улучшена интеграция вызовов A/V с операционной системой From c0a43c1f99f3db73b8c4457d3f5fed69d2c30ff4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Mar 2024 09:39:59 +0100 Subject: [PATCH 273/556] fix version code of ru-RU changelog --- .../android/ru-RU/changelogs/{4209804.txt => 4210104.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fastlane/metadata/android/ru-RU/changelogs/{4209804.txt => 4210104.txt} (100%) diff --git a/fastlane/metadata/android/ru-RU/changelogs/4209804.txt b/fastlane/metadata/android/ru-RU/changelogs/4210104.txt similarity index 100% rename from fastlane/metadata/android/ru-RU/changelogs/4209804.txt rename to fastlane/metadata/android/ru-RU/changelogs/4210104.txt From e7edc2ce82ef44c8960ba74a9281715775d08e01 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 22 Mar 2024 11:56:21 +0100 Subject: [PATCH 274/556] unnecessary version code bump to trigger new Google review --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 734bf43eb..2d513477f 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42101 + versionCode 42103 versionName "2.14.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" From 21732237d4cfb0512155ae8bccdf3edf67a1d4a2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 25 Mar 2024 08:50:44 +0100 Subject: [PATCH 275/556] add safeguards to ringtone playing twice --- .../conversations/services/NotificationService.java | 10 ++++++---- .../xmpp/jingle/JingleConnectionManager.java | 2 ++ .../conversations/xmpp/jingle/JingleRtpConnection.java | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 3ad182867..fce5c3c49 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -502,11 +502,9 @@ public class NotificationService { public synchronized void startRinging( final AbstractJingleConnection.Id id, final Set media) { showIncomingCallNotification(id, media); - final NotificationManager notificationManager = - (NotificationManager) - mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); + final NotificationManager notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class); final int currentInterruptionFilter; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && notificationManager != null) { + if (notificationManager != null) { currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter(); } else { currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL @@ -525,6 +523,10 @@ public class NotificationService { if (currentVibrationFuture != null) { currentVibrationFuture.cancel(true); } + final var preexistingRingtone = this.currentlyPlayingRingtone; + if (preexistingRingtone != null) { + preexistingRingtone.stop(); + } final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final Resources resources = mXmppConnectionService.getResources(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0232478f5..e0eaaa08f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -504,6 +504,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { + // TODO remove the remove()!= null check to ensure we always call busy() if (proposal != null && rtpSessionProposals.remove(proposal) != null) { proposal.callIntegration.busy(); writeLogMissedOutgoing( @@ -763,6 +764,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration); callIntegration.setCallback(new ProposalStateCallback(proposal)); + // TODO ensure that there is no previous proposal?! this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index bd069fa8b..c9a7c055c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2691,6 +2691,15 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onCallIntegrationShowIncomingCallUi() { + if (isTerminated()) { + // there might be race conditions with the call integration service invoking this + // callback when the rtp session has already ended. It should be enough to just return + // instead of throwing an exception. however throwing an exception gives us a sense of + // if and how frequently this happens + throw new IllegalStateException( + "CallIntegration requested incoming call UI but session was already terminated"); + } + // TODO apparently this can be called too early as well? xmppConnectionService.getNotificationService().startRinging(id, getMedia()); } From 6b5fb6fee6d8105db246804fb504267c0290c72b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 25 Mar 2024 10:39:24 +0100 Subject: [PATCH 276/556] deliver session-initiate before integrating call otherwise there could potentially be race conditions with showIncomingCallUi being called before we have media information --- .../xmpp/jingle/JingleConnectionManager.java | 44 ++++++++----------- .../xmpp/jingle/JingleRtpConnection.java | 18 ++++++++ 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index e0eaaa08f..9dc13e174 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -128,33 +128,31 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } connections.put(id, connection); - - if (connection instanceof JingleRtpConnection) { - if (!CallIntegrationConnectionService.addNewIncomingCall( - mXmppConnectionService, id)) { - connections.remove(id); - Log.e( - Config.LOGTAG, - account.getJid().asBareJid() + ": could not add incoming call"); - sendSessionTerminate(account, packet, id); - writeLogMissedIncoming( - account, - id.with, - id.sessionId, - null, - System.currentTimeMillis(), - false); - } - } - mXmppConnectionService.updateConversationUi(); connection.deliverPacket(packet); + if (connection instanceof JingleRtpConnection rtpConnection) { + addNewIncomingCall(rtpConnection); + } } else { Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); } } + private void addNewIncomingCall(final JingleRtpConnection rtpConnection) { + if (rtpConnection.isTerminated()) { + Log.d( + Config.LOGTAG, + "skip call integration because something must have gone during initiate"); + return; + } + if (CallIntegrationConnectionService.addNewIncomingCall( + mXmppConnectionService, rtpConnection.getId())) { + return; + } + rtpConnection.integrationFailure(); + } + private void sendSessionTerminate(final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { mXmppConnectionService.sendIqPacket( account, request.generateResponse(IqPacket.TYPE.RESULT), null); @@ -398,9 +396,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); - - CallIntegrationConnectionService.addNewIncomingCall( - getXmppConnectionService(), id); + addNewIncomingCall(rtpConnection); // TODO actually do the automatic accept?! } else { Log.d( @@ -450,9 +446,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); - - CallIntegrationConnectionService.addNewIncomingCall( - getXmppConnectionService(), id); + addNewIncomingCall(rtpConnection); } } else { Log.d( diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c9a7c055c..ab9659185 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2237,6 +2237,24 @@ public class JingleRtpConnection extends AbstractJingleConnection } } + public synchronized void integrationFailure() { + final var state = getState(); + if (state == State.PROPOSED) { + Log.e( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": failed call integration in state proposed"); + rejectCallFromProposed(); + } else if (state == State.SESSION_INITIALIZED) { + Log.e(Config.LOGTAG, id.account.getJid().asBareJid() + ": failed call integration"); + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, "CallIntegration failed"); + } else { + throw new IllegalStateException( + String.format("Can not fail integration in state %s", state)); + } + } + public synchronized void endCall() { if (isTerminated()) { Log.w( From f18ec53233b52240520bb88d186a8cbe9b6daedc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 25 Mar 2024 10:58:20 +0100 Subject: [PATCH 277/556] minor safeguard to ensure call integration ends --- .../conversations/xmpp/jingle/JingleConnectionManager.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 9dc13e174..b70463eb0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -498,8 +498,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { - // TODO remove the remove()!= null check to ensure we always call busy() - if (proposal != null && rtpSessionProposals.remove(proposal) != null) { + if (proposal != null) { + rtpSessionProposals.remove(proposal); proposal.callIntegration.busy(); writeLogMissedOutgoing( account, proposal.with, proposal.sessionId, serverMsgId, timestamp); @@ -758,7 +758,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media, callIntegration); callIntegration.setCallback(new ProposalStateCallback(proposal)); - // TODO ensure that there is no previous proposal?! this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); From 38e9533be41f9079a0781b28a09edd7240357d83 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 26 Mar 2024 10:27:46 +0100 Subject: [PATCH 278/556] bump gradle and AGP --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2d513477f..d7d71d99c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.2' + classpath 'com.android.tools.build:gradle:8.3.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2c87f5ef..239aaaa41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip From dd73b01ab1f128c6eccaaa7d18c96efab01927a6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 27 Mar 2024 10:30:14 +0100 Subject: [PATCH 279/556] rudimentary XEP-0490 implementation --- .../conversations/entities/Conversation.java | 22 +- .../generator/AbstractGenerator.java | 83 ++++---- .../conversations/generator/IqGenerator.java | 22 ++ .../conversations/parser/MessageParser.java | 15 +- .../services/XmppConnectionService.java | 200 ++++++++++++++++-- .../eu/siacs/conversations/xml/Namespace.java | 3 + .../conversations/xmpp/XmppConnection.java | 12 ++ .../xmpp/pep/PublishOptions.java | 1 - 8 files changed, 287 insertions(+), 71 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8bb65cc0f..587bfc496 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.json.JSONArray; @@ -437,6 +438,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } + public Message findReceivedWithRemoteId(final String id) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + public Message findMessageWithServerMsgId(String id) { synchronized (this.messages) { for (Message message : this.messages) { @@ -576,20 +588,20 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public List markRead(String upToUuid) { - final List unread = new ArrayList<>(); + public List markRead(final String upToUuid) { + final ImmutableList.Builder unread = new ImmutableList.Builder<>(); synchronized (this.messages) { - for (Message message : this.messages) { + for (final Message message : this.messages) { if (!message.isRead()) { message.markRead(); unread.add(message); } if (message.getUuid().equals(upToUuid)) { - return unread; + return unread.build(); } } } - return unread; + return unread.build(); } public Message getLatestMessage() { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 42fc3c00f..3f63edbd7 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -2,6 +2,15 @@ package eu.siacs.conversations.generator; import android.util.Base64; +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.XmppConnection; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; @@ -12,54 +21,42 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; - public abstract class AbstractGenerator { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); private final String[] FEATURES = { - Namespace.JINGLE, - Namespace.JINGLE_APPS_FILE_TRANSFER, - Namespace.JINGLE_TRANSPORTS_S5B, - Namespace.JINGLE_TRANSPORTS_IBB, - Namespace.JINGLE_ENCRYPTED_TRANSPORT, - Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - "http://jabber.org/protocol/muc", - "jabber:x:conference", - Namespace.OOB, - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - Namespace.NICK + "+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates" + Namespace.JINGLE, + Namespace.JINGLE_APPS_FILE_TRANSFER, + Namespace.JINGLE_TRANSPORTS_S5B, + Namespace.JINGLE_TRANSPORTS_IBB, + Namespace.JINGLE_ENCRYPTED_TRANSPORT, + Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, + "http://jabber.org/protocol/muc", + "jabber:x:conference", + Namespace.OOB, + "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify", + Namespace.NICK + "+notify", + "urn:xmpp:ping", + "jabber:iq:version", + "http://jabber.org/protocol/chatstates", + Namespace.MDS_DISPLAYED + "+notify" }; private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", - "urn:xmpp:receipts" - }; - private final String[] MESSAGE_CORRECTION_FEATURES = { - "urn:xmpp:message-correct:0" + "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; + private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"}; private final String[] PRIVACY_SENSITIVE = { - "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone + "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone }; private final String[] VOIP_NAMESPACES = { - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - Namespace.JINGLE_MESSAGE + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, + Namespace.JINGLE_MESSAGE }; protected XmppConnectionService mXmppConnectionService; @@ -90,7 +87,11 @@ public abstract class AbstractGenerator { String getCapHash(final Account account) { StringBuilder s = new StringBuilder(); - s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<'); + s.append("client/") + .append(getIdentityType()) + .append("//") + .append(getIdentityName()) + .append('<'); MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index c9fa7f6a6..df87932e5 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -129,6 +129,10 @@ public class IqGenerator extends AbstractGenerator { return retrieve(Namespace.BOOKMARKS2, null); } + public IqPacket retrieveMds() { + return retrieve(Namespace.MDS_DISPLAYED, null); + } + public IqPacket publishNick(String nick) { final Element item = new Element("item"); item.setAttribute("id", "current"); @@ -264,6 +268,24 @@ public class IqGenerator extends AbstractGenerator { return conference; } + public Element mdsDisplayed(final String stanzaId, final Conversation conversation) { + final Jid by; + if (conversation.getMode() == Conversation.MODE_MULTI) { + by = conversation.getJid().asBareJid(); + } else { + by = conversation.getAccount().getJid().asBareJid(); + } + return mdsDisplayed(stanzaId, by); + } + + private Element mdsDisplayed(final String stanzaId, final Jid by) { + final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED); + final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS); + stanzaIdElement.setAttribute("id", stanzaId); + stanzaIdElement.setAttribute("by", by); + return displayed; + } + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, final Set preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index cb0620fa1..ece8a7862 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -271,6 +271,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.updateConversationUi(); } } + } else if (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); } @@ -985,12 +988,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } } - Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); + final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); if (displayed != null) { final String id = displayed.getAttribute("id"); final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender")); if (packet.fromAccount(account) && !selfAddressed) { - dismissNotification(account, counterpart, query, id); + final Conversation c = + mXmppConnectionService.find(account, counterpart.asBareJid()); + final Message message = + (c == null || id == null) ? null : c.findReceivedWithRemoteId(id); + if (message != null && (query == null || query.isCatchup())) { + mXmppConnectionService.markReadUpTo(c, message); + } if (query == null) { activateGracePeriod(account); } @@ -1012,7 +1021,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections - mXmppConnectionService.markRead(conversation); + mXmppConnectionService.markReadUpTo(conversation, message); } } else if (!counterpart.isBareJid() && trueJid != null) { final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a28b2a484..bd9363696 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -49,6 +49,7 @@ import android.util.Pair; import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; @@ -56,6 +57,8 @@ import androidx.core.util.Consumer; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; import org.conscrypt.Conscrypt; import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; @@ -152,6 +155,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; @@ -368,6 +372,12 @@ public class XmppConnectionService extends Service { } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { fetchBookmarks(account); } + + if (connection.getFeatures().mds()) { + fetchMessageDisplayedSynchronization(account); + } else { + Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); + } final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); final boolean catchup = getMessageArchiveService().inCatchup(account); final boolean trackOfflineMessageRetrieval; @@ -392,6 +402,7 @@ public class XmppConnectionService extends Service { unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); } }; + private final AtomicLong mLastExpiryRun = new AtomicLong(0); private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -1902,18 +1913,88 @@ public class XmppConnectionService extends Service { public void fetchBookmarks2(final Account account) { final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket(account, retrieve, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); - final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); - processBookmarksInitial(account, bookmarks, true); - } + sendIqPacket(account, retrieve, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final Map bookmarks = Bookmark.parseFromPubsub(pubsub, a); + processBookmarksInitial(a, bookmarks, true); } }); } + private void fetchMessageDisplayedSynchronization(final Account account) { + Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); + final var retrieve = mIqGenerator.retrieveMds(); + sendIqPacket( + account, + retrieve, + (a, response) -> { + if (response.getType() != IqPacket.TYPE.RESULT) { + 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"))) { + return; + } + for (final Element child : items.getChildren()) { + if ("item".equals(child.getName())) { + processMdsItem(account, child); + } + } + }); + } + + public void processMdsItem(final Account account, final Element item) { + final Jid jid = + item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("id")); + if (jid == null) { + return; + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing mds item for " + jid); + 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 Conversation conversation = find(account, jid); + if (id != null && conversation != null) { + markReadUpToStanzaId(conversation, id); + } + } + + public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) { + final Message message = conversation.findMessageWithServerMsgId(stanzaId); + if (message == null) { // do we want to check if isRead? + return; + } + markReadUpTo(conversation, message); + } + + public void markReadUpTo(final Conversation conversation, final Message message) { + final boolean isDismissNotification = isDismissNotification(message); + final var uuid = message.getUuid(); + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": mark " + + conversation.getJid().asBareJid() + + " as read up to " + + uuid); + markRead(conversation, uuid, isDismissNotification); + } + + private static boolean isDismissNotification(final Message message) { + Message next = message.next(); + while (next != null) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + return false; + } + next = next.next(); + } + return true; + } + public void processBookmarksInitial(Account account, Map bookmarks, final boolean pep) { final Set previousBookmarks = account.getBookmarkedJids(); final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); @@ -2050,7 +2131,7 @@ public class XmppConnectionService extends Service { } }); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + retry + ") " + response); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing "+node+" (retry=" + retry + ") " + response); } }); } @@ -4534,24 +4615,101 @@ public class XmppConnectionService extends Service { } } - public void sendReadMarker(final Conversation conversation, String upToUuid) { - final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous(); + public void sendReadMarker(final Conversation conversation, final String upToUuid) { + final boolean isPrivateAndNonAnonymousMuc = + conversation.getMode() == Conversation.MODE_MULTI + && conversation.isPrivateAndNonAnonymous(); final List readMessages = this.markRead(conversation, upToUuid, true); - if (readMessages.size() > 0) { - updateConversationUi(); + if (readMessages.isEmpty()) { + return; } - final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc); - if (confirmMessages() - && markable != null - && (markable.trusted() || isPrivateAndNonAnonymousMuc) - && markable.getRemoteMsgId() != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - final Account account = conversation.getAccount(); - final MessagePacket packet = mMessageGenerator.confirm(markable); + final var account = conversation.getAccount(); + final var connection = account.getXmppConnection(); + updateConversationUi(); + final var last = + Iterables.getLast( + Collections2.filter( + readMessages, + m -> + !m.isPrivateMessage() + && m.getStatus() == Message.STATUS_RECEIVED), + null); + if (last == null) { + return; + } + + final boolean sendDisplayedMarker = + confirmMessages() + && (last.trusted() || isPrivateAndNonAnonymousMuc) + && last.getRemoteMsgId() != null + && (last.markable || isPrivateAndNonAnonymousMuc); + final boolean serverAssist = + connection != null && connection.getFeatures().mdsServerAssist(); + + final String stanzaId = last.getServerMsgId(); + + if (sendDisplayedMarker && serverAssist) { + final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation); + final MessagePacket packet = mMessageGenerator.confirm(last); + packet.addChild(mdsDisplayed); + if (!last.isPrivateMessage()) { + packet.setTo(packet.getTo().asBareJid()); + } + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server assisted "+packet); this.sendMessagePacket(account, packet); + } else { + publishMds(last); + // read markers will be sent after MDS to flush the CSI stanza queue + if (sendDisplayedMarker) { + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": sending displayed marker to " + + last.getCounterpart().toString()); + final MessagePacket packet = mMessageGenerator.confirm(last); + this.sendMessagePacket(account, packet); + } } } + private void publishMds(@Nullable final Message message) { + final String stanzaId = message == null ? null : message.getServerMsgId(); + if (Strings.isNullOrEmpty(stanzaId)) { + return; + } + final Conversation conversation; + final var conversational = message.getConversation(); + if (conversational instanceof Conversation c) { + conversation = c; + } else { + return; + } + final var account = conversation.getAccount(); + final var connection = account.getXmppConnection(); + if (connection == null || !connection.getFeatures().mds()) { + return; + } + final Jid itemId; + if (message.isPrivateMessage()) { + itemId = message.getCounterpart(); + } else { + itemId = conversation.getJid().asBareJid(); + } + Log.d(Config.LOGTAG,"publishing mds for "+itemId+"/"+stanzaId); + publishMds(account, itemId, stanzaId, conversation); + } + + private void publishMds( + final Account account, final Jid itemId, final String stanzaId, final Conversation conversation) { + final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation); + pushNodeAndEnforcePublishOptions( + account, + Namespace.MDS_DISPLAYED, + item, + itemId.toEscapedString(), + PublishOptions.persistentWhitelistAccessMaxItems()); + } + public MemorizingTrustManager getMemorizingTrustManager() { return this.mMemorizingTrustManager; } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 85714c765..2e5eb1c5a 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -24,6 +24,7 @@ public final class Namespace { public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; + public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String NICK = "http://jabber.org/protocol/nick"; @@ -76,4 +77,6 @@ public final class Namespace { public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264"; public static final String HASHES = "urn:xmpp:hashes:2"; + public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; + public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index cba12e19e..b29129ee2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2968,6 +2968,10 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS); } + public boolean pepConfigNodeMax() { + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX); + } + public boolean pepOmemoWhitelisted() { return hasDiscoFeature( account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); @@ -3068,5 +3072,13 @@ public class XmppConnection implements Runnable { public boolean externalServiceDiscovery() { return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY); } + + public boolean mds() { + return pepPublishOptions() && pepConfigNodeMax(); + } + + public boolean mdsServerAssist() { + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED); + } } } 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 24b429fd7..ef1da8561 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -31,7 +31,6 @@ public class PublishOptions { options.putString("pubsub#access_model", "whitelist"); options.putString("pubsub#send_last_published_item", "never"); options.putString("pubsub#max_items", "max"); - options.putString("pubsub#notify_delete", "true"); options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract From 6b37b6377bd77dd57364122f884b7bf04dac4234 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 27 Mar 2024 14:11:20 +0100 Subject: [PATCH 280/556] remember mds display state until after mam catchup --- .../eu/siacs/conversations/entities/Conversation.java | 9 +++++++++ .../conversations/services/MessageArchiveService.java | 10 +++++++++- .../conversations/services/XmppConnectionService.java | 9 ++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 587bfc496..223a48efa 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -85,6 +85,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; private String mFirstMamReference = null; + private String displayState = null; public Conversation(final String name, final Account account, final Jid contactJid, final int mode) { @@ -1121,6 +1122,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return getName().toString(); } + public void setDisplayState(final String stanzaId) { + this.displayState = stanzaId; + } + + public String getDisplayState() { + return this.displayState; + } + public interface OnMessageFound { void onMessageFound(final Message message); } diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index e74af3773..afa7f79ef 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -282,10 +282,18 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { if (conversation != null) { conversation.sort(); conversation.setHasMessagesLeftOnServer(!done); + final var displayState = conversation.getDisplayState(); + if (displayState != null) { + mXmppConnectionService.markReadUpToStanzaId(conversation, displayState); + } } else { - for (Conversation tmp : this.mXmppConnectionService.getConversations()) { + for (final Conversation tmp : this.mXmppConnectionService.getConversations()) { if (tmp.getAccount() == query.getAccount()) { tmp.sort(); + final var displayState = tmp.getDisplayState(); + if (displayState != null) { + mXmppConnectionService.markReadUpToStanzaId(tmp, displayState); + } } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index bd9363696..a581926b8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1952,13 +1952,13 @@ public class XmppConnectionService extends Service { if (jid == null) { return; } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing mds item for " + jid); 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 Conversation conversation = find(account, jid); if (id != null && conversation != null) { + conversation.setDisplayState(id); markReadUpToStanzaId(conversation, id); } } @@ -3381,7 +3381,7 @@ public class XmppConnectionService extends Service { new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start(); } - public void persistSelfNick(MucOptions.User self) { + public void persistSelfNick(final MucOptions.User self) { final Conversation conversation = self.getConversation(); final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark(); Jid full = self.getFullJid(); @@ -3393,11 +3393,10 @@ public class XmppConnectionService extends Service { final Bookmark bookmark = conversation.getBookmark(); final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); - if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) { + if (bookmark != null && (tookProposedNickFromBookmark || Strings.isNullOrEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) { final Account account = conversation.getAccount(); final String defaultNick = MucOptions.defaultNick(account); - if (TextUtils.isEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not overwrite empty bookmark nick with default nick for " + conversation.getJid().asBareJid()); + if (Strings.isNullOrEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { return; } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid()); From fbfb6c803fa3f844f8b5c62ee96d2ebec0222dbf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 27 Mar 2024 14:43:34 +0100 Subject: [PATCH 281/556] catch early exception in video transcoder --- .../services/AttachFileToConversationRunnable.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 1ddee27b5..5889ba333 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -99,17 +99,25 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis final boolean highQuality = "720".equals(getVideoCompression()); - final Future future = Transcoder.into(file.getAbsolutePath()). + final Future future; + try { + future = Transcoder.into(file.getAbsolutePath()). addDataSource(mXmppConnectionService, uri) .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P) .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ) .setListener(this) .transcode(); + } catch (final RuntimeException e) { + // transcode can already throw if there is an invalid file format or a platform bug + mXmppConnectionService.stopForcingForegroundNotification(); + processAsFile(); + return; + } try { future.get(); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { throw new AssertionError(e); - } catch (ExecutionException e) { + } catch (final ExecutionException e) { if (e.getCause() instanceof Error) { mXmppConnectionService.stopForcingForegroundNotification(); processAsFile(); From 378efe1a8a1207bdac436ad18ec29c089e8ec2c8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 27 Mar 2024 14:44:17 +0100 Subject: [PATCH 282/556] do not crash when error notification comes after service has shut down --- .../services/NotificationService.java | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index fce5c3c49..041f8185f 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.services; import static eu.siacs.conversations.utils.Compatibility.s; +import android.Manifest; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -10,6 +11,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.graphics.Bitmap; @@ -29,6 +31,7 @@ import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.BigPictureStyle; import androidx.core.app.NotificationCompat.Builder; @@ -1694,8 +1697,7 @@ public class NotificationService { } private boolean wasHighlightedOrPrivate(final Message message) { - if (message.getConversation() instanceof Conversation) { - Conversation conversation = (Conversation) message.getConversation(); + if (message.getConversation() instanceof Conversation conversation) { final String nick = conversation.getMucOptions().getActualNick(); final Pattern highlight = generateNickHighlightPattern(nick); if (message.getBody() == null || nick == null) { @@ -1825,10 +1827,17 @@ public class NotificationService { } } if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) { - notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); + try { + notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + "not refreshing foreground service notification because service has died", + e); + } } final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - if (errors.size() == 0) { + if (errors.isEmpty()) { cancel(ERROR_NOTIFICATION_ID); return; } else if (errors.size() == 1) { @@ -1840,10 +1849,23 @@ public class NotificationService { mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); } - mBuilder.addAction( - R.drawable.ic_autorenew_white_24dp, - mXmppConnectionService.getString(R.string.try_again), - pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45)); + try { + mBuilder.addAction( + R.drawable.ic_autorenew_white_24dp, + mXmppConnectionService.getString(R.string.try_again), + pendingServiceIntent( + mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45)); + mBuilder.setDeleteIntent( + pendingServiceIntent( + mXmppConnectionService, + XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, + 69)); + } catch (final RuntimeException e) { + Log.d( + Config.LOGTAG, + "not including some actions in error notification because service has died", + e); + } if (torNotAvailable) { if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) { mBuilder.addAction( @@ -1871,7 +1893,6 @@ public class NotificationService { : PendingIntent.FLAG_UPDATE_CURRENT)); } } - mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69)); mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); mBuilder.setLocalOnly(true); @@ -1912,27 +1933,37 @@ public class NotificationService { notify(FOREGROUND_NOTIFICATION_ID, notification); } - private void notify(String tag, int id, Notification notification) { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(mXmppConnectionService); + private void notify(final String tag, final int id, final Notification notification) { + if (ActivityCompat.checkSelfPermission( + mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + return; + } + final var notificationManager = + mXmppConnectionService.getSystemService(NotificationManager.class); try { notificationManager.notify(tag, id, notification); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to make notification", e); } } - public void notify(int id, Notification notification) { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(mXmppConnectionService); + public void notify(final int id, final Notification notification) { + if (ActivityCompat.checkSelfPermission( + mXmppConnectionService, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + return; + } + final var notificationManager = + mXmppConnectionService.getSystemService(NotificationManager.class); try { notificationManager.notify(id, notification); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to make notification", e); } } - public void cancel(int id) { + public void cancel(final int id) { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); try { From 26dde5370a8b6268831caff5d60b98a34ac44542 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 28 Mar 2024 13:53:29 +0100 Subject: [PATCH 283/556] use distinct notification id for video transcoder --- .../AttachFileToConversationRunnable.java | 12 ++--- .../services/NotificationService.java | 39 ++++++++++---- .../services/XmppConnectionService.java | 52 ++++++++++++------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 5889ba333..5df423502 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -90,7 +90,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis private void processAsVideo() throws FileNotFoundException { Log.d(Config.LOGTAG, "processing file as video"); - mXmppConnectionService.startForcingForegroundNotification(); + mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification(); mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4")); final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { @@ -109,7 +109,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis .transcode(); } catch (final RuntimeException e) { // transcode can already throw if there is an invalid file format or a platform bug - mXmppConnectionService.stopForcingForegroundNotification(); + mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); processAsFile(); return; } @@ -119,7 +119,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis throw new AssertionError(e); } catch (final ExecutionException e) { if (e.getCause() instanceof Error) { - mXmppConnectionService.stopForcingForegroundNotification(); + mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); processAsFile(); } else { Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e); @@ -138,7 +138,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis @Override public void onTranscodeCompleted(int successCode) { - mXmppConnectionService.stopForcingForegroundNotification(); + mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); final File file = mXmppConnectionService.getFileBackend().getFile(message); long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize(); Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize); @@ -162,13 +162,13 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis @Override public void onTranscodeCanceled() { - mXmppConnectionService.stopForcingForegroundNotification(); + mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); processAsFile(); } @Override public void onTranscodeFailed(@NonNull @NotNull Throwable exception) { - mXmppConnectionService.stopForcingForegroundNotification(); + mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); Log.d(Config.LOGTAG, "video transcoding failed", exception); processAsFile(); } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 041f8185f..e054fe54a 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -30,6 +30,7 @@ import android.text.style.StyleSpan; import android.util.DisplayMetrics; import android.util.Log; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; @@ -108,6 +109,7 @@ public class NotificationService { public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13; + public static final int ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); @@ -1919,18 +1921,33 @@ public class NotificationService { notify(ERROR_NOTIFICATION_ID, mBuilder.build()); } - void updateFileAddingNotification(int current, Message message) { - Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video)); - mBuilder.setProgress(100, current, false); - mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp); - mBuilder.setContentIntent(createContentIntent(message.getConversation())); - mBuilder.setOngoing(true); - if (Compatibility.runsTwentySix()) { - mBuilder.setChannelId("compression"); + void updateFileAddingNotification(final int current, final Message message) { + + final Notification notification = videoTranscoding(current, message); + notify(ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID, notification); + } + + private Notification videoTranscoding(final int current, @Nullable final Message message) { + final Notification.Builder builder = new Notification.Builder(mXmppConnectionService); + builder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video)); + if (current >= 0) { + builder.setProgress(100, current, false); + } else { + builder.setProgress(100, 0, true); } - Notification notification = mBuilder.build(); - notify(FOREGROUND_NOTIFICATION_ID, notification); + builder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp); + if (message != null) { + builder.setContentIntent(createContentIntent(message.getConversation())); + } + builder.setOngoing(true); + if (Compatibility.runsTwentySix()) { + builder.setChannelId("compression"); + } + return builder.build(); + } + + public Notification getIndeterminateVideoTranscoding() { + return videoTranscoding(-1, null); } private void notify(final String tag, final int id, final Notification notification) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a581926b8..4bc85a016 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -243,7 +243,7 @@ public class XmppConnectionService extends Service { private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this); private final ShortcutService mShortcutService = new ShortcutService(this); private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); - private final AtomicBoolean mForceForegroundService = new AtomicBoolean(false); + private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false); private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); private final AtomicReference ongoingCall = new AtomicReference<>(); private final OnMessagePacketReceived mMessageParser = new MessageParser(this); @@ -526,13 +526,13 @@ public class XmppConnectionService extends Service { } } - public void startForcingForegroundNotification() { - mForceForegroundService.set(true); + public void startOngoingVideoTranscodingForegroundNotification() { + mOngoingVideoTranscoding.set(true); toggleForegroundService(); } - public void stopForcingForegroundNotification() { - mForceForegroundService.set(false); + public void stopOngoingVideoTranscodingForegroundNotification() { + mOngoingVideoTranscoding.set(false); toggleForegroundService(); } @@ -1467,35 +1467,47 @@ public class XmppConnectionService extends Service { private void toggleForegroundService(final boolean force) { final boolean status; final OngoingCall ongoing = ongoingCall.get(); - if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || ongoing != null || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { + final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get(); + final int id; + if (force + || mForceDuringOnCreate.get() + || ongoingVideoTranscoding + || ongoing != null + || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { final Notification notification; - final int id; if (ongoing != null) { notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; startForegroundOrCatch(id, notification, true); - mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); + } else if (ongoingVideoTranscoding) { + notification = this.mNotificationService.getIndeterminateVideoTranscoding(); + id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID; + startForegroundOrCatch(id, notification, false); } else { notification = this.mNotificationService.createForegroundNotification(); id = NotificationService.FOREGROUND_NOTIFICATION_ID; startForegroundOrCatch(id, notification, false); } - - if (!mForceForegroundService.get()) { - mNotificationService.notify(id, notification); - } + mNotificationService.notify(id, notification); status = true; } else { + id = 0; stopForeground(true); status = false; } - if (!mForceForegroundService.get()) { - mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); + + for (final int toBeRemoved : + Collections2.filter( + Arrays.asList( + NotificationService.FOREGROUND_NOTIFICATION_ID, + NotificationService.ONGOING_CALL_NOTIFICATION_ID, + NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID), + i -> i != id)) { + mNotificationService.cancel(toBeRemoved); } - if (ongoing == null) { - mNotificationService.cancel(NotificationService.ONGOING_CALL_NOTIFICATION_ID); - } - Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); + Log.d( + Config.LOGTAG, + "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id); } private void startForegroundOrCatch( @@ -1531,13 +1543,13 @@ public class XmppConnectionService extends Service { } public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { - return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); + return !mOngoingVideoTranscoding.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); } @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get() || ongoingCall.get() != null) { + if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mOngoingVideoTranscoding.get() || ongoingCall.get() != null) { Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated"); } else { this.logoutAndSave(false); From 283f363088349c4709fc72d018b18345f2b53e8f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 29 Mar 2024 06:55:24 +0100 Subject: [PATCH 284/556] use voice call stream to play connect sound --- .../services/AppRTCAudioManager.java | 2 +- .../services/CallIntegration.java | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index d9eaeee48..0aa0c0cd0 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -466,7 +466,7 @@ public class AppRTCAudioManager { final var toneGenerator = new ToneGenerator( AudioManager.STREAM_MUSIC, - CallIntegration.DEFAULT_VOLUME); + CallIntegration.DEFAULT_TONE_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); }, 0, diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 67ea605f5..93b25e25f 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.services; import android.content.Context; import android.content.pm.PackageManager; +import android.media.AudioAttributes; import android.media.AudioManager; import android.media.ToneGenerator; import android.net.Uri; @@ -35,7 +36,8 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CallIntegration extends Connection { - public static final int DEFAULT_VOLUME = 80; + public static final int DEFAULT_TONE_VOLUME = 60; + private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90; private final Context context; @@ -338,14 +340,25 @@ public class CallIntegration extends Connection { } private void playConnectedSound() { - final var mediaPlayer = MediaPlayer.create(context, R.raw.connected); - mediaPlayer.setVolume(DEFAULT_VOLUME / 100f, DEFAULT_VOLUME / 100f); + final var audioAttributes = + new AudioAttributes.Builder() + .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) + .build(); + final var mediaPlayer = + MediaPlayer.create( + context, + R.raw.connected, + audioAttributes, + AudioManager.AUDIO_SESSION_ID_GENERATE); + mediaPlayer.setVolume( + DEFAULT_MEDIA_PLAYER_VOLUME / 100f, DEFAULT_MEDIA_PLAYER_VOLUME / 100f); mediaPlayer.start(); } public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, DEFAULT_VOLUME); + final var toneGenerator = + new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); } @@ -361,7 +374,8 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, DEFAULT_VOLUME); + final var toneGenerator = + new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); } @@ -379,7 +393,7 @@ public class CallIntegration extends Connection { public void busy() { Log.d(Config.LOGTAG, "CallIntegration.busy()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80); + final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80); toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500); } From c415e7d1d52cbdf1659ec1773dd2119ec4ea0b46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 29 Mar 2024 15:59:53 +0100 Subject: [PATCH 285/556] add 0490 (mds) to doap file --- conversations.doap | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/conversations.doap b/conversations.doap index 486baed40..231859b2b 100644 --- a/conversations.doap +++ b/conversations.doap @@ -481,12 +481,19 @@ 0.1.0 + + + + complete + 0.1.0 + + - 2.13.4 - 2024-02-20 - + 2.14.0 + 2024-03-22 + From 14fb9035226a0e0e2d5daf8a5a6b48d10c2625f4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 29 Mar 2024 21:22:53 +0100 Subject: [PATCH 286/556] use fake 'tel:0' address on Android 8 --- .../CallIntegrationConnectionService.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index d47a28a22..b7cf00474 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -40,6 +40,7 @@ import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -52,6 +53,9 @@ import java.util.concurrent.TimeoutException; public class CallIntegrationConnectionService extends ConnectionService { + private static final String EXTRA_ADDRESS = "eu.siacs.conversations.address"; + private static final String EXTRA_SESSION_ID = "eu.siacs.conversations.sid"; + private static final ExecutorService ACCOUNT_REGISTRATION_EXECUTOR = Executors.newSingleThreadExecutor(); @@ -160,8 +164,17 @@ public class CallIntegrationConnectionService extends ConnectionService { final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) { Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")"); final var uri = request.getAddress(); - final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); final var extras = request.getExtras(); + if (uri == null || !Arrays.asList("xmpp", "tel").contains(uri.getScheme())) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR, "invalid address")); + } + final Jid jid; + if ("tel".equals(uri.getScheme())) { + jid = Jid.ofEscaped(extras.getString(EXTRA_ADDRESS)); + } else { + jid = Jid.ofEscaped(uri.getSchemeSpecificPart()); + } final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE); final Set media = videoState == VideoProfile.STATE_AUDIO_ONLY @@ -183,7 +196,7 @@ public class CallIntegrationConnectionService extends ConnectionService { final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS); final String incomingCallAddress = extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS); - final String sid = extraExtras == null ? null : extraExtras.getString("sid"); + final String sid = extraExtras == null ? null : extraExtras.getString(EXTRA_SESSION_ID); Log.d(Config.LOGTAG, "sid " + sid); final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress); Log.d(Config.LOGTAG, "uri=" + uri); @@ -329,9 +342,19 @@ public class CallIntegrationConnectionService extends ConnectionService { .show(); return; } + final Uri address; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Android 9+ supports putting xmpp uris into the address + address = CallIntegration.address(with); + } else { + // for Android 8 we need to put in a fake tel uri + final var outgoingCallExtras = new Bundle(); + outgoingCallExtras.putString(EXTRA_ADDRESS, with.toEscapedString()); + extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingCallExtras); + address = Uri.parse("tel:0"); + } try { - service.getSystemService(TelecomManager.class) - .placeCall(CallIntegration.address(with), extras); + service.getSystemService(TelecomManager.class).placeCall(address, extras); } catch (final SecurityException e) { Toast.makeText(service, R.string.call_integration_not_available, Toast.LENGTH_LONG) .show(); @@ -363,7 +386,7 @@ public class CallIntegrationConnectionService extends ConnectionService { TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, CallIntegration.address(id.with).toString()); final var extras = new Bundle(); - extras.putString("sid", id.sessionId); + extras.putString(EXTRA_SESSION_ID, id.sessionId); bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras); try { context.getSystemService(TelecomManager.class) From c527e763377e6957054c483ecf4c714ad8c80299 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 30 Mar 2024 08:48:09 +0100 Subject: [PATCH 287/556] parse invalid jingle actions --- .../xmpp/jingle/JingleConnectionManager.java | 20 +++++++++++-------- .../xmpp/jingle/stanzas/JinglePacket.java | 11 ++++++++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index b70463eb0..c94ed0ab1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -76,15 +76,20 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void deliverPacket(final Account account, final JinglePacket packet) { final String sessionId = packet.getSessionId(); + final JinglePacket.Action action = packet.getAction(); if (sessionId == null) { respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); return; } + if (action == null) { + respondWithJingleError(account, packet, null, "bad-request", "cancel"); + return; + } final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { existingJingleConnection.deliverPacket(packet); - } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { + } else if (action == JinglePacket.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); final Content content = packet.getJingleContent(); final String descriptionNamespace = @@ -153,7 +158,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.integrationFailure(); } - private void sendSessionTerminate(final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { + private void sendSessionTerminate( + final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { mXmppConnectionService.sendIqPacket( account, request.generateResponse(IqPacket.TYPE.RESULT), null); final JinglePacket sessionTermination = @@ -255,9 +261,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { void respondWithJingleError( final Account account, final IqPacket original, - String jingleCondition, - String condition, - String conditionType) { + final String jingleCondition, + final String condition, + final String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); @@ -1171,8 +1177,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void onCallIntegrationAnswer() {} @Override - public void onCallIntegrationSilence() { - - } + public void onCallIntegrationSilence() {} } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 82c5b155c..a24040d3d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -142,8 +142,15 @@ public class JinglePacket extends IqPacket { TRANSPORT_REPLACE; public static Action of(final String value) { - // TODO handle invalid - return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + if (Strings.isNullOrEmpty(value)) { + return null; + } + try { + return Action.valueOf( + CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + } catch (final IllegalArgumentException e) { + return null; + } } @Override From cb37321ecb39f04aa0c3b7f19010938304d11b3c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 30 Mar 2024 11:15:41 +0100 Subject: [PATCH 288/556] rtp session propsoal: fix race condition with very fast 'busy' or 'error' --- .../CallIntegrationConnectionService.java | 1 + .../conversations/ui/RtpSessionActivity.java | 22 ++++++-- .../xmpp/jingle/JingleConnectionManager.java | 52 +++++++++++++++---- .../xmpp/jingle/JingleRtpConnection.java | 10 ++-- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index b7cf00474..e86f6c4f8 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -131,6 +131,7 @@ public class CallIntegrationConnectionService extends ConnectionService { intent.putExtra( RtpSessionActivity.EXTRA_LAST_REPORTED_STATE, RtpEndUserState.FINDING_DEVICE.toString()); + intent.putExtra(RtpSessionActivity.EXTRA_PROPOSED_SESSION_ID, proposal.sessionId); callIntegration = proposal.getCallIntegration(); } if (Media.audioOnly(media)) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 793ef16f9..871b7c259 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -80,6 +80,7 @@ public class RtpSessionActivity extends XmppActivity public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; + public static final String EXTRA_PROPOSED_SESSION_ID = "proposed_session_id"; public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; public static final String EXTRA_LAST_ACTION = "last_action"; public static final String ACTION_ACCEPT_CALL = "action_accept_call"; @@ -386,7 +387,6 @@ public class RtpSessionActivity extends XmppActivity } } - private void putScreenInCallMode() { putScreenInCallMode(requireRtpConnection().getMedia()); } @@ -509,6 +509,16 @@ public class RtpSessionActivity extends XmppActivity proposeJingleRtpSession(account, with, actionToMedia(action)); setWith(account.getRoster().getContact(with), null); } else if (Intent.ACTION_VIEW.equals(action)) { + final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID); + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = + xmppConnectionService + .getJingleConnectionManager() + .getTerminalSessionState(with, proposedSessionId); + if (terminatedRtpSession != null) { + // termination (due to message error or 'busy' was faster than opening the activity + initializeWithTerminatedSessionState(account, with, terminatedRtpSession); + return; + } final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); final Set media = actionToMedia(lastAction); @@ -1007,7 +1017,7 @@ public class RtpSessionActivity extends XmppActivity private void updateInCallButtonConfiguration( final RtpEndUserState state, final Set media) { if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { - Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); updateInCallButtonConfigurationVideo( @@ -1028,7 +1038,13 @@ public class RtpSessionActivity extends XmppActivity } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state) && !isPictureInPicture() && Media.audioOnly(media)) { - final CallIntegration callIntegration = requireCallIntegration(); + final CallIntegration callIntegration; + try { + callIntegration = requireCallIntegration(); + } catch (final IllegalStateException e) { + Log.e(Config.LOGTAG, "can not update InCallButtonConfiguration in state " + state); + return; + } updateInCallButtonConfigurationSpeaker( callIntegration.getSelectedAudioDevice(), callIntegration.getAudioDevices().size()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index c94ed0ab1..ce3628697 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -505,6 +505,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (proposal != null) { + setTerminalSessionState(proposal, RtpEndUserState.DECLINED_OR_BUSY); rtpSessionProposals.remove(proposal); proposal.callIntegration.busy(); writeLogMissedOutgoing( @@ -938,7 +939,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); if (currentState == null) { - Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); + Log.d( + Config.LOGTAG, + "unable to find session proposal for session id " + + sessionId + + " target=" + + target); return; } if (currentState == DeviceDiscoveryState.DISCOVERED) { @@ -947,14 +953,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { "session proposal already at discovered. not going to fall back"); return; } - this.rtpSessionProposals.put(sessionProposal, target); - final RtpEndUserState endUserState = target.toEndUserState(); - if (endUserState == RtpEndUserState.RINGING) { - sessionProposal.callIntegration.setDialing(); - } - // toneManager.transition(endUserState, sessionProposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, sessionProposal.with, sessionProposal.sessionId, endUserState); + Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -962,6 +961,30 @@ public class JingleConnectionManager extends AbstractConnectionManager { + sessionId + " as " + target); + + final RtpEndUserState endUserState = target.toEndUserState(); + + if (target == DeviceDiscoveryState.FAILED) { + Log.d(Config.LOGTAG, "removing session proposal after failure"); + setTerminalSessionState(sessionProposal, endUserState); + this.rtpSessionProposals.remove(sessionProposal); + sessionProposal.getCallIntegration().error(); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + sessionProposal.with, + sessionProposal.sessionId, + endUserState); + return; + } + + this.rtpSessionProposals.put(sessionProposal, target); + + if (endUserState == RtpEndUserState.RINGING) { + sessionProposal.callIntegration.setDialing(); + } + + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, sessionProposal.with, sessionProposal.sessionId, endUserState); } } @@ -1020,6 +1043,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); } + void setTerminalSessionState(final RtpSessionProposal proposal, final RtpEndUserState state) { + this.terminatedSessions.put( + PersistableSessionId.of(proposal), new TerminatedRtpSession(state, proposal.media)); + } + public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) { return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId)); } @@ -1033,10 +1061,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.sessionId = sessionId; } - public static PersistableSessionId of(AbstractJingleConnection.Id id) { + public static PersistableSessionId of(final AbstractJingleConnection.Id id) { return new PersistableSessionId(id.with, id.sessionId); } + public static PersistableSessionId of(final RtpSessionProposal proposal) { + return new PersistableSessionId(proposal.with, proposal.sessionId); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ab9659185..bb7c70839 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2708,14 +2708,14 @@ public class JingleRtpConnection extends AbstractJingleConnection } @Override - public void onCallIntegrationShowIncomingCallUi() { + public synchronized void onCallIntegrationShowIncomingCallUi() { if (isTerminated()) { // there might be race conditions with the call integration service invoking this - // callback when the rtp session has already ended. It should be enough to just return - // instead of throwing an exception. however throwing an exception gives us a sense of - // if and how frequently this happens - throw new IllegalStateException( + // callback when the rtp session has already ended. + Log.w( + Config.LOGTAG, "CallIntegration requested incoming call UI but session was already terminated"); + return; } // TODO apparently this can be called too early as well? xmppConnectionService.getNotificationService().startRinging(id, getMedia()); From f22d19f7d07733c524730627b1dca72a7bdfaafb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 30 Mar 2024 14:54:28 +0100 Subject: [PATCH 289/556] disable mds for bug fix release --- src/main/java/eu/siacs/conversations/Config.java | 2 ++ .../conversations/generator/AbstractGenerator.java | 10 ++++++---- .../eu/siacs/conversations/parser/MessageParser.java | 10 ++++++++-- .../eu/siacs/conversations/xmpp/XmppConnection.java | 4 +++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index da3628b6f..06434a20d 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -56,6 +56,8 @@ public final class Config { public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false; + public static final boolean MESSAGE_DISPLAYED_SYNCHRONIZATION = false; + public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; // very dangerous. you should have a good reason to set this to true diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 3f63edbd7..905b08286 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -24,7 +24,7 @@ import java.util.TimeZone; public abstract class AbstractGenerator { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - private final String[] FEATURES = { + private final String[] STATIC_FEATURES = { Namespace.JINGLE, Namespace.JINGLE_APPS_FILE_TRANSFER, Namespace.JINGLE_TRANSPORTS_S5B, @@ -40,8 +40,7 @@ public abstract class AbstractGenerator { Namespace.NICK + "+notify", "urn:xmpp:ping", "jabber:iq:version", - "http://jabber.org/protocol/chatstates", - Namespace.MDS_DISPLAYED + "+notify" + "http://jabber.org/protocol/chatstates" }; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" @@ -108,7 +107,10 @@ public abstract class AbstractGenerator { public List getFeatures(Account account) { final XmppConnection connection = account.getXmppConnection(); - final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + final ArrayList features = new ArrayList<>(Arrays.asList(STATIC_FEATURES)); + if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) { + features.add(Namespace.MDS_DISPLAYED + "+notify"); + } if (mXmppConnectionService.confirmMessages()) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index ece8a7862..a949b088d 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -271,11 +271,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.updateConversationUi(); } } - } else if (Namespace.MDS_DISPLAYED.equals(node) && account.getJid().asBareJid().equals(from)) { + } 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); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " received pubsub notification for node=" + + node); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b29129ee2..58f9e5644 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3074,7 +3074,9 @@ public class XmppConnection implements Runnable { } public boolean mds() { - return pepPublishOptions() && pepConfigNodeMax(); + return pepPublishOptions() + && pepConfigNodeMax() + && Config.MESSAGE_DISPLAYED_SYNCHRONIZATION; } public boolean mdsServerAssist() { From 429b190b8e7fb09fd9a24c7deb86779df96bb0bb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 30 Mar 2024 15:00:32 +0100 Subject: [PATCH 290/556] version bump to 2.14.1 + changelog --- CHANGELOG.md | 6 ++++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4210404.txt | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4210404.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd607ce7..bcd66cb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### Version 2.14.1 + +* Fix A/V calls on Android 8 +* Fix race conditions in new call integration +* Fix video compression sticking around + ### Version 2.14.0 * Improve integration of A/V calls into the operating system diff --git a/build.gradle b/build.gradle index d7d71d99c..70cecf825 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42103 - versionName "2.14.0" + versionCode 42104 + versionName "2.14.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4210404.txt b/fastlane/metadata/android/en-US/changelogs/4210404.txt new file mode 100644 index 000000000..0715795ad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4210404.txt @@ -0,0 +1,3 @@ +* Fix A/V calls on Android 8 +* Fix race conditions in new call integration +* Fix video compression sticking around From 0e9f4e5265e61796cbad7f6e91a8690bf3f43bca Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 3 Apr 2024 08:33:21 +0200 Subject: [PATCH 291/556] Channel discovery service / okttp needs bundled letsencrypt too --- .../conversations/crypto/TrustManagers.java | 17 +++++ .../services/ChannelDiscoveryService.java | 66 ++++++++++++++----- .../services/MemorizingTrustManager.java | 13 +--- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java index 11fe182dd..4fc11eecf 100644 --- a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java +++ b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java @@ -1,17 +1,23 @@ package eu.siacs.conversations.crypto; +import android.content.Context; + import androidx.annotation.Nullable; import com.google.common.collect.Iterables; +import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.Arrays; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import eu.siacs.conversations.R; + public final class TrustManagers { private TrustManagers() { @@ -34,5 +40,16 @@ public final class TrustManagers { return createTrustManager(null); } + public static X509TrustManager defaultWithBundledLetsEncrypt(final Context context) + throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { + final BundledTrustManager bundleTrustManager = + BundledTrustManager.builder() + .loadKeyStore( + context.getResources().openRawResource(R.raw.letsencrypt), + "letsencrypt") + .build(); + return CombiningTrustManager.combineWithDefault(bundleTrustManager); + } + } diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 2f9553bfc..3ca6bfde4 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.services; +import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; + +import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -8,7 +11,33 @@ import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.TrustManagers; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Room; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.http.services.MuclumbusService; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.utils.TLSSocketFactory; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -18,23 +47,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.http.services.MuclumbusService; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; public class ChannelDiscoveryService { @@ -55,6 +69,24 @@ public class ChannelDiscoveryService { return; } final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); + try { + final X509TrustManager trustManager; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + trustManager = TrustManagers.defaultWithBundledLetsEncrypt(service); + } else { + trustManager = TrustManagers.createDefaultTrustManager(); + } + final SSLSocketFactory socketFactory = + new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); + builder.sslSocketFactory(socketFactory, trustManager); + } catch (final IOException + | KeyManagementException + | NoSuchAlgorithmException + | KeyStoreException + | CertificateException e) { + Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); + throw new RuntimeException(e); + } if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 81cfb951f..d05fa4ac3 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -176,7 +176,7 @@ public class MemorizingTrustManager { this.appTrustManager = getTrustManager(appKeyStore); try { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - this.defaultTrustManager = defaultWithBundledLetsEncrypt(context); + this.defaultTrustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); } else { this.defaultTrustManager = TrustManagers.createDefaultTrustManager(); } @@ -188,17 +188,6 @@ public class MemorizingTrustManager { } } - private static X509TrustManager defaultWithBundledLetsEncrypt(final Context context) - throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { - final BundledTrustManager bundleTrustManager = - BundledTrustManager.builder() - .loadKeyStore( - context.getResources().openRawResource(R.raw.letsencrypt), - "letsencrypt") - .build(); - return CombiningTrustManager.combineWithDefault(bundleTrustManager); - } - private static boolean isIp(final String server) { return server != null && (PATTERN_IPV4.matcher(server).matches() From a619cfe0d8c2bea4c3b6d1987a5f76a671e14228 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 1 Apr 2024 02:00:00 +0200 Subject: [PATCH 292/556] Add MY_PACKAGE_REPLACED to the EventReceiver This ensures that Conversations is restarted after a update without waiting for the next eu.siacs.conversations.POST_CONNECTIVITY_CHANGE from the AlarmManager --- src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 6233770df..b6e8a6457 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -145,6 +145,7 @@ android:exported="false"> + From b90906b973a5b1c79bb194adf4a504308a9e2572 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 3 Apr 2024 20:51:48 +0200 Subject: [PATCH 293/556] make RtpSessionActivity onNewIntent and onBackendConnected run through the same code --- .../ui/ConversationFragment.java | 14 +++-- .../conversations/ui/RtpSessionActivity.java | 59 +++++++------------ 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 763bc3760..ae46b8075 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -127,6 +127,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import org.jetbrains.annotations.NotNull; @@ -1556,20 +1557,25 @@ public class ConversationFragment extends XmppFragment if (ongoingRtpSession.isPresent()) { final OngoingRtpSession id = ongoingRtpSession.get(); final Intent intent = new Intent(getActivity(), RtpSessionActivity.class); + intent.setAction(Intent.ACTION_VIEW); intent.putExtra( RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); if (id instanceof AbstractJingleConnection) { - intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.getSessionId()); startActivity(intent); } else if (id instanceof JingleConnectionManager.RtpSessionProposal proposal) { - if (proposal.media.contains(Media.VIDEO)) { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); + if (Media.audioOnly(proposal.media)) { + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_ACTION, + RtpSessionActivity.ACTION_MAKE_VOICE_CALL); } else { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + intent.putExtra( + RtpSessionActivity.EXTRA_LAST_ACTION, + RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } + intent.putExtra(RtpSessionActivity.EXTRA_PROPOSED_SESSION_ID, proposal.sessionId); startActivity(intent); } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 871b7c259..97b2ccb52 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -460,6 +460,9 @@ public class RtpSessionActivity extends XmppActivity public void onNewIntent(final Intent intent) { Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()"); super.onNewIntent(intent); + if (intent == null) { + return; + } setIntent(intent); if (xmppConnectionService == null) { Log.d( @@ -467,32 +470,21 @@ public class RtpSessionActivity extends XmppActivity "RtpSessionActivity: background service wasn't bound in onNewIntent()"); return; } - final Account account = extractAccount(intent); - final String action = intent.getAction(); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); - if (sessionId != null) { - Log.d(Config.LOGTAG, "reinitializing from onNewIntent()"); - if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { - return; - } - if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { - Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); - requestPermissionsAndAcceptCall(); - resetIntent(intent.getExtras()); - } - } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { - proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with), null); - } else { - throw new IllegalStateException("received onNewIntent without sessionId"); - } + initializeWithIntent(Event.ON_NEW_INTENT, intent); } @Override void onBackendConnected() { - final Intent intent = getIntent(); + final var intent = getIntent(); + if (intent == null) { + return; + } + initializeWithIntent(Event.ON_BACKEND_CONNECTED, intent); + } + + private void initializeWithIntent(final Event event, @NonNull final Intent intent) { final String action = intent.getAction(); + Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")"); final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); @@ -505,9 +497,6 @@ public class RtpSessionActivity extends XmppActivity requestPermissionsAndAcceptCall(); resetIntent(intent.getExtras()); } - } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { - proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with), null); } else if (Intent.ACTION_VIEW.equals(action)) { final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID); final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = @@ -520,8 +509,6 @@ public class RtpSessionActivity extends XmppActivity return; } final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); - final Set media = actionToMedia(lastAction); final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); if (state != null) { @@ -541,6 +528,8 @@ public class RtpSessionActivity extends XmppActivity if (END_CARD.contains(state)) { return; } + final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); + final Set media = actionToMedia(lastAction); if (xmppConnectionService .getJingleConnectionManager() .hasMatchingProposal(account, with)) { @@ -567,19 +556,6 @@ public class RtpSessionActivity extends XmppActivity } } - private void proposeJingleRtpSession( - final Account account, final Jid with, final Set media) { - if (with.isBareJid()) { - xmppConnectionService - .getJingleConnectionManager() - .proposeJingleRtpSession(account, with, media); - } else { - throw new IllegalStateException( - "We should not be initializing direct calls from the RtpSessionActivity. Go through CallIntegrationConnectionService.placeCall instead!"); - } - putScreenInCallMode(media); - } - @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { @@ -1526,4 +1502,9 @@ public class RtpSessionActivity extends XmppActivity private static boolean emptyReference(final WeakReference weakReference) { return weakReference == null || weakReference.get() == null; } + + private enum Event { + ON_BACKEND_CONNECTED, + ON_NEW_INTENT + } } From c294a24f4e470baaea23aebd2e3d579d5b1cd1ff Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Apr 2024 09:57:01 +0200 Subject: [PATCH 294/556] add some call integration debug aids --- build.gradle | 2 +- .../CallIntegrationConnectionService.java | 1 + .../conversations/ui/AboutPreference.java | 23 ++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 70cecf825..6b36db34a 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { implementation 'com.google.guava:guava:32.1.3-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.28' - implementation 'im.conversations.webrtc:webrtc-android:119.0.0' + implementation 'im.conversations.webrtc:webrtc-android:119.0.1' } ext { diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index e86f6c4f8..ce4d9fb8a 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -357,6 +357,7 @@ public class CallIntegrationConnectionService extends ConnectionService { try { service.getSystemService(TelecomManager.class).placeCall(address, extras); } catch (final SecurityException e) { + Log.e(Config.LOGTAG, "call integration not available", e); Toast.makeText(service, R.string.call_integration_not_available, Toast.LENGTH_LONG) .show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java index f2cd1e348..acfa0be8c 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java @@ -2,26 +2,28 @@ package eu.siacs.conversations.ui; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.preference.Preference; import android.util.AttributeSet; +import com.google.common.base.Strings; + import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.R; -import eu.siacs.conversations.utils.PhoneHelper; public class AboutPreference extends Preference { - public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); + public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); setSummaryAndTitle(context); - } + } - public AboutPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - setSummaryAndTitle(context); - } + public AboutPreference(final Context context, final AttributeSet attrs) { + super(context, attrs); + setSummaryAndTitle(context); + } - private void setSummaryAndTitle(final Context context) { - setSummary(String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME)); + private void setSummaryAndTitle(final Context context) { + setSummary(String.format("%s%s %s (%s)", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME, im.conversations.webrtc.BuildConfig.WEBRTC_VERSION, Strings.nullToEmpty(Build.DEVICE))); setTitle(context.getString(R.string.title_activity_about_x, BuildConfig.APP_NAME)); } @@ -32,4 +34,3 @@ public class AboutPreference extends Preference { getContext().startActivity(intent); } } - From 4968bde77468b609298cd179341c36cda12c0130 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Apr 2024 11:07:53 +0200 Subject: [PATCH 295/556] version bump to 2.14.2 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4210504.txt | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4210504.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd66cb96..86167b5a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.14.2 + +* Restore access to Channel Discovery for Android 6+7 +* Improve logging for failed call integration + ### Version 2.14.1 * Fix A/V calls on Android 8 diff --git a/build.gradle b/build.gradle index 6b36db34a..908b80c17 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42104 - versionName "2.14.1" + versionCode 42105 + versionName "2.14.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4210504.txt b/fastlane/metadata/android/en-US/changelogs/4210504.txt new file mode 100644 index 000000000..b4c432acc --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4210504.txt @@ -0,0 +1,2 @@ +* Restore access to Channel Discovery for Android 6+7 +* Improve logging for failed call integration From 6e432481353442d7192e94c77ba259b5e31d0ef6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 4 Apr 2024 11:38:27 +0200 Subject: [PATCH 296/556] apply Material 3 theme to all activites --- build.gradle | 2 +- src/conversations/AndroidManifest.xml | 1 + .../services/ImportBackupService.java | 4 +- .../ui/EasyOnboardingInviteActivity.java | 68 ++- .../ui/ImportBackupActivity.java | 21 +- .../conversations/ui/MagicCreateActivity.java | 141 +++--- .../ui/ManageAccountActivity.java | 109 +++-- .../conversations/ui/PickServerActivity.java | 15 +- .../ui/ShareViaAccountActivity.java | 9 - .../conversations/ui/WelcomeActivity.java | 94 ++-- .../ui/adapter/BackupFileAdapter.java | 13 +- .../drawable-hdpi/ic_unarchive_white_24dp.png | Bin 258 -> 0 bytes .../drawable-mdpi/ic_unarchive_white_24dp.png | Bin 181 -> 0 bytes .../ic_unarchive_white_24dp.png | Bin 273 -> 0 bytes .../ic_unarchive_white_24dp.png | Bin 391 -> 0 bytes .../ic_unarchive_white_24dp.png | Bin 503 -> 0 bytes .../res/layout/activity_easy_invite.xml | 32 +- .../res/layout/activity_import_backup.xml | 27 +- ...c_create.xml => activity_magic_create.xml} | 54 ++- .../res/layout/activity_pick_server.xml | 38 +- .../res/layout/activity_welcome.xml | 41 +- .../res/layout/dialog_enter_password.xml | 32 +- .../res/menu/easy_onboarding_invite.xml | 2 +- src/conversations/res/menu/manageaccounts.xml | 53 ++- src/conversations/res/menu/welcome_menu.xml | 2 +- .../res/values/colors-themed.xml | 63 +++ src/main/AndroidManifest.xml | 28 +- .../java/eu/siacs/conversations/Config.java | 2 - .../eu/siacs/conversations/Conversations.java | 67 +++ .../entities/RtpSessionStatus.java | 10 +- .../services/BarcodeProvider.java | 10 +- .../services/ExportBackupService.java | 6 +- .../services/NotificationService.java | 57 +-- .../services/XmppConnectionService.java | 3 - .../siacs/conversations/ui/AboutActivity.java | 28 +- .../AbstractSearchableListItemActivity.java | 6 +- .../conversations/ui/ActionBarActivity.java | 2 +- .../eu/siacs/conversations/ui/Activities.java | 47 ++ .../siacs/conversations/ui/BaseActivity.java | 53 +++ .../conversations/ui/BlockContactDialog.java | 4 +- .../ui/ChangePasswordActivity.java | 102 ++-- .../ui/ChannelDiscoveryActivity.java | 28 +- ...hooseAccountForProfilePictureActivity.java | 29 +- .../ui/ChooseContactActivity.java | 24 +- .../ui/ConferenceDetailsActivity.java | 47 +- .../ui/ContactDetailsActivity.java | 32 +- .../ui/ConversationActivity.java | 6 - .../ui/ConversationFragment.java | 24 +- .../ui/ConversationsActivity.java | 45 +- .../ui/ConversationsOverviewFragment.java | 12 +- .../ui/CreatePrivateGroupChatDialog.java | 12 +- .../ui/CreatePublicChannelDialog.java | 25 +- .../conversations/ui/EditAccountActivity.java | 45 +- .../conversations/ui/EnterJidDialog.java | 58 +-- .../ui/JoinConferenceDialog.java | 9 +- .../conversations/ui/LocationActivity.java | 5 +- .../ui/MediaBrowserActivity.java | 1 + .../conversations/ui/MemorizingActivity.java | 8 +- .../conversations/ui/MucUsersActivity.java | 3 +- .../siacs/conversations/ui/OmemoActivity.java | 20 +- ...ublishGroupChatProfilePictureActivity.java | 2 + .../ui/PublishProfilePictureActivity.java | 15 +- .../conversations/ui/RecordingActivity.java | 17 +- .../conversations/ui/RtpSessionActivity.java | 37 +- .../siacs/conversations/ui/ScanActivity.java | 1 - .../conversations/ui/SearchActivity.java | 18 +- .../conversations/ui/SettingsActivity.java | 90 ++-- .../ui/ShareLocationActivity.java | 39 +- .../conversations/ui/ShareWithActivity.java | 120 +++-- .../conversations/ui/ShortcutActivity.java | 2 +- .../ui/ShowLocationActivity.java | 2 + .../ui/StartConversationActivity.java | 60 ++- .../conversations/ui/TrustKeysActivity.java | 39 +- .../conversations/ui/UriHandlerActivity.java | 2 +- .../siacs/conversations/ui/XmppActivity.java | 95 ++-- .../ui/adapter/AccountAdapter.java | 26 +- .../adapter/ChannelSearchResultAdapter.java | 14 +- .../ui/adapter/ConversationAdapter.java | 115 +---- .../ui/adapter/KnownHostsAdapter.java | 61 +-- .../ui/adapter/ListItemAdapter.java | 30 +- .../ui/adapter/MediaAdapter.java | 182 ++++---- .../ui/adapter/MediaPreviewAdapter.java | 85 ++-- .../ui/adapter/MessageAdapter.java | 290 +++++++----- .../conversations/ui/adapter/UserAdapter.java | 10 +- .../ui/adapter/UserPreviewAdapter.java | 47 +- .../ui/forms/FormBooleanFieldWrapper.java | 80 ---- .../ui/forms/FormFieldFactory.java | 30 -- .../ui/forms/FormFieldWrapper.java | 93 ---- .../ui/forms/FormJidSingleFieldWrapper.java | 43 -- .../ui/forms/FormTextFieldWrapper.java | 97 ---- .../conversations/ui/forms/FormWrapper.java | 72 --- .../conversations/ui/service/AudioPlayer.java | 168 ++++--- .../conversations/ui/util/ActionBarUtil.java | 88 ---- .../conversations/ui/util/Attachment.java | 83 +++- .../ui/util/ConversationMenuConfigurator.java | 8 +- .../ui/util/MucDetailsContextMenuHelper.java | 4 +- .../ui/util/PresenceSelector.java | 8 +- .../conversations/ui/util/SendButtonTool.java | 236 ++++------ .../ui/util/StyledAttributes.java | 59 --- .../conversations/ui/util/ToolbarUtils.java | 166 +++++++ .../widget/ImmediateAutoCompleteTextView.java | 4 +- .../conversations/ui/widget/ScannerView.java | 19 +- .../ui/widget/SwipeRefreshListFragment.java | 5 +- .../ui/widget/UnreadCountCustomView.java | 9 +- .../conversations/utils/AccountUtils.java | 6 +- .../conversations/utils/ExceptionHelper.java | 7 +- .../utils/IrregularUnicodeDetector.java | 7 +- .../siacs/conversations/utils/MimeUtils.java | 2 + .../conversations/utils/StylingHelper.java | 44 +- .../conversations/utils/ThemeHelper.java | 117 ----- .../drawable-hdpi/baseline_tour_black_48.png | Bin 369 -> 0 bytes .../drawable-hdpi/baseline_tour_white_48.png | Bin 372 -> 0 bytes .../res/drawable-hdpi/date_bubble_grey.9.png | Bin 657 -> 0 bytes .../res/drawable-hdpi/date_bubble_white.9.png | Bin 689 -> 0 bytes .../ic_account_box_white_24dp.png | Bin 337 -> 0 bytes .../res/drawable-hdpi/ic_action_reply.png | Bin 462 -> 0 bytes .../res/drawable-hdpi/ic_add_white_24dp.png | Bin 223 -> 0 bytes .../drawable-hdpi/ic_android_black_48dp.png | Bin 519 -> 0 bytes .../drawable-hdpi/ic_android_white_48dp.png | Bin 536 -> 0 bytes .../ic_announcement_white_24dp.png | Bin 251 -> 0 bytes .../drawable-hdpi/ic_archive_black_48dp.png | Bin 377 -> 0 bytes .../drawable-hdpi/ic_archive_white_24dp.png | Bin 247 -> 0 bytes .../drawable-hdpi/ic_archive_white_48dp.png | Bin 390 -> 0 bytes .../res/drawable-hdpi/ic_attach_camera.png | Bin 1235 -> 0 bytes .../drawable-hdpi/ic_attach_camera_white.png | Bin 636 -> 0 bytes .../res/drawable-hdpi/ic_attach_document.png | Bin 1060 -> 0 bytes .../ic_attach_document_white.png | Bin 393 -> 0 bytes .../ic_attach_file_white_24dp.png | Bin 452 -> 0 bytes .../res/drawable-hdpi/ic_attach_location.png | Bin 1280 -> 0 bytes .../ic_attach_location_white.png | Bin 665 -> 0 bytes .../res/drawable-hdpi/ic_attach_photo.png | Bin 1117 -> 0 bytes .../drawable-hdpi/ic_attach_photo_white.png | Bin 463 -> 0 bytes .../res/drawable-hdpi/ic_attach_record.png | Bin 1233 -> 0 bytes .../drawable-hdpi/ic_attach_record_white.png | Bin 614 -> 0 bytes .../res/drawable-hdpi/ic_attach_videocam.png | Bin 277 -> 0 bytes .../ic_attach_videocam_white.png | Bin 376 -> 0 bytes .../drawable-hdpi/ic_autorenew_white_24dp.png | Bin 489 -> 0 bytes .../drawable-hdpi/ic_backup_black_48dp.png | Bin 561 -> 0 bytes .../drawable-hdpi/ic_backup_white_48dp.png | Bin 589 -> 0 bytes .../res/drawable-hdpi/ic_block_white_24dp.png | Bin 606 -> 0 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 420 -> 0 bytes .../res/drawable-hdpi/ic_book_black_48dp.png | Bin 278 -> 0 bytes .../res/drawable-hdpi/ic_book_white_48dp.png | Bin 283 -> 0 bytes .../res/drawable-hdpi/ic_call_black_24dp.png | Bin 326 -> 0 bytes .../drawable-hdpi/ic_call_end_white_48dp.png | Bin 553 -> 0 bytes .../drawable-hdpi/ic_call_made_black_18dp.png | Bin 159 -> 0 bytes .../drawable-hdpi/ic_call_made_white_18dp.png | Bin 174 -> 0 bytes .../ic_call_missed_black_18dp.png | Bin 179 -> 0 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 180 -> 0 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 180 -> 0 bytes .../ic_call_missed_white_18dp.png | Bin 191 -> 0 bytes .../ic_call_received_black_18dp.png | Bin 159 -> 0 bytes .../ic_call_received_white_18dp.png | Bin 169 -> 0 bytes .../res/drawable-hdpi/ic_call_white_24dp.png | Bin 340 -> 0 bytes .../res/drawable-hdpi/ic_call_white_48dp.png | Bin 597 -> 0 bytes .../ic_camera_alt_white_24dp.png | Bin 364 -> 0 bytes .../drawable-hdpi/ic_cancel_black_24dp.png | Bin 397 -> 0 bytes .../drawable-hdpi/ic_cancel_white_24dp.png | Bin 510 -> 0 bytes .../res/drawable-hdpi/ic_chat_white_24dp.png | Bin 168 -> 0 bytes .../res/drawable-hdpi/ic_clear_white_48dp.png | Bin 347 -> 0 bytes .../ic_cloud_download_white_24dp.png | Bin 353 -> 0 bytes .../ic_content_copy_white_24dp.png | Bin 203 -> 0 bytes .../res/drawable-hdpi/ic_crop_white_24dp.png | Bin 302 -> 0 bytes .../drawable-hdpi/ic_delete_black_24dp.png | Bin 155 -> 0 bytes .../drawable-hdpi/ic_delete_white_24dp.png | Bin 246 -> 0 bytes .../ic_description_black_48dp.png | Bin 279 -> 0 bytes .../ic_description_white_48dp.png | Bin 295 -> 0 bytes .../ic_directions_black_24dp.png | Bin 233 -> 0 bytes .../ic_directions_white_24dp.png | Bin 252 -> 0 bytes .../res/drawable-hdpi/ic_done_black_18dp.png | Bin 149 -> 0 bytes .../res/drawable-hdpi/ic_done_white_18dp.png | Bin 157 -> 0 bytes .../drawable-hdpi/ic_drafts_white_24dp.png | Bin 392 -> 0 bytes .../res/drawable-hdpi/ic_edit_black_24dp.png | Bin 202 -> 0 bytes .../res/drawable-hdpi/ic_edit_white_24dp.png | Bin 339 -> 0 bytes .../res/drawable-hdpi/ic_error_white_24dp.png | Bin 324 -> 0 bytes .../res/drawable-hdpi/ic_event_black_48dp.png | Bin 245 -> 0 bytes .../res/drawable-hdpi/ic_event_white_48dp.png | Bin 254 -> 0 bytes .../ic_file_download_white_24dp.png | Bin 277 -> 0 bytes .../ic_flip_camera_android_black_24dp.png | Bin 717 -> 0 bytes .../drawable-hdpi/ic_forward_white_24dp.png | Bin 139 -> 0 bytes .../drawable-hdpi/ic_gps_fixed_black_24dp.png | Bin 549 -> 0 bytes .../drawable-hdpi/ic_gps_fixed_white_24dp.png | Bin 546 -> 0 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 472 -> 0 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 468 -> 0 bytes .../drawable-hdpi/ic_group_add_white_24dp.png | Bin 396 -> 0 bytes .../res/drawable-hdpi/ic_group_white_24dp.png | Bin 417 -> 0 bytes .../drawable-hdpi/ic_headset_black_24dp.png | Bin 349 -> 0 bytes .../drawable-hdpi/ic_headset_black_48dp.png | Bin 586 -> 0 bytes .../drawable-hdpi/ic_headset_white_48dp.png | Bin 610 -> 0 bytes .../res/drawable-hdpi/ic_help_black_48dp.png | Bin 834 -> 0 bytes .../res/drawable-hdpi/ic_help_white_24dp.png | Bin 476 -> 0 bytes .../res/drawable-hdpi/ic_help_white_48dp.png | Bin 842 -> 0 bytes .../ic_hourglass_empty_white_24dp.png | Bin 159 -> 0 bytes .../res/drawable-hdpi/ic_image_black_48dp.png | Bin 424 -> 0 bytes .../res/drawable-hdpi/ic_image_white_48dp.png | Bin 450 -> 0 bytes .../res/drawable-hdpi/ic_input_white_24dp.png | Bin 226 -> 0 bytes .../drawable-hdpi/ic_link_off_white_24dp.png | Bin 415 -> 0 bytes .../res/drawable-hdpi/ic_link_white_24dp.png | Bin 430 -> 0 bytes .../res/drawable-hdpi/ic_lock_black_18dp.png | Bin 275 -> 0 bytes .../drawable-hdpi/ic_lock_open_white_24dp.png | Bin 400 -> 0 bytes .../res/drawable-hdpi/ic_lock_white_18dp.png | Bin 281 -> 0 bytes .../res/drawable-hdpi/ic_lock_white_24dp.png | Bin 399 -> 0 bytes .../res/drawable-hdpi/ic_mic_black_24dp.png | Bin 344 -> 0 bytes .../res/drawable-hdpi/ic_mic_black_48dp.png | Bin 581 -> 0 bytes .../drawable-hdpi/ic_mic_off_black_24dp.png | Bin 402 -> 0 bytes .../res/drawable-hdpi/ic_mic_white_48dp.png | Bin 606 -> 0 bytes .../drawable-hdpi/ic_mode_edit_black_18dp.png | Bin 264 -> 0 bytes .../drawable-hdpi/ic_mode_edit_white_18dp.png | Bin 299 -> 0 bytes .../ic_new_releases_black_24dp.png | Bin 384 -> 0 bytes .../ic_new_releases_white_24dp.png | Bin 395 -> 0 bytes .../ic_no_results_background_black.png | Bin 3757 -> 0 bytes .../ic_no_results_background_white.png | Bin 4625 -> 0 bytes .../ic_notifications_black_24dp.png | Bin 236 -> 0 bytes .../ic_notifications_none_black_24dp.png | Bin 295 -> 0 bytes .../ic_notifications_none_white80.png | Bin 574 -> 0 bytes .../ic_notifications_none_white_24dp.png | Bin 285 -> 0 bytes .../ic_notifications_off_black_24dp.png | Bin 390 -> 0 bytes .../ic_notifications_off_white80.png | Bin 762 -> 0 bytes .../ic_notifications_off_white_24dp.png | Bin 385 -> 0 bytes .../ic_notifications_paused_black_24dp.png | Bin 308 -> 0 bytes .../ic_notifications_paused_white80.png | Bin 610 -> 0 bytes .../ic_notifications_paused_white_24dp.png | Bin 309 -> 0 bytes .../ic_notifications_white80.png | Bin 471 -> 0 bytes .../ic_notifications_white_24dp.png | Bin 234 -> 0 bytes .../res/drawable-hdpi/ic_pause_black_36dp.png | Bin 123 -> 0 bytes .../res/drawable-hdpi/ic_pause_white_36dp.png | Bin 124 -> 0 bytes .../ic_person_add_white_24dp.png | Bin 383 -> 0 bytes .../drawable-hdpi/ic_person_black_48dp.png | Bin 428 -> 0 bytes .../drawable-hdpi/ic_person_white_48dp.png | Bin 440 -> 0 bytes .../ic_phone_in_talk_black_18dp.png | Bin 374 -> 0 bytes .../ic_phone_in_talk_white_18dp.png | Bin 393 -> 0 bytes .../ic_phone_in_talk_white_24dp.png | Bin 483 -> 0 bytes .../ic_play_arrow_black_36dp.png | Bin 236 -> 0 bytes .../ic_play_arrow_white_36dp.png | Bin 242 -> 0 bytes .../ic_play_circle_filled_white_48dp.png | Bin 666 -> 0 bytes src/main/res/drawable-hdpi/ic_profile.png | Bin 999 -> 0 bytes .../drawable-hdpi/ic_public_white_24dp.png | Bin 504 -> 0 bytes .../ic_qr_code_scan_white_24dp.png | Bin 461 -> 0 bytes .../ic_question_answer_white_24dp.png | Bin 192 -> 0 bytes .../drawable-hdpi/ic_refresh_black_24dp.png | Bin 391 -> 0 bytes .../drawable-hdpi/ic_refresh_white_24dp.png | Bin 387 -> 0 bytes .../drawable-hdpi/ic_replay_white_48dp.png | Bin 675 -> 0 bytes .../res/drawable-hdpi/ic_reply_white_24dp.png | Bin 253 -> 0 bytes .../res/drawable-hdpi/ic_room_black_48dp.png | Bin 683 -> 0 bytes .../res/drawable-hdpi/ic_room_white_24dp.png | Bin 494 -> 0 bytes .../res/drawable-hdpi/ic_room_white_48dp.png | Bin 675 -> 0 bytes .../res/drawable-hdpi/ic_save_black_24dp.png | Bin 240 -> 0 bytes .../res/drawable-hdpi/ic_save_white_24dp.png | Bin 247 -> 0 bytes .../ic_search_background_black.png | Bin 3220 -> 0 bytes .../ic_search_background_white.png | Bin 4077 -> 0 bytes .../drawable-hdpi/ic_search_white_24dp.png | Bin 504 -> 0 bytes .../res/drawable-hdpi/ic_send_cancel_away.png | Bin 1396 -> 0 bytes .../res/drawable-hdpi/ic_send_cancel_dnd.png | Bin 1587 -> 0 bytes .../drawable-hdpi/ic_send_cancel_offline.png | Bin 1206 -> 0 bytes .../ic_send_cancel_offline_dark.png | Bin 1163 -> 0 bytes .../ic_send_cancel_offline_white.png | Bin 1205 -> 0 bytes .../drawable-hdpi/ic_send_cancel_online.png | Bin 1550 -> 0 bytes .../drawable-hdpi/ic_send_file_offline.png | Bin 412 -> 0 bytes .../ic_send_file_offline_white.png | Bin 356 -> 0 bytes .../drawable-hdpi/ic_send_location_away.png | Bin 1159 -> 0 bytes .../drawable-hdpi/ic_send_location_dnd.png | Bin 1333 -> 0 bytes .../ic_send_location_offline.png | Bin 971 -> 0 bytes .../ic_send_location_offline_dark.png | Bin 945 -> 0 bytes .../ic_send_location_offline_white.png | Bin 1010 -> 0 bytes .../drawable-hdpi/ic_send_location_online.png | Bin 1345 -> 0 bytes .../res/drawable-hdpi/ic_send_photo_away.png | Bin 1203 -> 0 bytes .../res/drawable-hdpi/ic_send_photo_dnd.png | Bin 1383 -> 0 bytes .../drawable-hdpi/ic_send_photo_offline.png | Bin 987 -> 0 bytes .../ic_send_photo_offline_dark.png | Bin 978 -> 0 bytes .../ic_send_photo_offline_white.png | Bin 1033 -> 0 bytes .../drawable-hdpi/ic_send_photo_online.png | Bin 1402 -> 0 bytes .../drawable-hdpi/ic_send_picture_away.png | Bin 790 -> 0 bytes .../res/drawable-hdpi/ic_send_picture_dnd.png | Bin 884 -> 0 bytes .../drawable-hdpi/ic_send_picture_offline.png | Bin 657 -> 0 bytes .../ic_send_picture_offline_dark.png | Bin 639 -> 0 bytes .../ic_send_picture_offline_white.png | Bin 675 -> 0 bytes .../drawable-hdpi/ic_send_picture_online.png | Bin 887 -> 0 bytes .../res/drawable-hdpi/ic_send_text_away.png | Bin 982 -> 0 bytes .../res/drawable-hdpi/ic_send_text_dnd.png | Bin 1128 -> 0 bytes .../drawable-hdpi/ic_send_text_offline.png | Bin 800 -> 0 bytes .../ic_send_text_offline_dark.png | Bin 765 -> 0 bytes .../ic_send_text_offline_white.png | Bin 844 -> 0 bytes .../res/drawable-hdpi/ic_send_text_online.png | Bin 1121 -> 0 bytes .../drawable-hdpi/ic_send_videocam_away.png | Bin 487 -> 0 bytes .../drawable-hdpi/ic_send_videocam_dnd.png | Bin 540 -> 0 bytes .../ic_send_videocam_offline.png | Bin 435 -> 0 bytes .../ic_send_videocam_offline_white.png | Bin 447 -> 0 bytes .../drawable-hdpi/ic_send_videocam_online.png | Bin 540 -> 0 bytes .../res/drawable-hdpi/ic_send_voice_away.png | Bin 1017 -> 0 bytes .../res/drawable-hdpi/ic_send_voice_dnd.png | Bin 1161 -> 0 bytes .../drawable-hdpi/ic_send_voice_offline.png | Bin 844 -> 0 bytes .../ic_send_voice_offline_dark.png | Bin 824 -> 0 bytes .../ic_send_voice_offline_white.png | Bin 878 -> 0 bytes .../drawable-hdpi/ic_send_voice_online.png | Bin 1170 -> 0 bytes .../drawable-hdpi/ic_settings_black_24dp.png | Bin 453 -> 0 bytes .../drawable-hdpi/ic_settings_white_24dp.png | Bin 460 -> 0 bytes .../res/drawable-hdpi/ic_share_white_24dp.png | Bin 506 -> 0 bytes .../res/drawable-hdpi/ic_star_black_24dp.png | Bin 369 -> 0 bytes .../res/drawable-hdpi/ic_star_white_24dp.png | Bin 370 -> 0 bytes .../drawable-hdpi/ic_stat_alert_warning.png | Bin 757 -> 0 bytes .../ic_stat_communication_import_export.png | Bin 620 -> 0 bytes .../drawable-hdpi/ic_verified_fingerprint.png | Bin 1324 -> 0 bytes .../ic_verified_user_black_18dp.png | Bin 320 -> 0 bytes .../ic_verified_user_white_18dp.png | Bin 329 -> 0 bytes .../drawable-hdpi/ic_videocam_black_24dp.png | Bin 169 -> 0 bytes .../ic_videocam_off_black_24dp.png | Bin 260 -> 0 bytes .../drawable-hdpi/ic_videocam_white_24dp.png | Bin 173 -> 0 bytes .../drawable-hdpi/ic_voicemail_white_24dp.png | Bin 478 -> 0 bytes .../ic_volume_off_black_24dp.png | Bin 407 -> 0 bytes .../drawable-hdpi/ic_volume_up_black_24dp.png | Bin 364 -> 0 bytes .../drawable-hdpi/ic_warning_white_24dp.png | Bin 421 -> 0 bytes .../drawable-hdpi/ic_warning_white_48dp.png | Bin 714 -> 0 bytes src/main/res/drawable-hdpi/ic_wear_reply.png | Bin 518 -> 0 bytes .../message_bubble_received.9.png | Bin 772 -> 0 bytes .../message_bubble_received_dark.9.png | Bin 773 -> 0 bytes .../message_bubble_received_grey.9.png | Bin 750 -> 0 bytes .../message_bubble_received_warning.9.png | Bin 776 -> 0 bytes .../message_bubble_received_white.9.png | Bin 779 -> 0 bytes .../drawable-hdpi/message_bubble_sent.9.png | Bin 687 -> 0 bytes .../message_bubble_sent_grey.9.png | Bin 707 -> 0 bytes .../drawable-mdpi/baseline_tour_black_48.png | Bin 273 -> 0 bytes .../drawable-mdpi/baseline_tour_white_48.png | Bin 277 -> 0 bytes .../res/drawable-mdpi/date_bubble_grey.9.png | Bin 514 -> 0 bytes .../res/drawable-mdpi/date_bubble_white.9.png | Bin 525 -> 0 bytes .../ic_account_box_white_24dp.png | Bin 290 -> 0 bytes .../res/drawable-mdpi/ic_action_reply.png | Bin 343 -> 0 bytes .../res/drawable-mdpi/ic_add_white_24dp.png | Bin 174 -> 0 bytes .../drawable-mdpi/ic_android_black_48dp.png | Bin 343 -> 0 bytes .../drawable-mdpi/ic_android_white_48dp.png | Bin 356 -> 0 bytes .../ic_announcement_white_24dp.png | Bin 214 -> 0 bytes .../drawable-mdpi/ic_archive_black_48dp.png | Bin 261 -> 0 bytes .../drawable-mdpi/ic_archive_white_24dp.png | Bin 181 -> 0 bytes .../drawable-mdpi/ic_archive_white_48dp.png | Bin 267 -> 0 bytes .../res/drawable-mdpi/ic_attach_camera.png | Bin 1114 -> 0 bytes .../drawable-mdpi/ic_attach_camera_white.png | Bin 453 -> 0 bytes .../res/drawable-mdpi/ic_attach_document.png | Bin 1021 -> 0 bytes .../ic_attach_document_white.png | Bin 322 -> 0 bytes .../ic_attach_file_white_24dp.png | Bin 332 -> 0 bytes .../res/drawable-mdpi/ic_attach_location.png | Bin 1146 -> 0 bytes .../ic_attach_location_white.png | Bin 486 -> 0 bytes .../res/drawable-mdpi/ic_attach_photo.png | Bin 1066 -> 0 bytes .../drawable-mdpi/ic_attach_photo_white.png | Bin 395 -> 0 bytes .../res/drawable-mdpi/ic_attach_record.png | Bin 1115 -> 0 bytes .../drawable-mdpi/ic_attach_record_white.png | Bin 451 -> 0 bytes .../res/drawable-mdpi/ic_attach_videocam.png | Bin 215 -> 0 bytes .../ic_attach_videocam_white.png | Bin 308 -> 0 bytes .../drawable-mdpi/ic_autorenew_white_24dp.png | Bin 353 -> 0 bytes .../drawable-mdpi/ic_backup_black_48dp.png | Bin 386 -> 0 bytes .../drawable-mdpi/ic_backup_white_48dp.png | Bin 405 -> 0 bytes .../res/drawable-mdpi/ic_block_white_24dp.png | Bin 428 -> 0 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 283 -> 0 bytes .../res/drawable-mdpi/ic_book_black_48dp.png | Bin 201 -> 0 bytes .../res/drawable-mdpi/ic_book_white_48dp.png | Bin 197 -> 0 bytes .../res/drawable-mdpi/ic_call_black_24dp.png | Bin 244 -> 0 bytes .../drawable-mdpi/ic_call_end_white_48dp.png | Bin 389 -> 0 bytes .../drawable-mdpi/ic_call_made_black_18dp.png | Bin 132 -> 0 bytes .../drawable-mdpi/ic_call_made_white_18dp.png | Bin 135 -> 0 bytes .../ic_call_missed_black_18dp.png | Bin 141 -> 0 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 134 -> 0 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 136 -> 0 bytes .../ic_call_missed_white_18dp.png | Bin 147 -> 0 bytes .../ic_call_received_black_18dp.png | Bin 133 -> 0 bytes .../ic_call_received_white_18dp.png | Bin 140 -> 0 bytes .../res/drawable-mdpi/ic_call_white_24dp.png | Bin 246 -> 0 bytes .../res/drawable-mdpi/ic_call_white_48dp.png | Bin 420 -> 0 bytes .../ic_camera_alt_white_24dp.png | Bin 240 -> 0 bytes .../drawable-mdpi/ic_cancel_black_24dp.png | Bin 291 -> 0 bytes .../drawable-mdpi/ic_cancel_white_24dp.png | Bin 393 -> 0 bytes .../res/drawable-mdpi/ic_chat_white_24dp.png | Bin 133 -> 0 bytes .../res/drawable-mdpi/ic_clear_white_48dp.png | Bin 257 -> 0 bytes .../ic_cloud_download_white_24dp.png | Bin 242 -> 0 bytes .../ic_content_copy_white_24dp.png | Bin 134 -> 0 bytes .../res/drawable-mdpi/ic_crop_white_24dp.png | Bin 214 -> 0 bytes .../drawable-mdpi/ic_delete_black_24dp.png | Bin 111 -> 0 bytes .../drawable-mdpi/ic_delete_white_24dp.png | Bin 197 -> 0 bytes .../ic_description_black_48dp.png | Bin 202 -> 0 bytes .../ic_description_white_48dp.png | Bin 214 -> 0 bytes .../ic_directions_black_24dp.png | Bin 181 -> 0 bytes .../ic_directions_white_24dp.png | Bin 191 -> 0 bytes .../res/drawable-mdpi/ic_done_black_18dp.png | Bin 138 -> 0 bytes .../res/drawable-mdpi/ic_done_white_18dp.png | Bin 144 -> 0 bytes .../drawable-mdpi/ic_drafts_white_24dp.png | Bin 284 -> 0 bytes .../res/drawable-mdpi/ic_edit_black_24dp.png | Bin 160 -> 0 bytes .../res/drawable-mdpi/ic_edit_white_24dp.png | Bin 272 -> 0 bytes .../res/drawable-mdpi/ic_error_white_24dp.png | Bin 232 -> 0 bytes .../res/drawable-mdpi/ic_event_black_48dp.png | Bin 188 -> 0 bytes .../res/drawable-mdpi/ic_event_white_48dp.png | Bin 193 -> 0 bytes .../ic_file_download_white_24dp.png | Bin 210 -> 0 bytes .../ic_flip_camera_android_black_24dp.png | Bin 472 -> 0 bytes .../drawable-mdpi/ic_forward_white_24dp.png | Bin 117 -> 0 bytes .../drawable-mdpi/ic_gps_fixed_black_24dp.png | Bin 341 -> 0 bytes .../drawable-mdpi/ic_gps_fixed_white_24dp.png | Bin 350 -> 0 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 295 -> 0 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 298 -> 0 bytes .../drawable-mdpi/ic_group_add_white_24dp.png | Bin 313 -> 0 bytes .../res/drawable-mdpi/ic_group_white_24dp.png | Bin 296 -> 0 bytes .../drawable-mdpi/ic_headset_black_24dp.png | Bin 230 -> 0 bytes .../drawable-mdpi/ic_headset_black_48dp.png | Bin 412 -> 0 bytes .../drawable-mdpi/ic_headset_white_48dp.png | Bin 433 -> 0 bytes .../res/drawable-mdpi/ic_help_black_48dp.png | Bin 579 -> 0 bytes .../res/drawable-mdpi/ic_help_white_24dp.png | Bin 304 -> 0 bytes .../res/drawable-mdpi/ic_help_white_48dp.png | Bin 585 -> 0 bytes .../ic_hourglass_empty_white_24dp.png | Bin 135 -> 0 bytes .../res/drawable-mdpi/ic_image_black_48dp.png | Bin 295 -> 0 bytes .../res/drawable-mdpi/ic_image_white_48dp.png | Bin 304 -> 0 bytes .../res/drawable-mdpi/ic_input_white_24dp.png | Bin 148 -> 0 bytes .../drawable-mdpi/ic_link_off_white_24dp.png | Bin 273 -> 0 bytes .../res/drawable-mdpi/ic_link_white_24dp.png | Bin 297 -> 0 bytes .../res/drawable-mdpi/ic_lock_black_18dp.png | Bin 206 -> 0 bytes .../drawable-mdpi/ic_lock_open_white_24dp.png | Bin 290 -> 0 bytes .../res/drawable-mdpi/ic_lock_white_18dp.png | Bin 211 -> 0 bytes .../res/drawable-mdpi/ic_lock_white_24dp.png | Bin 296 -> 0 bytes .../res/drawable-mdpi/ic_mic_black_24dp.png | Bin 232 -> 0 bytes .../res/drawable-mdpi/ic_mic_black_48dp.png | Bin 418 -> 0 bytes .../drawable-mdpi/ic_mic_off_black_24dp.png | Bin 271 -> 0 bytes .../res/drawable-mdpi/ic_mic_white_48dp.png | Bin 436 -> 0 bytes .../drawable-mdpi/ic_mode_edit_black_18dp.png | Bin 229 -> 0 bytes .../drawable-mdpi/ic_mode_edit_white_18dp.png | Bin 249 -> 0 bytes .../ic_new_releases_black_24dp.png | Bin 277 -> 0 bytes .../ic_new_releases_white_24dp.png | Bin 287 -> 0 bytes .../ic_no_results_background_black.png | Bin 2359 -> 0 bytes .../ic_no_results_background_white.png | Bin 3019 -> 0 bytes .../ic_notifications_black_24dp.png | Bin 179 -> 0 bytes .../ic_notifications_none_black_24dp.png | Bin 206 -> 0 bytes .../ic_notifications_none_white80.png | Bin 409 -> 0 bytes .../ic_notifications_none_white_24dp.png | Bin 207 -> 0 bytes .../ic_notifications_off_black_24dp.png | Bin 290 -> 0 bytes .../ic_notifications_off_white80.png | Bin 505 -> 0 bytes .../ic_notifications_off_white_24dp.png | Bin 279 -> 0 bytes .../ic_notifications_paused_black_24dp.png | Bin 235 -> 0 bytes .../ic_notifications_paused_white80.png | Bin 466 -> 0 bytes .../ic_notifications_paused_white_24dp.png | Bin 235 -> 0 bytes .../ic_notifications_white80.png | Bin 369 -> 0 bytes .../ic_notifications_white_24dp.png | Bin 177 -> 0 bytes .../res/drawable-mdpi/ic_pause_black_36dp.png | Bin 102 -> 0 bytes .../res/drawable-mdpi/ic_pause_white_36dp.png | Bin 105 -> 0 bytes .../ic_person_add_white_24dp.png | Bin 289 -> 0 bytes .../drawable-mdpi/ic_person_black_48dp.png | Bin 307 -> 0 bytes .../drawable-mdpi/ic_person_white_48dp.png | Bin 312 -> 0 bytes .../ic_phone_in_talk_black_18dp.png | Bin 253 -> 0 bytes .../ic_phone_in_talk_white_18dp.png | Bin 261 -> 0 bytes .../ic_phone_in_talk_white_24dp.png | Bin 325 -> 0 bytes .../ic_play_arrow_black_36dp.png | Bin 194 -> 0 bytes .../ic_play_arrow_white_36dp.png | Bin 195 -> 0 bytes .../ic_play_circle_filled_white_48dp.png | Bin 464 -> 0 bytes src/main/res/drawable-mdpi/ic_profile.png | Bin 622 -> 0 bytes .../drawable-mdpi/ic_public_white_24dp.png | Bin 339 -> 0 bytes .../ic_qr_code_scan_white_24dp.png | Bin 309 -> 0 bytes .../ic_question_answer_white_24dp.png | Bin 137 -> 0 bytes .../drawable-mdpi/ic_refresh_black_24dp.png | Bin 249 -> 0 bytes .../drawable-mdpi/ic_refresh_white_24dp.png | Bin 254 -> 0 bytes .../drawable-mdpi/ic_replay_white_48dp.png | Bin 457 -> 0 bytes .../res/drawable-mdpi/ic_reply_white_24dp.png | Bin 186 -> 0 bytes .../res/drawable-mdpi/ic_room_black_48dp.png | Bin 457 -> 0 bytes .../res/drawable-mdpi/ic_room_white_24dp.png | Bin 362 -> 0 bytes .../res/drawable-mdpi/ic_room_white_48dp.png | Bin 456 -> 0 bytes .../res/drawable-mdpi/ic_save_black_24dp.png | Bin 167 -> 0 bytes .../res/drawable-mdpi/ic_save_white_24dp.png | Bin 168 -> 0 bytes .../ic_search_background_black.png | Bin 2080 -> 0 bytes .../ic_search_background_white.png | Bin 2344 -> 0 bytes .../drawable-mdpi/ic_search_white_24dp.png | Bin 346 -> 0 bytes .../res/drawable-mdpi/ic_send_cancel_away.png | Bin 901 -> 0 bytes .../res/drawable-mdpi/ic_send_cancel_dnd.png | Bin 1025 -> 0 bytes .../drawable-mdpi/ic_send_cancel_offline.png | Bin 837 -> 0 bytes .../ic_send_cancel_offline_dark.png | Bin 786 -> 0 bytes .../ic_send_cancel_offline_white.png | Bin 799 -> 0 bytes .../drawable-mdpi/ic_send_cancel_online.png | Bin 1029 -> 0 bytes .../drawable-mdpi/ic_send_file_offline.png | Bin 282 -> 0 bytes .../ic_send_file_offline_white.png | Bin 248 -> 0 bytes .../drawable-mdpi/ic_send_location_away.png | Bin 784 -> 0 bytes .../drawable-mdpi/ic_send_location_dnd.png | Bin 917 -> 0 bytes .../ic_send_location_offline.png | Bin 673 -> 0 bytes .../ic_send_location_offline_dark.png | Bin 681 -> 0 bytes .../ic_send_location_offline_white.png | Bin 699 -> 0 bytes .../drawable-mdpi/ic_send_location_online.png | Bin 928 -> 0 bytes .../res/drawable-mdpi/ic_send_photo_away.png | Bin 776 -> 0 bytes .../res/drawable-mdpi/ic_send_photo_dnd.png | Bin 915 -> 0 bytes .../drawable-mdpi/ic_send_photo_offline.png | Bin 666 -> 0 bytes .../ic_send_photo_offline_dark.png | Bin 649 -> 0 bytes .../ic_send_photo_offline_white.png | Bin 683 -> 0 bytes .../drawable-mdpi/ic_send_photo_online.png | Bin 917 -> 0 bytes .../drawable-mdpi/ic_send_picture_away.png | Bin 512 -> 0 bytes .../res/drawable-mdpi/ic_send_picture_dnd.png | Bin 585 -> 0 bytes .../drawable-mdpi/ic_send_picture_offline.png | Bin 456 -> 0 bytes .../ic_send_picture_offline_dark.png | Bin 436 -> 0 bytes .../ic_send_picture_offline_white.png | Bin 475 -> 0 bytes .../drawable-mdpi/ic_send_picture_online.png | Bin 594 -> 0 bytes .../res/drawable-mdpi/ic_send_text_away.png | Bin 677 -> 0 bytes .../res/drawable-mdpi/ic_send_text_dnd.png | Bin 781 -> 0 bytes .../drawable-mdpi/ic_send_text_offline.png | Bin 542 -> 0 bytes .../ic_send_text_offline_dark.png | Bin 557 -> 0 bytes .../ic_send_text_offline_white.png | Bin 583 -> 0 bytes .../res/drawable-mdpi/ic_send_text_online.png | Bin 781 -> 0 bytes .../drawable-mdpi/ic_send_videocam_away.png | Bin 365 -> 0 bytes .../drawable-mdpi/ic_send_videocam_dnd.png | Bin 397 -> 0 bytes .../ic_send_videocam_offline.png | Bin 341 -> 0 bytes .../ic_send_videocam_offline_white.png | Bin 340 -> 0 bytes .../drawable-mdpi/ic_send_videocam_online.png | Bin 403 -> 0 bytes .../res/drawable-mdpi/ic_send_voice_away.png | Bin 679 -> 0 bytes .../res/drawable-mdpi/ic_send_voice_dnd.png | Bin 795 -> 0 bytes .../drawable-mdpi/ic_send_voice_offline.png | Bin 575 -> 0 bytes .../ic_send_voice_offline_dark.png | Bin 576 -> 0 bytes .../ic_send_voice_offline_white.png | Bin 606 -> 0 bytes .../drawable-mdpi/ic_send_voice_online.png | Bin 796 -> 0 bytes .../drawable-mdpi/ic_settings_black_24dp.png | Bin 322 -> 0 bytes .../drawable-mdpi/ic_settings_white_24dp.png | Bin 326 -> 0 bytes .../res/drawable-mdpi/ic_share_white_24dp.png | Bin 361 -> 0 bytes .../res/drawable-mdpi/ic_star_black_24dp.png | Bin 263 -> 0 bytes .../res/drawable-mdpi/ic_star_white_24dp.png | Bin 271 -> 0 bytes .../drawable-mdpi/ic_stat_alert_warning.png | Bin 425 -> 0 bytes .../ic_stat_communication_import_export.png | Bin 392 -> 0 bytes .../drawable-mdpi/ic_verified_fingerprint.png | Bin 1022 -> 0 bytes .../ic_verified_user_black_18dp.png | Bin 238 -> 0 bytes .../ic_verified_user_white_18dp.png | Bin 241 -> 0 bytes .../drawable-mdpi/ic_videocam_black_24dp.png | Bin 127 -> 0 bytes .../ic_videocam_off_black_24dp.png | Bin 193 -> 0 bytes .../drawable-mdpi/ic_videocam_white_24dp.png | Bin 131 -> 0 bytes .../drawable-mdpi/ic_voicemail_white_24dp.png | Bin 221 -> 0 bytes .../ic_volume_off_black_24dp.png | Bin 279 -> 0 bytes .../drawable-mdpi/ic_volume_up_black_24dp.png | Bin 235 -> 0 bytes .../drawable-mdpi/ic_warning_white_24dp.png | Bin 328 -> 0 bytes .../drawable-mdpi/ic_warning_white_48dp.png | Bin 364 -> 0 bytes src/main/res/drawable-mdpi/ic_wear_reply.png | Bin 429 -> 0 bytes .../message_bubble_received.9.png | Bin 596 -> 0 bytes .../message_bubble_received_dark.9.png | Bin 617 -> 0 bytes .../message_bubble_received_grey.9.png | Bin 595 -> 0 bytes .../message_bubble_received_warning.9.png | Bin 599 -> 0 bytes .../message_bubble_received_white.9.png | Bin 610 -> 0 bytes .../drawable-mdpi/message_bubble_sent.9.png | Bin 558 -> 0 bytes .../message_bubble_sent_grey.9.png | Bin 568 -> 0 bytes src/main/res/drawable-mdpi/play_gif_black.png | Bin 584 -> 0 bytes src/main/res/drawable-mdpi/play_gif_white.png | Bin 612 -> 0 bytes .../res/drawable-mdpi/play_video_black.png | Bin 3204 -> 0 bytes .../res/drawable-mdpi/play_video_white.png | Bin 3383 -> 0 bytes .../drawable-xhdpi/baseline_tour_black_48.png | Bin 451 -> 0 bytes .../drawable-xhdpi/baseline_tour_white_48.png | Bin 451 -> 0 bytes .../res/drawable-xhdpi/date_bubble_grey.9.png | Bin 739 -> 0 bytes .../drawable-xhdpi/date_bubble_white.9.png | Bin 769 -> 0 bytes .../ic_account_box_white_24dp.png | Bin 431 -> 0 bytes .../res/drawable-xhdpi/ic_action_reply.png | Bin 561 -> 0 bytes .../res/drawable-xhdpi/ic_add_white_24dp.png | Bin 198 -> 0 bytes .../drawable-xhdpi/ic_android_black_48dp.png | Bin 641 -> 0 bytes .../drawable-xhdpi/ic_android_white_48dp.png | Bin 665 -> 0 bytes .../ic_announcement_white_24dp.png | Bin 285 -> 0 bytes .../drawable-xhdpi/ic_archive_black_48dp.png | Bin 483 -> 0 bytes .../drawable-xhdpi/ic_archive_white_24dp.png | Bin 267 -> 0 bytes .../drawable-xhdpi/ic_archive_white_48dp.png | Bin 489 -> 0 bytes .../res/drawable-xhdpi/ic_attach_camera.png | Bin 1313 -> 0 bytes .../drawable-xhdpi/ic_attach_camera_white.png | Bin 757 -> 0 bytes .../res/drawable-xhdpi/ic_attach_document.png | Bin 1097 -> 0 bytes .../ic_attach_document_white.png | Bin 427 -> 0 bytes .../ic_attach_file_white_24dp.png | Bin 576 -> 0 bytes .../res/drawable-xhdpi/ic_attach_location.png | Bin 1372 -> 0 bytes .../ic_attach_location_white.png | Bin 766 -> 0 bytes .../res/drawable-xhdpi/ic_attach_photo.png | Bin 1169 -> 0 bytes .../drawable-xhdpi/ic_attach_photo_white.png | Bin 543 -> 0 bytes .../res/drawable-xhdpi/ic_attach_record.png | Bin 1310 -> 0 bytes .../drawable-xhdpi/ic_attach_record_white.png | Bin 689 -> 0 bytes .../res/drawable-xhdpi/ic_attach_videocam.png | Bin 276 -> 0 bytes .../ic_attach_videocam_white.png | Bin 378 -> 0 bytes .../ic_autorenew_white_24dp.png | Bin 604 -> 0 bytes .../drawable-xhdpi/ic_backup_black_48dp.png | Bin 733 -> 0 bytes .../drawable-xhdpi/ic_backup_white_48dp.png | Bin 770 -> 0 bytes .../drawable-xhdpi/ic_block_white_24dp.png | Bin 796 -> 0 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 479 -> 0 bytes .../res/drawable-xhdpi/ic_book_black_48dp.png | Bin 340 -> 0 bytes .../res/drawable-xhdpi/ic_book_white_48dp.png | Bin 350 -> 0 bytes .../res/drawable-xhdpi/ic_call_black_24dp.png | Bin 408 -> 0 bytes .../drawable-xhdpi/ic_call_end_white_48dp.png | Bin 712 -> 0 bytes .../ic_call_made_black_18dp.png | Bin 174 -> 0 bytes .../ic_call_made_white_18dp.png | Bin 189 -> 0 bytes .../ic_call_missed_black_18dp.png | Bin 201 -> 0 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 188 -> 0 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 193 -> 0 bytes .../ic_call_missed_white_18dp.png | Bin 215 -> 0 bytes .../ic_call_received_black_18dp.png | Bin 175 -> 0 bytes .../ic_call_received_white_18dp.png | Bin 189 -> 0 bytes .../res/drawable-xhdpi/ic_call_white_24dp.png | Bin 420 -> 0 bytes .../res/drawable-xhdpi/ic_call_white_48dp.png | Bin 778 -> 0 bytes .../ic_camera_alt_white_24dp.png | Bin 446 -> 0 bytes .../drawable-xhdpi/ic_cancel_black_24dp.png | Bin 517 -> 0 bytes .../drawable-xhdpi/ic_cancel_white_24dp.png | Bin 645 -> 0 bytes .../res/drawable-xhdpi/ic_chat_white_24dp.png | Bin 205 -> 0 bytes .../drawable-xhdpi/ic_clear_white_48dp.png | Bin 436 -> 0 bytes .../ic_cloud_download_white_24dp.png | Bin 417 -> 0 bytes .../ic_content_copy_white_24dp.png | Bin 188 -> 0 bytes .../res/drawable-xhdpi/ic_crop_white_24dp.png | Bin 272 -> 0 bytes .../drawable-xhdpi/ic_delete_black_24dp.png | Bin 148 -> 0 bytes .../drawable-xhdpi/ic_delete_white_24dp.png | Bin 270 -> 0 bytes .../ic_description_black_48dp.png | Bin 355 -> 0 bytes .../ic_description_white_48dp.png | Bin 378 -> 0 bytes .../ic_directions_black_24dp.png | Bin 274 -> 0 bytes .../ic_directions_white_24dp.png | Bin 307 -> 0 bytes .../res/drawable-xhdpi/ic_done_black_18dp.png | Bin 177 -> 0 bytes .../res/drawable-xhdpi/ic_done_white_18dp.png | Bin 188 -> 0 bytes .../drawable-xhdpi/ic_drafts_white_24dp.png | Bin 509 -> 0 bytes .../res/drawable-xhdpi/ic_edit_black_24dp.png | Bin 222 -> 0 bytes .../res/drawable-xhdpi/ic_edit_white_24dp.png | Bin 378 -> 0 bytes .../drawable-xhdpi/ic_error_white_24dp.png | Bin 431 -> 0 bytes .../drawable-xhdpi/ic_event_black_48dp.png | Bin 316 -> 0 bytes .../drawable-xhdpi/ic_event_white_48dp.png | Bin 326 -> 0 bytes .../ic_file_download_white_24dp.png | Bin 282 -> 0 bytes .../ic_flip_camera_android_black_24dp.png | Bin 915 -> 0 bytes .../drawable-xhdpi/ic_forward_white_24dp.png | Bin 159 -> 0 bytes .../ic_gps_fixed_black_24dp.png | Bin 660 -> 0 bytes .../ic_gps_fixed_white_24dp.png | Bin 687 -> 0 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 561 -> 0 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 577 -> 0 bytes .../ic_group_add_white_24dp.png | Bin 484 -> 0 bytes .../drawable-xhdpi/ic_group_white_24dp.png | Bin 464 -> 0 bytes .../drawable-xhdpi/ic_headset_black_24dp.png | Bin 412 -> 0 bytes .../drawable-xhdpi/ic_headset_black_48dp.png | Bin 786 -> 0 bytes .../drawable-xhdpi/ic_headset_white_48dp.png | Bin 838 -> 0 bytes .../res/drawable-xhdpi/ic_help_black_48dp.png | Bin 1122 -> 0 bytes .../res/drawable-xhdpi/ic_help_white_24dp.png | Bin 585 -> 0 bytes .../res/drawable-xhdpi/ic_help_white_48dp.png | Bin 1132 -> 0 bytes .../ic_hourglass_empty_white_24dp.png | Bin 174 -> 0 bytes .../drawable-xhdpi/ic_image_black_48dp.png | Bin 548 -> 0 bytes .../drawable-xhdpi/ic_image_white_48dp.png | Bin 570 -> 0 bytes .../drawable-xhdpi/ic_input_white_24dp.png | Bin 219 -> 0 bytes .../drawable-xhdpi/ic_link_off_white_24dp.png | Bin 472 -> 0 bytes .../res/drawable-xhdpi/ic_link_white_24dp.png | Bin 494 -> 0 bytes .../res/drawable-xhdpi/ic_lock_black_18dp.png | Bin 308 -> 0 bytes .../ic_lock_open_white_24dp.png | Bin 513 -> 0 bytes .../res/drawable-xhdpi/ic_lock_white_18dp.png | Bin 309 -> 0 bytes .../res/drawable-xhdpi/ic_lock_white_24dp.png | Bin 465 -> 0 bytes .../res/drawable-xhdpi/ic_mic_black_24dp.png | Bin 418 -> 0 bytes .../res/drawable-xhdpi/ic_mic_black_48dp.png | Bin 773 -> 0 bytes .../drawable-xhdpi/ic_mic_off_black_24dp.png | Bin 454 -> 0 bytes .../res/drawable-xhdpi/ic_mic_white_48dp.png | Bin 819 -> 0 bytes .../ic_mode_edit_black_18dp.png | Bin 291 -> 0 bytes .../ic_mode_edit_white_18dp.png | Bin 351 -> 0 bytes .../ic_new_releases_black_24dp.png | Bin 480 -> 0 bytes .../ic_new_releases_white_24dp.png | Bin 498 -> 0 bytes .../ic_no_results_background_black.png | Bin 5368 -> 0 bytes .../ic_no_results_background_white.png | Bin 6250 -> 0 bytes .../ic_notifications_black_24dp.png | Bin 272 -> 0 bytes .../ic_notifications_none_black_24dp.png | Bin 349 -> 0 bytes .../ic_notifications_none_white80.png | Bin 706 -> 0 bytes .../ic_notifications_none_white_24dp.png | Bin 344 -> 0 bytes .../ic_notifications_off_black_24dp.png | Bin 492 -> 0 bytes .../ic_notifications_off_white80.png | Bin 1014 -> 0 bytes .../ic_notifications_off_white_24dp.png | Bin 493 -> 0 bytes .../ic_notifications_paused_black_24dp.png | Bin 351 -> 0 bytes .../ic_notifications_paused_white80.png | Bin 745 -> 0 bytes .../ic_notifications_paused_white_24dp.png | Bin 351 -> 0 bytes .../ic_notifications_white80.png | Bin 583 -> 0 bytes .../ic_notifications_white_24dp.png | Bin 265 -> 0 bytes .../drawable-xhdpi/ic_pause_black_36dp.png | Bin 109 -> 0 bytes .../drawable-xhdpi/ic_pause_white_36dp.png | Bin 92 -> 0 bytes .../ic_person_add_white_24dp.png | Bin 423 -> 0 bytes .../drawable-xhdpi/ic_person_black_48dp.png | Bin 565 -> 0 bytes .../drawable-xhdpi/ic_person_white_48dp.png | Bin 577 -> 0 bytes .../ic_phone_in_talk_black_18dp.png | Bin 477 -> 0 bytes .../ic_phone_in_talk_white_18dp.png | Bin 483 -> 0 bytes .../ic_phone_in_talk_white_24dp.png | Bin 601 -> 0 bytes .../ic_play_arrow_black_36dp.png | Bin 265 -> 0 bytes .../ic_play_arrow_white_36dp.png | Bin 283 -> 0 bytes .../ic_play_circle_filled_white_48dp.png | Bin 883 -> 0 bytes src/main/res/drawable-xhdpi/ic_profile.png | Bin 1374 -> 0 bytes .../drawable-xhdpi/ic_public_white_24dp.png | Bin 661 -> 0 bytes .../ic_qr_code_scan_white_24dp.png | Bin 444 -> 0 bytes .../ic_question_answer_white_24dp.png | Bin 195 -> 0 bytes .../drawable-xhdpi/ic_refresh_black_24dp.png | Bin 506 -> 0 bytes .../drawable-xhdpi/ic_refresh_white_24dp.png | Bin 509 -> 0 bytes .../drawable-xhdpi/ic_replay_white_48dp.png | Bin 908 -> 0 bytes .../drawable-xhdpi/ic_reply_white_24dp.png | Bin 306 -> 0 bytes .../res/drawable-xhdpi/ic_room_black_48dp.png | Bin 868 -> 0 bytes .../res/drawable-xhdpi/ic_room_white_24dp.png | Bin 587 -> 0 bytes .../res/drawable-xhdpi/ic_room_white_48dp.png | Bin 869 -> 0 bytes .../res/drawable-xhdpi/ic_save_black_24dp.png | Bin 264 -> 0 bytes .../res/drawable-xhdpi/ic_save_white_24dp.png | Bin 273 -> 0 bytes .../ic_search_background_black.png | Bin 4465 -> 0 bytes .../ic_search_background_white.png | Bin 5477 -> 0 bytes .../drawable-xhdpi/ic_search_white_24dp.png | Bin 591 -> 0 bytes .../drawable-xhdpi/ic_send_cancel_away.png | Bin 1724 -> 0 bytes .../res/drawable-xhdpi/ic_send_cancel_dnd.png | Bin 1958 -> 0 bytes .../drawable-xhdpi/ic_send_cancel_offline.png | Bin 1519 -> 0 bytes .../ic_send_cancel_offline_dark.png | Bin 1458 -> 0 bytes .../ic_send_cancel_offline_white.png | Bin 1494 -> 0 bytes .../drawable-xhdpi/ic_send_cancel_online.png | Bin 1972 -> 0 bytes .../drawable-xhdpi/ic_send_file_offline.png | Bin 429 -> 0 bytes .../ic_send_file_offline_white.png | Bin 380 -> 0 bytes .../drawable-xhdpi/ic_send_location_away.png | Bin 1507 -> 0 bytes .../drawable-xhdpi/ic_send_location_dnd.png | Bin 1753 -> 0 bytes .../ic_send_location_offline.png | Bin 1278 -> 0 bytes .../ic_send_location_offline_dark.png | Bin 1250 -> 0 bytes .../ic_send_location_offline_white.png | Bin 1311 -> 0 bytes .../ic_send_location_online.png | Bin 1773 -> 0 bytes .../res/drawable-xhdpi/ic_send_photo_away.png | Bin 1516 -> 0 bytes .../res/drawable-xhdpi/ic_send_photo_dnd.png | Bin 1756 -> 0 bytes .../drawable-xhdpi/ic_send_photo_offline.png | Bin 1247 -> 0 bytes .../ic_send_photo_offline_dark.png | Bin 1191 -> 0 bytes .../ic_send_photo_offline_white.png | Bin 1290 -> 0 bytes .../drawable-xhdpi/ic_send_photo_online.png | Bin 1758 -> 0 bytes .../drawable-xhdpi/ic_send_picture_away.png | Bin 899 -> 0 bytes .../drawable-xhdpi/ic_send_picture_dnd.png | Bin 1016 -> 0 bytes .../ic_send_picture_offline.png | Bin 799 -> 0 bytes .../ic_send_picture_offline_dark.png | Bin 752 -> 0 bytes .../ic_send_picture_offline_white.png | Bin 828 -> 0 bytes .../drawable-xhdpi/ic_send_picture_online.png | Bin 1006 -> 0 bytes .../res/drawable-xhdpi/ic_send_text_away.png | Bin 1270 -> 0 bytes .../res/drawable-xhdpi/ic_send_text_dnd.png | Bin 1430 -> 0 bytes .../drawable-xhdpi/ic_send_text_offline.png | Bin 1007 -> 0 bytes .../ic_send_text_offline_dark.png | Bin 958 -> 0 bytes .../ic_send_text_offline_white.png | Bin 1079 -> 0 bytes .../drawable-xhdpi/ic_send_text_online.png | Bin 1429 -> 0 bytes .../drawable-xhdpi/ic_send_videocam_away.png | Bin 502 -> 0 bytes .../drawable-xhdpi/ic_send_videocam_dnd.png | Bin 542 -> 0 bytes .../ic_send_videocam_offline.png | Bin 479 -> 0 bytes .../ic_send_videocam_offline_white.png | Bin 470 -> 0 bytes .../ic_send_videocam_online.png | Bin 539 -> 0 bytes .../res/drawable-xhdpi/ic_send_voice_away.png | Bin 1257 -> 0 bytes .../res/drawable-xhdpi/ic_send_voice_dnd.png | Bin 1419 -> 0 bytes .../drawable-xhdpi/ic_send_voice_offline.png | Bin 1075 -> 0 bytes .../ic_send_voice_offline_dark.png | Bin 1034 -> 0 bytes .../ic_send_voice_offline_white.png | Bin 1109 -> 0 bytes .../drawable-xhdpi/ic_send_voice_online.png | Bin 1433 -> 0 bytes .../drawable-xhdpi/ic_settings_black_24dp.png | Bin 557 -> 0 bytes .../drawable-xhdpi/ic_settings_white_24dp.png | Bin 562 -> 0 bytes .../drawable-xhdpi/ic_share_white_24dp.png | Bin 625 -> 0 bytes .../res/drawable-xhdpi/ic_star_black_24dp.png | Bin 467 -> 0 bytes .../res/drawable-xhdpi/ic_star_white_24dp.png | Bin 475 -> 0 bytes .../drawable-xhdpi/ic_stat_alert_warning.png | Bin 1175 -> 0 bytes .../ic_stat_communication_import_export.png | Bin 972 -> 0 bytes .../ic_verified_fingerprint.png | Bin 1551 -> 0 bytes .../ic_verified_user_black_18dp.png | Bin 390 -> 0 bytes .../ic_verified_user_white_18dp.png | Bin 407 -> 0 bytes .../drawable-xhdpi/ic_videocam_black_24dp.png | Bin 171 -> 0 bytes .../ic_videocam_off_black_24dp.png | Bin 274 -> 0 bytes .../drawable-xhdpi/ic_videocam_white_24dp.png | Bin 178 -> 0 bytes .../ic_voicemail_white_24dp.png | Bin 487 -> 0 bytes .../ic_volume_off_black_24dp.png | Bin 493 -> 0 bytes .../ic_volume_up_black_24dp.png | Bin 434 -> 0 bytes .../drawable-xhdpi/ic_warning_white_24dp.png | Bin 460 -> 0 bytes .../drawable-xhdpi/ic_warning_white_48dp.png | Bin 590 -> 0 bytes src/main/res/drawable-xhdpi/ic_wear_reply.png | Bin 564 -> 0 bytes .../message_bubble_received.9.png | Bin 936 -> 0 bytes .../message_bubble_received_dark.9.png | Bin 926 -> 0 bytes .../message_bubble_received_grey.9.png | Bin 915 -> 0 bytes .../message_bubble_received_warning.9.png | Bin 916 -> 0 bytes .../message_bubble_received_white.9.png | Bin 935 -> 0 bytes .../drawable-xhdpi/message_bubble_sent.9.png | Bin 857 -> 0 bytes .../message_bubble_sent_grey.9.png | Bin 842 -> 0 bytes .../baseline_tour_black_48.png | Bin 662 -> 0 bytes .../baseline_tour_white_48.png | Bin 662 -> 0 bytes .../drawable-xxhdpi/date_bubble_grey.9.png | Bin 1072 -> 0 bytes .../drawable-xxhdpi/date_bubble_white.9.png | Bin 1127 -> 0 bytes .../ic_account_box_white_24dp.png | Bin 578 -> 0 bytes .../res/drawable-xxhdpi/ic_action_reply.png | Bin 775 -> 0 bytes .../res/drawable-xxhdpi/ic_add_white_24dp.png | Bin 222 -> 0 bytes .../drawable-xxhdpi/ic_android_black_48dp.png | Bin 975 -> 0 bytes .../drawable-xxhdpi/ic_android_white_48dp.png | Bin 1012 -> 0 bytes .../ic_announcement_white_24dp.png | Bin 355 -> 0 bytes .../drawable-xxhdpi/ic_archive_black_48dp.png | Bin 718 -> 0 bytes .../drawable-xxhdpi/ic_archive_white_24dp.png | Bin 390 -> 0 bytes .../drawable-xxhdpi/ic_archive_white_48dp.png | Bin 738 -> 0 bytes .../res/drawable-xxhdpi/ic_attach_camera.png | Bin 1552 -> 0 bytes .../ic_attach_camera_white.png | Bin 1085 -> 0 bytes .../drawable-xxhdpi/ic_attach_document.png | Bin 1183 -> 0 bytes .../ic_attach_document_white.png | Bin 518 -> 0 bytes .../ic_attach_file_white_24dp.png | Bin 870 -> 0 bytes .../drawable-xxhdpi/ic_attach_location.png | Bin 1628 -> 0 bytes .../ic_attach_location_white.png | Bin 1117 -> 0 bytes .../res/drawable-xxhdpi/ic_attach_photo.png | Bin 1324 -> 0 bytes .../drawable-xxhdpi/ic_attach_photo_white.png | Bin 716 -> 0 bytes .../res/drawable-xxhdpi/ic_attach_record.png | Bin 1494 -> 0 bytes .../ic_attach_record_white.png | Bin 913 -> 0 bytes .../drawable-xxhdpi/ic_attach_videocam.png | Bin 373 -> 0 bytes .../ic_attach_videocam_white.png | Bin 469 -> 0 bytes .../ic_autorenew_white_24dp.png | Bin 869 -> 0 bytes .../drawable-xxhdpi/ic_backup_black_48dp.png | Bin 1116 -> 0 bytes .../drawable-xxhdpi/ic_backup_white_48dp.png | Bin 1177 -> 0 bytes .../drawable-xxhdpi/ic_block_white_24dp.png | Bin 1194 -> 0 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 724 -> 0 bytes .../drawable-xxhdpi/ic_book_black_48dp.png | Bin 516 -> 0 bytes .../drawable-xxhdpi/ic_book_white_48dp.png | Bin 532 -> 0 bytes .../drawable-xxhdpi/ic_call_black_24dp.png | Bin 574 -> 0 bytes .../ic_call_end_white_48dp.png | Bin 1039 -> 0 bytes .../ic_call_made_black_18dp.png | Bin 202 -> 0 bytes .../ic_call_made_white_18dp.png | Bin 225 -> 0 bytes .../ic_call_missed_black_18dp.png | Bin 247 -> 0 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 235 -> 0 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 235 -> 0 bytes .../ic_call_missed_white_18dp.png | Bin 263 -> 0 bytes .../ic_call_received_black_18dp.png | Bin 202 -> 0 bytes .../ic_call_received_white_18dp.png | Bin 228 -> 0 bytes .../drawable-xxhdpi/ic_call_white_24dp.png | Bin 597 -> 0 bytes .../drawable-xxhdpi/ic_call_white_48dp.png | Bin 1134 -> 0 bytes .../ic_camera_alt_white_24dp.png | Bin 666 -> 0 bytes .../drawable-xxhdpi/ic_cancel_black_24dp.png | Bin 721 -> 0 bytes .../drawable-xxhdpi/ic_cancel_white_24dp.png | Bin 893 -> 0 bytes .../drawable-xxhdpi/ic_chat_white_24dp.png | Bin 270 -> 0 bytes .../drawable-xxhdpi/ic_clear_white_48dp.png | Bin 524 -> 0 bytes .../ic_cloud_download_white_24dp.png | Bin 610 -> 0 bytes .../ic_content_copy_white_24dp.png | Bin 266 -> 0 bytes .../drawable-xxhdpi/ic_crop_white_24dp.png | Bin 326 -> 0 bytes .../drawable-xxhdpi/ic_delete_black_24dp.png | Bin 191 -> 0 bytes .../drawable-xxhdpi/ic_delete_white_24dp.png | Bin 338 -> 0 bytes .../ic_description_black_48dp.png | Bin 514 -> 0 bytes .../ic_description_white_48dp.png | Bin 563 -> 0 bytes .../ic_directions_black_24dp.png | Bin 393 -> 0 bytes .../ic_directions_white_24dp.png | Bin 444 -> 0 bytes .../drawable-xxhdpi/ic_done_black_18dp.png | Bin 199 -> 0 bytes .../drawable-xxhdpi/ic_done_white_18dp.png | Bin 217 -> 0 bytes .../drawable-xxhdpi/ic_drafts_white_24dp.png | Bin 717 -> 0 bytes .../drawable-xxhdpi/ic_edit_black_24dp.png | Bin 269 -> 0 bytes .../drawable-xxhdpi/ic_edit_white_24dp.png | Bin 490 -> 0 bytes .../drawable-xxhdpi/ic_error_white_24dp.png | Bin 614 -> 0 bytes .../drawable-xxhdpi/ic_event_black_48dp.png | Bin 484 -> 0 bytes .../drawable-xxhdpi/ic_event_white_48dp.png | Bin 513 -> 0 bytes .../ic_file_download_white_24dp.png | Bin 351 -> 0 bytes .../ic_flip_camera_android_black_24dp.png | Bin 1340 -> 0 bytes .../drawable-xxhdpi/ic_forward_white_24dp.png | Bin 204 -> 0 bytes .../ic_gps_fixed_black_24dp.png | Bin 976 -> 0 bytes .../ic_gps_fixed_white_24dp.png | Bin 1012 -> 0 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 803 -> 0 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 830 -> 0 bytes .../ic_group_add_white_24dp.png | Bin 722 -> 0 bytes .../drawable-xxhdpi/ic_group_white_24dp.png | Bin 599 -> 0 bytes .../drawable-xxhdpi/ic_headset_black_24dp.png | Bin 586 -> 0 bytes .../drawable-xxhdpi/ic_headset_black_48dp.png | Bin 1183 -> 0 bytes .../drawable-xxhdpi/ic_headset_white_48dp.png | Bin 1246 -> 0 bytes .../drawable-xxhdpi/ic_help_black_48dp.png | Bin 1652 -> 0 bytes .../drawable-xxhdpi/ic_help_white_24dp.png | Bin 842 -> 0 bytes .../drawable-xxhdpi/ic_help_white_48dp.png | Bin 1677 -> 0 bytes .../ic_hourglass_empty_white_24dp.png | Bin 255 -> 0 bytes .../drawable-xxhdpi/ic_image_black_48dp.png | Bin 807 -> 0 bytes .../drawable-xxhdpi/ic_image_white_48dp.png | Bin 859 -> 0 bytes .../drawable-xxhdpi/ic_input_white_24dp.png | Bin 303 -> 0 bytes .../ic_link_off_white_24dp.png | Bin 650 -> 0 bytes .../drawable-xxhdpi/ic_link_white_24dp.png | Bin 670 -> 0 bytes .../drawable-xxhdpi/ic_lock_black_18dp.png | Bin 453 -> 0 bytes .../ic_lock_open_white_24dp.png | Bin 739 -> 0 bytes .../drawable-xxhdpi/ic_lock_white_18dp.png | Bin 463 -> 0 bytes .../drawable-xxhdpi/ic_lock_white_24dp.png | Bin 760 -> 0 bytes .../res/drawable-xxhdpi/ic_mic_black_24dp.png | Bin 581 -> 0 bytes .../res/drawable-xxhdpi/ic_mic_black_48dp.png | Bin 1146 -> 0 bytes .../drawable-xxhdpi/ic_mic_off_black_24dp.png | Bin 671 -> 0 bytes .../res/drawable-xxhdpi/ic_mic_white_48dp.png | Bin 1220 -> 0 bytes .../ic_mode_edit_black_18dp.png | Bin 336 -> 0 bytes .../ic_mode_edit_white_18dp.png | Bin 436 -> 0 bytes .../ic_new_releases_black_24dp.png | Bin 699 -> 0 bytes .../ic_new_releases_white_24dp.png | Bin 736 -> 0 bytes .../ic_no_results_background_black.png | Bin 8790 -> 0 bytes .../ic_no_results_background_white.png | Bin 9662 -> 0 bytes .../ic_notifications_black_24dp.png | Bin 381 -> 0 bytes .../ic_notifications_none_black_24dp.png | Bin 495 -> 0 bytes .../ic_notifications_none_white80.png | Bin 1033 -> 0 bytes .../ic_notifications_none_white_24dp.png | Bin 485 -> 0 bytes .../ic_notifications_off_black_24dp.png | Bin 690 -> 0 bytes .../ic_notifications_off_white80.png | Bin 1483 -> 0 bytes .../ic_notifications_off_white_24dp.png | Bin 683 -> 0 bytes .../ic_notifications_paused_black_24dp.png | Bin 482 -> 0 bytes .../ic_notifications_paused_white80.png | Bin 1049 -> 0 bytes .../ic_notifications_paused_white_24dp.png | Bin 482 -> 0 bytes .../ic_notifications_white80.png | Bin 831 -> 0 bytes .../ic_notifications_white_24dp.png | Bin 378 -> 0 bytes .../drawable-xxhdpi/ic_pause_black_36dp.png | Bin 143 -> 0 bytes .../drawable-xxhdpi/ic_pause_white_36dp.png | Bin 158 -> 0 bytes .../ic_person_add_white_24dp.png | Bin 683 -> 0 bytes .../drawable-xxhdpi/ic_person_black_48dp.png | Bin 846 -> 0 bytes .../drawable-xxhdpi/ic_person_white_48dp.png | Bin 863 -> 0 bytes .../ic_phone_in_talk_black_18dp.png | Bin 685 -> 0 bytes .../ic_phone_in_talk_white_18dp.png | Bin 704 -> 0 bytes .../ic_phone_in_talk_white_24dp.png | Bin 882 -> 0 bytes .../ic_play_arrow_black_36dp.png | Bin 366 -> 0 bytes .../ic_play_arrow_white_36dp.png | Bin 390 -> 0 bytes .../ic_play_circle_filled_white_48dp.png | Bin 1331 -> 0 bytes src/main/res/drawable-xxhdpi/ic_profile.png | Bin 2137 -> 0 bytes .../drawable-xxhdpi/ic_public_white_24dp.png | Bin 982 -> 0 bytes .../ic_qr_code_scan_white_24dp.png | Bin 599 -> 0 bytes .../ic_question_answer_white_24dp.png | Bin 256 -> 0 bytes .../drawable-xxhdpi/ic_refresh_black_24dp.png | Bin 721 -> 0 bytes .../drawable-xxhdpi/ic_refresh_white_24dp.png | Bin 734 -> 0 bytes .../drawable-xxhdpi/ic_replay_white_48dp.png | Bin 1390 -> 0 bytes .../drawable-xxhdpi/ic_reply_white_24dp.png | Bin 436 -> 0 bytes .../drawable-xxhdpi/ic_room_black_48dp.png | Bin 1264 -> 0 bytes .../drawable-xxhdpi/ic_room_white_24dp.png | Bin 861 -> 0 bytes .../drawable-xxhdpi/ic_room_white_48dp.png | Bin 1270 -> 0 bytes .../drawable-xxhdpi/ic_save_black_24dp.png | Bin 368 -> 0 bytes .../drawable-xxhdpi/ic_save_white_24dp.png | Bin 391 -> 0 bytes .../ic_search_background_black.png | Bin 7248 -> 0 bytes .../ic_search_background_white.png | Bin 9102 -> 0 bytes .../drawable-xxhdpi/ic_search_white_24dp.png | Bin 871 -> 0 bytes .../drawable-xxhdpi/ic_send_cancel_away.png | Bin 2583 -> 0 bytes .../drawable-xxhdpi/ic_send_cancel_dnd.png | Bin 2983 -> 0 bytes .../ic_send_cancel_offline.png | Bin 2290 -> 0 bytes .../ic_send_cancel_offline_white.png | Bin 2239 -> 0 bytes .../drawable-xxhdpi/ic_send_cancel_online.png | Bin 2974 -> 0 bytes .../drawable-xxhdpi/ic_send_file_offline.png | Bin 750 -> 0 bytes .../ic_send_file_offline_white.png | Bin 669 -> 0 bytes .../drawable-xxhdpi/ic_send_location_away.png | Bin 2229 -> 0 bytes .../drawable-xxhdpi/ic_send_location_dnd.png | Bin 2589 -> 0 bytes .../ic_send_location_offline.png | Bin 1881 -> 0 bytes .../ic_send_location_offline_white.png | Bin 1940 -> 0 bytes .../ic_send_location_online.png | Bin 2607 -> 0 bytes .../drawable-xxhdpi/ic_send_photo_away.png | Bin 2256 -> 0 bytes .../res/drawable-xxhdpi/ic_send_photo_dnd.png | Bin 2618 -> 0 bytes .../drawable-xxhdpi/ic_send_photo_offline.png | Bin 1802 -> 0 bytes .../ic_send_photo_offline_white.png | Bin 1943 -> 0 bytes .../drawable-xxhdpi/ic_send_photo_online.png | Bin 2636 -> 0 bytes .../drawable-xxhdpi/ic_send_picture_away.png | Bin 1327 -> 0 bytes .../drawable-xxhdpi/ic_send_picture_dnd.png | Bin 1516 -> 0 bytes .../ic_send_picture_offline.png | Bin 1159 -> 0 bytes .../ic_send_picture_offline_white.png | Bin 1216 -> 0 bytes .../ic_send_picture_online.png | Bin 1515 -> 0 bytes .../res/drawable-xxhdpi/ic_send_text_away.png | Bin 1776 -> 0 bytes .../res/drawable-xxhdpi/ic_send_text_dnd.png | Bin 2005 -> 0 bytes .../drawable-xxhdpi/ic_send_text_offline.png | Bin 1410 -> 0 bytes .../ic_send_text_offline_white.png | Bin 1486 -> 0 bytes .../drawable-xxhdpi/ic_send_text_online.png | Bin 1970 -> 0 bytes .../drawable-xxhdpi/ic_send_videocam_away.png | Bin 705 -> 0 bytes .../drawable-xxhdpi/ic_send_videocam_dnd.png | Bin 777 -> 0 bytes .../ic_send_videocam_offline.png | Bin 650 -> 0 bytes .../ic_send_videocam_offline_white.png | Bin 670 -> 0 bytes .../ic_send_videocam_online.png | Bin 776 -> 0 bytes .../drawable-xxhdpi/ic_send_voice_away.png | Bin 1895 -> 0 bytes .../res/drawable-xxhdpi/ic_send_voice_dnd.png | Bin 2200 -> 0 bytes .../drawable-xxhdpi/ic_send_voice_offline.png | Bin 1621 -> 0 bytes .../ic_send_voice_offline_white.png | Bin 1644 -> 0 bytes .../drawable-xxhdpi/ic_send_voice_online.png | Bin 2187 -> 0 bytes .../ic_settings_black_24dp.png | Bin 827 -> 0 bytes .../ic_settings_white_24dp.png | Bin 843 -> 0 bytes .../drawable-xxhdpi/ic_share_white_24dp.png | Bin 857 -> 0 bytes .../drawable-xxhdpi/ic_star_black_24dp.png | Bin 668 -> 0 bytes .../drawable-xxhdpi/ic_star_white_24dp.png | Bin 676 -> 0 bytes .../drawable-xxhdpi/ic_stat_alert_warning.png | Bin 2276 -> 0 bytes .../ic_stat_communication_import_export.png | Bin 1860 -> 0 bytes .../ic_verified_fingerprint.png | Bin 2306 -> 0 bytes .../ic_verified_user_black_18dp.png | Bin 516 -> 0 bytes .../ic_verified_user_white_18dp.png | Bin 538 -> 0 bytes .../ic_videocam_black_24dp.png | Bin 224 -> 0 bytes .../ic_videocam_off_black_24dp.png | Bin 375 -> 0 bytes .../ic_videocam_white_24dp.png | Bin 234 -> 0 bytes .../ic_voicemail_white_24dp.png | Bin 625 -> 0 bytes .../ic_volume_off_black_24dp.png | Bin 704 -> 0 bytes .../ic_volume_up_black_24dp.png | Bin 626 -> 0 bytes .../drawable-xxhdpi/ic_warning_white_24dp.png | Bin 639 -> 0 bytes .../drawable-xxhdpi/ic_warning_white_48dp.png | Bin 843 -> 0 bytes .../res/drawable-xxhdpi/ic_wear_reply.png | Bin 866 -> 0 bytes .../message_bubble_received.9.png | Bin 1319 -> 0 bytes .../message_bubble_received_dark.9.png | Bin 1319 -> 0 bytes .../message_bubble_received_grey.9.png | Bin 1301 -> 0 bytes .../message_bubble_received_warning.9.png | Bin 1332 -> 0 bytes .../message_bubble_received_white.9.png | Bin 1344 -> 0 bytes .../drawable-xxhdpi/message_bubble_sent.9.png | Bin 1190 -> 0 bytes .../message_bubble_sent_grey.9.png | Bin 1173 -> 0 bytes .../baseline_tour_black_48.png | Bin 809 -> 0 bytes .../baseline_tour_white_48.png | Bin 809 -> 0 bytes .../drawable-xxxhdpi/date_bubble_grey.9.png | Bin 1392 -> 0 bytes .../drawable-xxxhdpi/date_bubble_white.9.png | Bin 1430 -> 0 bytes .../ic_account_box_white_24dp.png | Bin 894 -> 0 bytes .../drawable-xxxhdpi/ic_add_white_24dp.png | Bin 269 -> 0 bytes .../ic_android_black_48dp.png | Bin 1295 -> 0 bytes .../ic_android_white_48dp.png | Bin 1350 -> 0 bytes .../ic_announcement_white_24dp.png | Bin 519 -> 0 bytes .../ic_archive_black_48dp.png | Bin 945 -> 0 bytes .../ic_archive_white_24dp.png | Bin 489 -> 0 bytes .../ic_archive_white_48dp.png | Bin 971 -> 0 bytes .../res/drawable-xxxhdpi/ic_attach_camera.png | Bin 1790 -> 0 bytes .../ic_attach_camera_white.png | Bin 1416 -> 0 bytes .../drawable-xxxhdpi/ic_attach_document.png | Bin 1276 -> 0 bytes .../ic_attach_document_white.png | Bin 627 -> 0 bytes .../ic_attach_file_white_24dp.png | Bin 1039 -> 0 bytes .../drawable-xxxhdpi/ic_attach_location.png | Bin 1872 -> 0 bytes .../ic_attach_location_white.png | Bin 1395 -> 0 bytes .../res/drawable-xxxhdpi/ic_attach_photo.png | Bin 1454 -> 0 bytes .../ic_attach_photo_white.png | Bin 900 -> 0 bytes .../res/drawable-xxxhdpi/ic_attach_record.png | Bin 1703 -> 0 bytes .../ic_attach_record_white.png | Bin 1170 -> 0 bytes .../drawable-xxxhdpi/ic_attach_videocam.png | Bin 429 -> 0 bytes .../ic_attach_videocam_white.png | Bin 547 -> 0 bytes .../ic_autorenew_white_24dp.png | Bin 1114 -> 0 bytes .../drawable-xxxhdpi/ic_backup_black_48dp.png | Bin 1465 -> 0 bytes .../drawable-xxxhdpi/ic_backup_white_48dp.png | Bin 1562 -> 0 bytes .../drawable-xxxhdpi/ic_block_white_24dp.png | Bin 1497 -> 0 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 867 -> 0 bytes .../drawable-xxxhdpi/ic_book_black_48dp.png | Bin 750 -> 0 bytes .../drawable-xxxhdpi/ic_book_white_48dp.png | Bin 766 -> 0 bytes .../drawable-xxxhdpi/ic_call_black_24dp.png | Bin 758 -> 0 bytes .../ic_call_end_white_48dp.png | Bin 1355 -> 0 bytes .../ic_call_made_black_18dp.png | Bin 212 -> 0 bytes .../ic_call_made_white_18dp.png | Bin 247 -> 0 bytes .../ic_call_missed_black_18dp.png | Bin 267 -> 0 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 257 -> 0 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 248 -> 0 bytes .../ic_call_missed_white_18dp.png | Bin 291 -> 0 bytes .../ic_call_received_black_18dp.png | Bin 214 -> 0 bytes .../ic_call_received_white_18dp.png | Bin 257 -> 0 bytes .../drawable-xxxhdpi/ic_call_white_24dp.png | Bin 778 -> 0 bytes .../drawable-xxxhdpi/ic_call_white_48dp.png | Bin 1529 -> 0 bytes .../ic_camera_alt_white_24dp.png | Bin 894 -> 0 bytes .../drawable-xxxhdpi/ic_cancel_black_24dp.png | Bin 963 -> 0 bytes .../drawable-xxxhdpi/ic_cancel_white_24dp.png | Bin 1179 -> 0 bytes .../drawable-xxxhdpi/ic_chat_white_24dp.png | Bin 344 -> 0 bytes .../drawable-xxxhdpi/ic_clear_white_48dp.png | Bin 702 -> 0 bytes .../ic_cloud_download_white_24dp.png | Bin 789 -> 0 bytes .../ic_content_copy_white_24dp.png | Bin 329 -> 0 bytes .../drawable-xxxhdpi/ic_crop_white_24dp.png | Bin 394 -> 0 bytes .../drawable-xxxhdpi/ic_delete_black_24dp.png | Bin 237 -> 0 bytes .../drawable-xxxhdpi/ic_delete_white_24dp.png | Bin 397 -> 0 bytes .../ic_description_black_48dp.png | Bin 726 -> 0 bytes .../ic_description_white_48dp.png | Bin 788 -> 0 bytes .../ic_directions_black_24dp.png | Bin 491 -> 0 bytes .../ic_directions_white_24dp.png | Bin 554 -> 0 bytes .../drawable-xxxhdpi/ic_done_black_18dp.png | Bin 227 -> 0 bytes .../drawable-xxxhdpi/ic_done_white_18dp.png | Bin 255 -> 0 bytes .../drawable-xxxhdpi/ic_drafts_white_24dp.png | Bin 953 -> 0 bytes .../drawable-xxxhdpi/ic_edit_black_24dp.png | Bin 319 -> 0 bytes .../drawable-xxxhdpi/ic_edit_white_24dp.png | Bin 632 -> 0 bytes .../drawable-xxxhdpi/ic_error_white_24dp.png | Bin 814 -> 0 bytes .../drawable-xxxhdpi/ic_event_black_48dp.png | Bin 713 -> 0 bytes .../drawable-xxxhdpi/ic_event_white_48dp.png | Bin 744 -> 0 bytes .../ic_file_download_white_24dp.png | Bin 433 -> 0 bytes .../ic_flip_camera_android_black_24dp.png | Bin 1780 -> 0 bytes .../ic_forward_white_24dp.png | Bin 236 -> 0 bytes .../ic_gps_fixed_black_24dp.png | Bin 1334 -> 0 bytes .../ic_gps_fixed_white_24dp.png | Bin 1379 -> 0 bytes .../ic_gps_not_fixed_black_24dp.png | Bin 1102 -> 0 bytes .../ic_gps_not_fixed_white_24dp.png | Bin 1139 -> 0 bytes .../ic_group_add_white_24dp.png | Bin 935 -> 0 bytes .../drawable-xxxhdpi/ic_group_white_24dp.png | Bin 759 -> 0 bytes .../ic_headset_black_24dp.png | Bin 786 -> 0 bytes .../ic_headset_black_48dp.png | Bin 1574 -> 0 bytes .../ic_headset_white_48dp.png | Bin 1736 -> 0 bytes .../drawable-xxxhdpi/ic_help_black_48dp.png | Bin 2217 -> 0 bytes .../drawable-xxxhdpi/ic_help_white_24dp.png | Bin 1132 -> 0 bytes .../drawable-xxxhdpi/ic_help_white_48dp.png | Bin 2356 -> 0 bytes .../ic_hourglass_empty_white_24dp.png | Bin 273 -> 0 bytes .../drawable-xxxhdpi/ic_image_black_48dp.png | Bin 1108 -> 0 bytes .../drawable-xxxhdpi/ic_image_white_48dp.png | Bin 1178 -> 0 bytes .../drawable-xxxhdpi/ic_input_white_24dp.png | Bin 382 -> 0 bytes .../ic_link_off_white_24dp.png | Bin 811 -> 0 bytes .../drawable-xxxhdpi/ic_link_white_24dp.png | Bin 865 -> 0 bytes .../drawable-xxxhdpi/ic_lock_black_18dp.png | Bin 527 -> 0 bytes .../ic_lock_open_white_24dp.png | Bin 946 -> 0 bytes .../drawable-xxxhdpi/ic_lock_white_18dp.png | Bin 540 -> 0 bytes .../drawable-xxxhdpi/ic_lock_white_24dp.png | Bin 971 -> 0 bytes .../drawable-xxxhdpi/ic_mic_black_24dp.png | Bin 773 -> 0 bytes .../drawable-xxxhdpi/ic_mic_black_48dp.png | Bin 1529 -> 0 bytes .../ic_mic_off_black_24dp.png | Bin 832 -> 0 bytes .../drawable-xxxhdpi/ic_mic_white_48dp.png | Bin 1664 -> 0 bytes .../ic_mode_edit_black_18dp.png | Bin 366 -> 0 bytes .../ic_mode_edit_white_18dp.png | Bin 490 -> 0 bytes .../ic_new_releases_black_24dp.png | Bin 897 -> 0 bytes .../ic_new_releases_white_24dp.png | Bin 932 -> 0 bytes .../ic_no_results_background_black.png | Bin 12852 -> 0 bytes .../ic_no_results_background_white.png | Bin 13693 -> 0 bytes .../ic_notifications_black_24dp.png | Bin 493 -> 0 bytes .../ic_notifications_none_black_24dp.png | Bin 641 -> 0 bytes .../ic_notifications_none_white80.png | Bin 1288 -> 0 bytes .../ic_notifications_none_white_24dp.png | Bin 633 -> 0 bytes .../ic_notifications_off_black_24dp.png | Bin 885 -> 0 bytes .../ic_notifications_off_white80.png | Bin 1903 -> 0 bytes .../ic_notifications_off_white_24dp.png | Bin 877 -> 0 bytes .../ic_notifications_paused_black_24dp.png | Bin 618 -> 0 bytes .../ic_notifications_paused_white80.png | Bin 1308 -> 0 bytes .../ic_notifications_paused_white_24dp.png | Bin 616 -> 0 bytes .../ic_notifications_white80.png | Bin 1053 -> 0 bytes .../ic_notifications_white_24dp.png | Bin 482 -> 0 bytes .../drawable-xxxhdpi/ic_pause_black_36dp.png | Bin 127 -> 0 bytes .../drawable-xxxhdpi/ic_pause_white_36dp.png | Bin 110 -> 0 bytes .../ic_person_add_white_24dp.png | Bin 909 -> 0 bytes .../drawable-xxxhdpi/ic_person_black_48dp.png | Bin 1121 -> 0 bytes .../drawable-xxxhdpi/ic_person_white_48dp.png | Bin 1140 -> 0 bytes .../ic_phone_in_talk_black_18dp.png | Bin 868 -> 0 bytes .../ic_phone_in_talk_white_18dp.png | Bin 882 -> 0 bytes .../ic_phone_in_talk_white_24dp.png | Bin 1162 -> 0 bytes .../ic_play_arrow_black_36dp.png | Bin 394 -> 0 bytes .../ic_play_arrow_white_36dp.png | Bin 461 -> 0 bytes .../ic_play_circle_filled_white_48dp.png | Bin 1826 -> 0 bytes .../drawable-xxxhdpi/ic_public_white_24dp.png | Bin 1288 -> 0 bytes .../ic_qr_code_scan_white_24dp.png | Bin 754 -> 0 bytes .../ic_question_answer_white_24dp.png | Bin 320 -> 0 bytes .../ic_refresh_black_24dp.png | Bin 948 -> 0 bytes .../ic_refresh_white_24dp.png | Bin 967 -> 0 bytes .../drawable-xxxhdpi/ic_replay_white_48dp.png | Bin 1885 -> 0 bytes .../drawable-xxxhdpi/ic_reply_white_24dp.png | Bin 579 -> 0 bytes .../drawable-xxxhdpi/ic_room_black_48dp.png | Bin 1691 -> 0 bytes .../drawable-xxxhdpi/ic_room_white_24dp.png | Bin 1093 -> 0 bytes .../drawable-xxxhdpi/ic_room_white_48dp.png | Bin 1709 -> 0 bytes .../drawable-xxxhdpi/ic_save_black_24dp.png | Bin 477 -> 0 bytes .../drawable-xxxhdpi/ic_save_white_24dp.png | Bin 504 -> 0 bytes .../ic_search_background_black.png | Bin 10595 -> 0 bytes .../ic_search_background_white.png | Bin 12848 -> 0 bytes .../drawable-xxxhdpi/ic_search_white_24dp.png | Bin 1090 -> 0 bytes .../drawable-xxxhdpi/ic_send_cancel_away.png | Bin 3377 -> 0 bytes .../drawable-xxxhdpi/ic_send_cancel_dnd.png | Bin 3862 -> 0 bytes .../ic_send_cancel_offline.png | Bin 2956 -> 0 bytes .../ic_send_cancel_offline_white.png | Bin 2987 -> 0 bytes .../ic_send_cancel_online.png | Bin 3861 -> 0 bytes .../ic_send_location_away.png | Bin 3015 -> 0 bytes .../drawable-xxxhdpi/ic_send_location_dnd.png | Bin 3514 -> 0 bytes .../ic_send_location_offline.png | Bin 2523 -> 0 bytes .../ic_send_location_offline_white.png | Bin 2563 -> 0 bytes .../ic_send_location_online.png | Bin 3519 -> 0 bytes .../drawable-xxxhdpi/ic_send_photo_away.png | Bin 3040 -> 0 bytes .../drawable-xxxhdpi/ic_send_photo_dnd.png | Bin 3569 -> 0 bytes .../ic_send_photo_offline.png | Bin 2491 -> 0 bytes .../ic_send_photo_offline_white.png | Bin 2631 -> 0 bytes .../drawable-xxxhdpi/ic_send_photo_online.png | Bin 3567 -> 0 bytes .../drawable-xxxhdpi/ic_send_picture_away.png | Bin 1632 -> 0 bytes .../drawable-xxxhdpi/ic_send_picture_dnd.png | Bin 1784 -> 0 bytes .../ic_send_picture_offline.png | Bin 1428 -> 0 bytes .../ic_send_picture_offline_white.png | Bin 1483 -> 0 bytes .../ic_send_picture_online.png | Bin 1777 -> 0 bytes .../drawable-xxxhdpi/ic_send_text_away.png | Bin 2311 -> 0 bytes .../res/drawable-xxxhdpi/ic_send_text_dnd.png | Bin 2592 -> 0 bytes .../drawable-xxxhdpi/ic_send_text_offline.png | Bin 1862 -> 0 bytes .../ic_send_text_offline_white.png | Bin 1974 -> 0 bytes .../drawable-xxxhdpi/ic_send_text_online.png | Bin 2665 -> 0 bytes .../ic_send_videocam_away.png | Bin 821 -> 0 bytes .../drawable-xxxhdpi/ic_send_videocam_dnd.png | Bin 884 -> 0 bytes .../ic_send_videocam_offline.png | Bin 772 -> 0 bytes .../ic_send_videocam_offline_white.png | Bin 777 -> 0 bytes .../ic_send_videocam_online.png | Bin 894 -> 0 bytes .../drawable-xxxhdpi/ic_send_voice_away.png | Bin 2491 -> 0 bytes .../drawable-xxxhdpi/ic_send_voice_dnd.png | Bin 2849 -> 0 bytes .../ic_send_voice_offline.png | Bin 2092 -> 0 bytes .../ic_send_voice_offline_white.png | Bin 2181 -> 0 bytes .../drawable-xxxhdpi/ic_send_voice_online.png | Bin 2895 -> 0 bytes .../ic_settings_black_24dp.png | Bin 1073 -> 0 bytes .../ic_settings_white_24dp.png | Bin 1074 -> 0 bytes .../drawable-xxxhdpi/ic_share_white_24dp.png | Bin 1115 -> 0 bytes .../drawable-xxxhdpi/ic_star_black_24dp.png | Bin 887 -> 0 bytes .../drawable-xxxhdpi/ic_star_white_24dp.png | Bin 890 -> 0 bytes .../ic_verified_fingerprint.png | Bin 2773 -> 0 bytes .../ic_verified_user_black_18dp.png | Bin 634 -> 0 bytes .../ic_verified_user_white_18dp.png | Bin 668 -> 0 bytes .../ic_videocam_black_24dp.png | Bin 270 -> 0 bytes .../ic_videocam_off_black_24dp.png | Bin 447 -> 0 bytes .../ic_videocam_white_24dp.png | Bin 290 -> 0 bytes .../ic_voicemail_white_24dp.png | Bin 971 -> 0 bytes .../ic_volume_off_black_24dp.png | Bin 924 -> 0 bytes .../ic_volume_up_black_24dp.png | Bin 828 -> 0 bytes .../ic_warning_white_24dp.png | Bin 887 -> 0 bytes .../ic_warning_white_48dp.png | Bin 1044 -> 0 bytes .../message_bubble_received.9.png | Bin 1713 -> 0 bytes .../message_bubble_received_dark.9.png | Bin 1691 -> 0 bytes .../message_bubble_received_grey.9.png | Bin 1670 -> 0 bytes .../message_bubble_received_warning.9.png | Bin 1696 -> 0 bytes .../message_bubble_received_white.9.png | Bin 1705 -> 0 bytes .../message_bubble_sent.9.png | Bin 1499 -> 0 bytes .../message_bubble_sent_grey.9.png | Bin 1468 -> 0 bytes ...=> background_account_profile_picture.xml} | 6 +- .../res/drawable/background_list_item.xml | 4 + .../drawable/background_message_bubble.xml | 5 + ...y.xml => background_message_separator.xml} | 8 +- .../res/drawable/background_no_results.xml | 7 + src/main/res/drawable/background_search.xml | 7 + .../background_selectable_list_item.xml | 5 + .../background_selected_item_conversation.xml | 7 + ...round.xml => background_splash_screen.xml} | 2 +- .../drawable/background_surface_container.xml | 5 + .../res/drawable/es_slidingpane_shadow.xml | 12 - src/main/res/drawable/greybackground.xml | 6 - src/main/res/drawable/ic_adb_48dp.xml | 5 + ...croll_to_end_black.xml => ic_add_24dp.xml} | 9 +- .../res/drawable/ic_announcement_24dp.xml | 13 + src/main/res/drawable/ic_archive_24dp.xml | 12 + src/main/res/drawable/ic_archive_48dp.xml | 12 + src/main/res/drawable/ic_attach_file_24dp.xml | 10 + src/main/res/drawable/ic_autorenew_24dp.xml | 12 + src/main/res/drawable/ic_backup_48dp.xml | 12 + .../res/drawable/ic_bluetooth_audio_24dp.xml | 12 + src/main/res/drawable/ic_book_48dp.xml | 5 + src/main/res/drawable/ic_call_24dp.xml | 12 + .../res/drawable/ic_call_black54_24dp.xml | 4 - src/main/res/drawable/ic_call_end_24dp.xml | 12 + src/main/res/drawable/ic_call_made_24dp.xml | 5 + src/main/res/drawable/ic_call_missed_24db.xml | 11 + src/main/res/drawable/ic_call_missed_24dp.xml | 5 + .../drawable/ic_call_missed_outgoing_24dp.xml | 5 + .../drawable/ic_call_missed_white_24db.xml | 5 - .../res/drawable/ic_call_received_24dp.xml | 5 + .../res/drawable/ic_call_white70_24dp.xml | 4 - src/main/res/drawable/ic_camera_alt_24dp.xml | 13 + src/main/res/drawable/ic_cancel_24dp.xml | 12 + src/main/res/drawable/ic_cancel_96dp.xml | 12 + src/main/res/drawable/ic_chat_24dp.xml | 13 + src/main/res/drawable/ic_check_24dp.xml | 10 + src/main/res/drawable/ic_clear_24dp.xml | 12 + .../res/drawable/ic_cloud_download_24dp.xml | 12 + src/main/res/drawable/ic_code_48dp.xml | 12 + .../res/drawable/ic_content_copy_24dp.xml | 12 + src/main/res/drawable/ic_delete_24dp.xml | 10 + src/main/res/drawable/ic_description_24dp.xml | 10 + src/main/res/drawable/ic_description_48dp.xml | 5 + src/main/res/drawable/ic_directions_24dp.xml | 5 + .../res/drawable/ic_directions_black_24dp.xml | 9 - src/main/res/drawable/ic_done_24dp.xml | 10 + src/main/res/drawable/ic_download_24dp.xml | 5 + src/main/res/drawable/ic_edit_24dp.xml | 10 + src/main/res/drawable/ic_email_48dp.xml | 12 + src/main/res/drawable/ic_error_24dp.xml | 12 + src/main/res/drawable/ic_event_48dp.xml | 12 + .../drawable/ic_flip_camera_android_24dp.xml | 20 + src/main/res/drawable/ic_forum_24dp.xml | 12 + src/main/res/drawable/ic_gps_fixed_24dp.xml | 5 + .../res/drawable/ic_gps_fixed_black_24dp.xml | 9 - .../res/drawable/ic_gps_not_fixed_24dp.xml | 5 + .../drawable/ic_gps_not_fixed_black_24dp.xml | 9 - src/main/res/drawable/ic_group_24dp.xml | 5 + src/main/res/drawable/ic_headphones_48dp.xml | 12 + src/main/res/drawable/ic_headset_mic_24dp.xml | 12 + src/main/res/drawable/ic_help_24dp.xml | 13 + src/main/res/drawable/ic_help_center_48dp.xml | 5 + .../res/drawable/ic_hourglass_top_24dp.xml | 5 + src/main/res/drawable/ic_image_24dp.xml | 10 + src/main/res/drawable/ic_image_48dp.xml | 5 + .../ic_keyboard_double_arrow_down_24dp.xml | 16 + src/main/res/drawable/ic_link_24dp.xml | 12 + src/main/res/drawable/ic_link_off_24dp.xml | 12 + .../res/drawable/ic_location_pin_24dp.xml | 10 + .../res/drawable/ic_location_pin_48dp.xml | 5 + src/main/res/drawable/ic_lock_24dp.xml | 10 + .../drawable/ic_lock_open_outline_24dp.xml | 10 + src/main/res/drawable/ic_login_24dp.xml | 5 + src/main/res/drawable/ic_logout_24dp.xml | 11 + .../res/drawable/ic_logout_white_24dp.xml | 5 - .../res/drawable/ic_mark_chat_read_24dp.xml | 12 + src/main/res/drawable/ic_mic_24dp.xml | 10 + src/main/res/drawable/ic_mic_48dp.xml | 5 + src/main/res/drawable/ic_mic_off_24dp.xml | 12 + src/main/res/drawable/ic_movie_48dp.xml | 12 + .../res/drawable/ic_navigate_next_24dp.xml | 13 + ..._check_24.xml => ic_new_releases_24dp.xml} | 2 +- .../res/drawable/ic_notifications_24dp.xml | 12 + .../drawable/ic_notifications_none_24dp.xml | 12 + .../drawable/ic_notifications_off_24dp.xml | 12 + .../drawable/ic_notifications_paused_24dp.xml | 12 + src/main/res/drawable/ic_pause_24dp.xml | 10 + src/main/res/drawable/ic_person_24dp.xml | 5 + src/main/res/drawable/ic_person_48dp.xml | 12 + src/main/res/drawable/ic_person_add_24dp.xml | 12 + .../res/drawable/ic_phone_in_talk_24dp.xml | 10 + src/main/res/drawable/ic_place_black_24dp.xml | 9 - src/main/res/drawable/ic_play_arrow_24dp.xml | 10 + src/main/res/drawable/ic_play_circle_24dp.xml | 5 + src/main/res/drawable/ic_play_lesson_48dp.xml | 13 + .../res/drawable/ic_play_lesson_black_24.xml | 6 - .../drawable/ic_play_lesson_white_48dp.xml | 6 - src/main/res/drawable/ic_public_24dp.xml | 5 + src/main/res/drawable/ic_qr_code_24dp.xml | 10 + .../res/drawable/ic_qr_code_black_24dp.xml | 40 -- .../res/drawable/ic_qr_code_scanner_24dp.xml | 10 + .../res/drawable/ic_qr_code_white_24dp.xml | 40 -- src/main/res/drawable/ic_refresh_24dp.xml | 5 + src/main/res/drawable/ic_replay_24dp.xml | 12 + src/main/res/drawable/ic_reply_24dp.xml | 13 + src/main/res/drawable/ic_save_24dp.xml | 12 + .../res/drawable/ic_scroll_to_end_white.xml | 36 -- src/main/res/drawable/ic_search_128dp.xml | 12 + src/main/res/drawable/ic_search_24dp.xml | 12 + src/main/res/drawable/ic_send_24dp.xml | 13 + src/main/res/drawable/ic_settings_24dp.xml | 10 + src/main/res/drawable/ic_share_24dp.xml | 10 + src/main/res/drawable/ic_star_24dp.xml | 12 + src/main/res/drawable/ic_tour_48dp.xml | 5 + .../res/drawable/ic_travel_explore_24dp.xml | 5 + src/main/res/drawable/ic_unarchive_24dp.xml | 5 + .../res/drawable/ic_verified_user_24dp.xml | 10 + src/main/res/drawable/ic_videocam_24dp.xml | 11 + .../res/drawable/ic_videocam_black54_24dp.xml | 4 - .../res/drawable/ic_videocam_off_24dp.xml | 12 + .../res/drawable/ic_videocam_white70_24dp.xml | 4 - src/main/res/drawable/ic_voicemail_24dp.xml | 12 + src/main/res/drawable/ic_volume_off_24dp.xml | 13 + src/main/res/drawable/ic_volume_up_24dp.xml | 13 + src/main/res/drawable/ic_warning_24dp.xml | 12 + .../drawable/list_item_background_dark.xml | 34 -- .../drawable/list_item_background_light.xml | 34 -- .../drawable/no_results_background_dark.xml | 41 -- .../drawable/no_results_background_light.xml | 41 -- .../no_results_primary_background_dark.xml | 41 -- .../no_results_primary_background_light.xml | 41 -- .../res/drawable/search_background_dark.xml | 41 -- .../res/drawable/search_background_light.xml | 41 -- src/main/res/drawable/snackbar.xml | 4 +- .../drawable/visibility_toggle_drawable.xml | 5 - src/main/res/drawable/white_cursor.xml | 33 -- .../layout-w945dp/activity_conversations.xml | 18 +- src/main/res/layout/actionview_search.xml | 2 +- src/main/res/layout/activity_about.xml | 48 +- .../res/layout/activity_change_password.xml | 160 +++---- .../res/layout/activity_channel_discovery.xml | 19 +- .../res/layout/activity_choose_contact.xml | 26 +- .../res/layout/activity_contact_details.xml | 107 ++--- .../res/layout/activity_conversations.xml | 43 +- src/main/res/layout/activity_edit_account.xml | 227 +++++---- .../res/layout/activity_manage_accounts.xml | 39 +- .../res/layout/activity_media_browser.xml | 20 +- src/main/res/layout/activity_muc_details.xml | 170 +++---- src/main/res/layout/activity_muc_users.xml | 19 +- .../activity_publish_profile_picture.xml | 77 ++-- src/main/res/layout/activity_recording.xml | 43 +- src/main/res/layout/activity_rtp_session.xml | 95 ++-- src/main/res/layout/activity_search.xml | 45 +- src/main/res/layout/activity_settings.xml | 32 +- .../res/layout/activity_share_location.xml | 55 ++- src/main/res/layout/activity_share_with.xml | 42 +- .../res/layout/activity_show_location.xml | 29 +- .../layout/activity_start_conversation.xml | 51 +- src/main/res/layout/activity_trust_keys.xml | 172 +++---- src/main/res/layout/activity_uri_handler.xml | 8 +- src/main/res/layout/captcha.xml | 12 +- src/main/res/layout/contact_key.xml | 37 +- .../res/layout/create_conference_dialog.xml | 40 -- src/main/res/layout/dialog_block_contact.xml | 10 +- src/main/res/layout/dialog_clear_history.xml | 16 +- .../res/layout/dialog_create_conference.xml | 36 ++ ...g.xml => dialog_create_public_channel.xml} | 36 +- src/main/res/layout/dialog_delete_account.xml | 16 +- src/main/res/layout/dialog_enter_jid.xml | 40 ++ .../res/layout/dialog_join_conference.xml | 39 +- src/main/res/layout/dialog_presence.xml | 31 +- src/main/res/layout/dialog_quickedit.xml | 10 +- .../res/layout/dialog_verify_fingerprints.xml | 10 +- src/main/res/layout/enter_jid_dialog.xml | 42 -- src/main/res/layout/form_boolean.xml | 11 - src/main/res/layout/form_text.xml | 17 - src/main/res/layout/fragment_conversation.xml | 92 ++-- .../fragment_conversations_overview.xml | 13 +- .../{account_row.xml => item_account.xml} | 19 +- src/main/res/layout/item_autocomplete.xml | 11 + ...lt_item.xml => item_channel_discovery.xml} | 23 +- .../layout/{contact.xml => item_contact.xml} | 31 +- ...ion_list_row.xml => item_conversation.xml} | 103 +++-- .../res/layout/{media.xml => item_media.xml} | 10 +- ...dia_preview.xml => item_media_preview.xml} | 15 +- src/main/res/layout/item_message_content.xml | 68 +++ .../res/layout/item_message_date_bubble.xml | 30 ++ src/main/res/layout/item_message_received.xml | 97 ++++ .../res/layout/item_message_rtp_session.xml | 42 ++ src/main/res/layout/item_message_sent.xml | 105 +++++ src/main/res/layout/item_message_status.xml | 48 ++ ...user_preview.xml => item_user_preview.xml} | 11 +- src/main/res/layout/keys_card.xml | 20 +- src/main/res/layout/list_item_tag.xml | 21 +- src/main/res/layout/message_content.xml | 66 --- src/main/res/layout/message_date_bubble.xml | 27 -- src/main/res/layout/message_received.xml | 94 ---- src/main/res/layout/message_rtp_session.xml | 38 -- src/main/res/layout/message_sent.xml | 108 ----- src/main/res/layout/message_status.xml | 46 -- src/main/res/layout/presence_template.xml | 47 -- src/main/res/layout/simple_list_item.xml | 25 - src/main/res/layout/toolbar.xml | 11 - src/main/res/menu/activity_conversations.xml | 4 +- src/main/res/menu/activity_rtp_session.xml | 15 +- src/main/res/menu/activity_search.xml | 2 +- .../res/menu/channel_discovery_activity.xml | 2 +- src/main/res/menu/choose_contact.xml | 6 +- src/main/res/menu/contact_details.xml | 4 +- src/main/res/menu/editaccount.xml | 4 +- src/main/res/menu/fragment_conversation.xml | 26 +- src/main/res/menu/import_backup.xml | 2 +- src/main/res/menu/menu_show_location.xml | 4 +- src/main/res/menu/muc_details.xml | 4 +- src/main/res/menu/muc_users_activity.xml | 2 +- src/main/res/menu/share_with.xml | 2 +- src/main/res/menu/start_conversation.xml | 4 +- .../menu/start_conversation_fab_submenu.xml | 16 +- src/main/res/menu/trust_keys.xml | 2 +- src/main/res/values-ar/strings.xml | 7 - src/main/res/values-bg/strings.xml | 8 - src/main/res/values-bn-rIN/strings.xml | 1 - src/main/res/values-ca/strings.xml | 8 - src/main/res/values-cs/strings.xml | 8 - src/main/res/values-da-rDK/strings.xml | 8 - src/main/res/values-de/strings.xml | 9 - src/main/res/values-el/strings.xml | 8 - src/main/res/values-es/strings.xml | 9 - src/main/res/values-eu/strings.xml | 8 - src/main/res/values-fa-rIR/strings.xml | 8 - src/main/res/values-fi/strings.xml | 7 - src/main/res/values-fr/strings.xml | 8 - src/main/res/values-gl/strings.xml | 9 - src/main/res/values-hr/strings.xml | 1 - src/main/res/values-hu/strings.xml | 8 - src/main/res/values-id/strings.xml | 6 - src/main/res/values-it/strings.xml | 8 - src/main/res/values-iw/strings.xml | 1 - src/main/res/values-ja/strings.xml | 9 - src/main/res/values-ko/strings.xml | 4 - src/main/res/values-nb-rNO/strings.xml | 4 - src/main/res/values-night/colors.xml | 5 - src/main/res/values-night/themes.xml | 32 ++ src/main/res/values-nl/strings.xml | 8 - src/main/res/values-pl/strings.xml | 9 - src/main/res/values-pt-rBR/strings.xml | 8 - src/main/res/values-pt/strings.xml | 4 - src/main/res/values-ro-rRO/strings.xml | 9 - src/main/res/values-ru/strings.xml | 9 - src/main/res/values-sk/strings.xml | 4 - src/main/res/values-sq-rAL/strings.xml | 8 - src/main/res/values-sr/strings.xml | 5 - src/main/res/values-sv/strings.xml | 8 - src/main/res/values-szl/strings.xml | 8 - src/main/res/values-tr-rTR/strings.xml | 8 - src/main/res/values-uk/strings.xml | 9 - src/main/res/values-vi/strings.xml | 8 - src/main/res/values-zh-rCN/strings.xml | 9 - src/main/res/values-zh-rTW/strings.xml | 8 - src/main/res/values/arrays.xml | 11 - src/main/res/values/attrs.xml | 127 +---- src/main/res/values/colors-md.xml | 43 ++ src/main/res/values/colors.xml | 47 -- src/main/res/values/strings.xml | 17 +- src/main/res/values/styles.xml | 162 ------- src/main/res/values/themes.xml | 436 ++---------------- src/main/res/xml/preferences.xml | 39 +- .../ui/ChooseCountryActivity.java | 7 +- .../conversations/ui/EnterNameActivity.java | 4 +- .../ui/EnterPhoneNumberActivity.java | 3 +- .../siacs/conversations/ui/TosActivity.java | 26 +- .../conversations/ui/VerifyActivity.java | 7 +- .../ui/drawable/TextDrawable.java | 7 +- .../res/layout/activity_choose_country.xml | 20 +- .../res/layout/activity_enter_name.xml | 44 +- .../res/layout/activity_enter_number.xml | 33 +- src/quicksy/res/layout/activity_tos.xml | 148 +++--- src/quicksy/res/layout/activity_verify.xml | 34 +- src/quicksy/res/layout/country_item.xml | 13 +- ...se_country.xml => item_choose_country.xml} | 2 +- src/quicksy/res/values/colors-themed.xml | 64 +++ 1427 files changed, 5148 insertions(+), 5856 deletions(-) delete mode 100644 src/conversations/res/drawable-hdpi/ic_unarchive_white_24dp.png delete mode 100644 src/conversations/res/drawable-mdpi/ic_unarchive_white_24dp.png delete mode 100644 src/conversations/res/drawable-xhdpi/ic_unarchive_white_24dp.png delete mode 100644 src/conversations/res/drawable-xxhdpi/ic_unarchive_white_24dp.png delete mode 100644 src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png rename src/conversations/res/layout/{magic_create.xml => activity_magic_create.xml} (64%) create mode 100644 src/conversations/res/values/colors-themed.xml create mode 100644 src/main/java/eu/siacs/conversations/Conversations.java create mode 100644 src/main/java/eu/siacs/conversations/ui/Activities.java create mode 100644 src/main/java/eu/siacs/conversations/ui/BaseActivity.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java delete mode 100644 src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/ToolbarUtils.java delete mode 100644 src/main/java/eu/siacs/conversations/utils/ThemeHelper.java delete mode 100644 src/main/res/drawable-hdpi/baseline_tour_black_48.png delete mode 100644 src/main/res/drawable-hdpi/baseline_tour_white_48.png delete mode 100644 src/main/res/drawable-hdpi/date_bubble_grey.9.png delete mode 100644 src/main/res/drawable-hdpi/date_bubble_white.9.png delete mode 100644 src/main/res/drawable-hdpi/ic_account_box_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_action_reply.png delete mode 100644 src/main/res/drawable-hdpi/ic_add_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_android_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_android_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_announcement_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_archive_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_archive_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_archive_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_camera.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_camera_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_document.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_document_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_file_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_location.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_location_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_photo.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_photo_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_record.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_record_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_videocam.png delete mode 100644 src/main/res/drawable-hdpi/ic_attach_videocam_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_autorenew_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_backup_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_backup_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_block_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_bluetooth_audio_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_book_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_book_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_end_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_made_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_made_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_received_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_received_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_call_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_camera_alt_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_cancel_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_cancel_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_chat_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_clear_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_cloud_download_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_content_copy_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_crop_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_delete_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_delete_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_description_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_description_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_directions_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_directions_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_done_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_done_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_drafts_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_edit_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_edit_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_error_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_event_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_event_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_file_download_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_flip_camera_android_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_forward_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_group_add_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_group_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_headset_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_headset_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_headset_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_help_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_help_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_help_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_image_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_image_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_input_white_24dp.png delete mode 100755 src/main/res/drawable-hdpi/ic_link_off_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_link_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_lock_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_lock_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_lock_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mic_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mic_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mic_off_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mic_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mode_edit_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_mode_edit_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_new_releases_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_new_releases_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_no_results_background_black.png delete mode 100644 src/main/res/drawable-hdpi/ic_no_results_background_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_none_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_none_white80.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_none_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_off_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_off_white80.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_off_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_paused_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_paused_white80.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_paused_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_white80.png delete mode 100644 src/main/res/drawable-hdpi/ic_notifications_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_pause_black_36dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_pause_white_36dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_person_add_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_person_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_person_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_play_arrow_black_36dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_play_arrow_white_36dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_play_circle_filled_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_profile.png delete mode 100644 src/main/res/drawable-hdpi/ic_public_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_qr_code_scan_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_question_answer_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_refresh_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_refresh_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_replay_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_reply_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_room_black_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_room_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_room_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_save_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_save_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_search_background_black.png delete mode 100644 src/main/res/drawable-hdpi/ic_search_background_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_search_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_cancel_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_file_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_file_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_location_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_photo_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_picture_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_text_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_videocam_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_videocam_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_videocam_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_videocam_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_videocam_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_away.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_dnd.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_offline.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_offline_dark.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_offline_white.png delete mode 100644 src/main/res/drawable-hdpi/ic_send_voice_online.png delete mode 100644 src/main/res/drawable-hdpi/ic_settings_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_settings_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_share_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_star_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_star_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_stat_alert_warning.png delete mode 100644 src/main/res/drawable-hdpi/ic_stat_communication_import_export.png delete mode 100644 src/main/res/drawable-hdpi/ic_verified_fingerprint.png delete mode 100644 src/main/res/drawable-hdpi/ic_verified_user_black_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_verified_user_white_18dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_videocam_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_videocam_off_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_videocam_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_voicemail_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_volume_up_black_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_warning_white_24dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_warning_white_48dp.png delete mode 100644 src/main/res/drawable-hdpi/ic_wear_reply.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_received.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_received_dark.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_received_grey.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_received_warning.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_received_white.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_sent.9.png delete mode 100644 src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png delete mode 100644 src/main/res/drawable-mdpi/baseline_tour_black_48.png delete mode 100644 src/main/res/drawable-mdpi/baseline_tour_white_48.png delete mode 100644 src/main/res/drawable-mdpi/date_bubble_grey.9.png delete mode 100644 src/main/res/drawable-mdpi/date_bubble_white.9.png delete mode 100644 src/main/res/drawable-mdpi/ic_account_box_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_action_reply.png delete mode 100644 src/main/res/drawable-mdpi/ic_add_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_android_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_android_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_announcement_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_archive_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_archive_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_archive_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_camera.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_camera_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_document.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_document_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_file_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_location.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_location_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_photo.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_photo_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_record.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_record_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_videocam.png delete mode 100644 src/main/res/drawable-mdpi/ic_attach_videocam_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_autorenew_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_backup_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_backup_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_block_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_bluetooth_audio_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_book_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_book_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_end_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_made_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_made_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_received_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_received_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_call_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_camera_alt_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_cancel_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_cancel_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_chat_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_clear_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_cloud_download_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_content_copy_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_crop_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_delete_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_delete_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_description_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_description_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_directions_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_directions_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_done_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_done_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_drafts_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_edit_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_edit_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_error_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_event_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_event_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_file_download_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_flip_camera_android_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_forward_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_group_add_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_group_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_headset_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_headset_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_headset_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_help_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_help_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_help_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_image_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_image_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_input_white_24dp.png delete mode 100755 src/main/res/drawable-mdpi/ic_link_off_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_link_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_lock_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_lock_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_lock_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mic_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mic_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mic_off_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mic_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mode_edit_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_mode_edit_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_new_releases_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_new_releases_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_no_results_background_black.png delete mode 100644 src/main/res/drawable-mdpi/ic_no_results_background_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_none_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_none_white80.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_none_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_off_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_off_white80.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_off_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_paused_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_paused_white80.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_paused_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_white80.png delete mode 100644 src/main/res/drawable-mdpi/ic_notifications_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_pause_black_36dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_pause_white_36dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_person_add_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_person_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_person_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_play_arrow_black_36dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_play_arrow_white_36dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_play_circle_filled_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_profile.png delete mode 100644 src/main/res/drawable-mdpi/ic_public_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_qr_code_scan_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_question_answer_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_refresh_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_refresh_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_replay_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_reply_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_room_black_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_room_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_room_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_save_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_save_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_search_background_black.png delete mode 100644 src/main/res/drawable-mdpi/ic_search_background_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_search_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_cancel_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_file_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_file_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_location_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_photo_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_picture_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_text_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_videocam_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_videocam_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_videocam_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_videocam_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_videocam_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_away.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_dnd.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_offline.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_offline_dark.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_offline_white.png delete mode 100644 src/main/res/drawable-mdpi/ic_send_voice_online.png delete mode 100644 src/main/res/drawable-mdpi/ic_settings_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_settings_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_share_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_star_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_star_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_stat_alert_warning.png delete mode 100644 src/main/res/drawable-mdpi/ic_stat_communication_import_export.png delete mode 100644 src/main/res/drawable-mdpi/ic_verified_fingerprint.png delete mode 100644 src/main/res/drawable-mdpi/ic_verified_user_black_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_verified_user_white_18dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_videocam_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_videocam_off_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_videocam_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_voicemail_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_volume_up_black_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_warning_white_24dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_warning_white_48dp.png delete mode 100644 src/main/res/drawable-mdpi/ic_wear_reply.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_received.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_received_dark.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_received_grey.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_received_warning.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_received_white.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_sent.9.png delete mode 100644 src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png delete mode 100644 src/main/res/drawable-mdpi/play_gif_black.png delete mode 100644 src/main/res/drawable-mdpi/play_gif_white.png delete mode 100644 src/main/res/drawable-mdpi/play_video_black.png delete mode 100644 src/main/res/drawable-mdpi/play_video_white.png delete mode 100644 src/main/res/drawable-xhdpi/baseline_tour_black_48.png delete mode 100644 src/main/res/drawable-xhdpi/baseline_tour_white_48.png delete mode 100644 src/main/res/drawable-xhdpi/date_bubble_grey.9.png delete mode 100644 src/main/res/drawable-xhdpi/date_bubble_white.9.png delete mode 100644 src/main/res/drawable-xhdpi/ic_account_box_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_action_reply.png delete mode 100644 src/main/res/drawable-xhdpi/ic_add_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_android_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_android_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_announcement_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_archive_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_archive_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_archive_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_camera.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_camera_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_document.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_document_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_file_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_location.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_location_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_photo.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_photo_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_record.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_record_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_videocam.png delete mode 100644 src/main/res/drawable-xhdpi/ic_attach_videocam_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_autorenew_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_backup_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_backup_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_block_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_bluetooth_audio_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_book_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_book_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_end_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_call_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_camera_alt_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_cancel_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_cancel_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_chat_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_clear_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_cloud_download_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_content_copy_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_crop_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_delete_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_delete_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_description_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_description_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_directions_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_directions_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_done_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_done_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_drafts_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_edit_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_edit_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_error_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_event_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_event_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_flip_camera_android_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_forward_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_group_add_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_group_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_headset_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_headset_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_headset_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_help_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_help_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_help_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_image_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_image_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_input_white_24dp.png delete mode 100755 src/main/res/drawable-xhdpi/ic_link_off_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_link_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_lock_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_lock_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_lock_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mic_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mic_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mic_off_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mic_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mode_edit_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_mode_edit_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_new_releases_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_new_releases_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_no_results_background_black.png delete mode 100644 src/main/res/drawable-xhdpi/ic_no_results_background_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_none_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_none_white80.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_none_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_off_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_off_white80.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_off_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_paused_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_paused_white80.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_paused_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_white80.png delete mode 100644 src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_pause_black_36dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_pause_white_36dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_person_add_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_person_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_person_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_play_arrow_black_36dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_play_arrow_white_36dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_play_circle_filled_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_profile.png delete mode 100644 src/main/res/drawable-xhdpi/ic_public_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_qr_code_scan_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_question_answer_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_refresh_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_replay_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_reply_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_room_black_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_room_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_room_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_save_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_save_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_search_background_black.png delete mode 100644 src/main/res/drawable-xhdpi/ic_search_background_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_search_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_cancel_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_file_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_file_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_location_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_photo_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_picture_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_text_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_videocam_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_videocam_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_videocam_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_videocam_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_videocam_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_away.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_dnd.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_offline.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_offline_dark.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_offline_white.png delete mode 100644 src/main/res/drawable-xhdpi/ic_send_voice_online.png delete mode 100644 src/main/res/drawable-xhdpi/ic_settings_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_settings_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_share_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_star_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_star_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_stat_alert_warning.png delete mode 100644 src/main/res/drawable-xhdpi/ic_stat_communication_import_export.png delete mode 100644 src/main/res/drawable-xhdpi/ic_verified_fingerprint.png delete mode 100644 src/main/res/drawable-xhdpi/ic_verified_user_black_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_verified_user_white_18dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_videocam_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_videocam_off_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_videocam_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_voicemail_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_warning_white_24dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_warning_white_48dp.png delete mode 100644 src/main/res/drawable-xhdpi/ic_wear_reply.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_received.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_received_white.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_sent.9.png delete mode 100644 src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png delete mode 100644 src/main/res/drawable-xxhdpi/baseline_tour_black_48.png delete mode 100644 src/main/res/drawable-xxhdpi/baseline_tour_white_48.png delete mode 100644 src/main/res/drawable-xxhdpi/date_bubble_grey.9.png delete mode 100644 src/main/res/drawable-xxhdpi/date_bubble_white.9.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_account_box_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_action_reply.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_android_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_android_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_announcement_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_archive_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_archive_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_archive_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_camera.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_camera_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_document.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_document_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_file_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_location.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_location_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_photo.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_photo_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_record.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_record_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_videocam.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_attach_videocam_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_autorenew_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_backup_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_backup_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_block_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_bluetooth_audio_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_book_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_book_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_end_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_call_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_camera_alt_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_cancel_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_cancel_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_chat_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_clear_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_cloud_download_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_content_copy_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_crop_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_description_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_description_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_directions_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_directions_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_done_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_done_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_drafts_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_edit_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_error_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_event_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_event_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_flip_camera_android_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_forward_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_group_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_group_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_headset_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_headset_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_help_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_help_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_help_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_hourglass_empty_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_image_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_image_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_input_white_24dp.png delete mode 100755 src/main/res/drawable-xxhdpi/ic_link_off_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_link_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_lock_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_lock_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_lock_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mic_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mic_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mic_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mic_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mode_edit_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_mode_edit_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_new_releases_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_new_releases_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_no_results_background_black.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_no_results_background_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_none_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_none_white80.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_none_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_off_white80.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_off_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_paused_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_paused_white80.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_paused_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_white80.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_notifications_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_pause_black_36dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_pause_white_36dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_person_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_person_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_person_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_play_arrow_black_36dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_play_arrow_white_36dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_play_circle_filled_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_profile.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_public_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_qr_code_scan_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_question_answer_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_refresh_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_replay_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_reply_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_room_black_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_room_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_room_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_save_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_save_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_search_background_black.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_search_background_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_search_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_cancel_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_cancel_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_cancel_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_cancel_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_cancel_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_file_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_file_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_location_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_location_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_location_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_location_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_location_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_photo_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_photo_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_photo_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_photo_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_photo_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_picture_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_picture_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_picture_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_picture_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_picture_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_text_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_text_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_text_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_text_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_text_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_videocam_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_videocam_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_videocam_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_videocam_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_videocam_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_voice_away.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_voice_dnd.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_voice_offline.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_voice_offline_white.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_send_voice_online.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_share_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_star_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_star_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_stat_alert_warning.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_stat_communication_import_export.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_verified_fingerprint.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_verified_user_black_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_verified_user_white_18dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_voicemail_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_warning_white_24dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_warning_white_48dp.png delete mode 100644 src/main/res/drawable-xxhdpi/ic_wear_reply.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_received.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_sent.9.png delete mode 100644 src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/baseline_tour_black_48.png delete mode 100644 src/main/res/drawable-xxxhdpi/baseline_tour_white_48.png delete mode 100644 src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/date_bubble_white.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_account_box_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_android_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_android_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_announcement_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_archive_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_archive_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_archive_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_camera.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_camera_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_document.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_document_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_file_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_location.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_location_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_photo.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_photo_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_record.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_record_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_videocam.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_attach_videocam_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_autorenew_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_backup_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_backup_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_block_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_bluetooth_audio_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_book_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_book_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_end_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_made_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_call_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_camera_alt_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_cancel_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_cancel_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_chat_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_clear_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_cloud_download_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_content_copy_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_crop_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_description_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_description_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_directions_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_directions_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_done_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_done_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_drafts_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_edit_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_edit_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_error_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_event_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_event_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_flip_camera_android_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_forward_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_group_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_group_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_headset_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_headset_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_help_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_help_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_help_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_hourglass_empty_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_image_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_image_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_input_white_24dp.png delete mode 100755 src/main/res/drawable-xxxhdpi/ic_link_off_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_link_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_lock_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_lock_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_lock_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mode_edit_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_mode_edit_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_new_releases_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_new_releases_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_no_results_background_black.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_no_results_background_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_none_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_none_white80.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_none_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_off_white80.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_off_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_paused_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_paused_white80.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_paused_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_white80.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_notifications_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_pause_black_36dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_pause_white_36dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_person_add_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_person_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_person_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_play_arrow_black_36dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_play_arrow_white_36dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_play_circle_filled_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_public_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_qr_code_scan_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_question_answer_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_refresh_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_refresh_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_replay_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_reply_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_room_black_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_room_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_room_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_search_background_black.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_search_background_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_cancel_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_cancel_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_cancel_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_cancel_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_cancel_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_location_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_location_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_location_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_location_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_location_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_photo_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_photo_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_photo_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_photo_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_photo_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_picture_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_picture_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_picture_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_picture_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_picture_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_text_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_text_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_text_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_text_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_text_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_videocam_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_videocam_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_videocam_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_videocam_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_videocam_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_voice_away.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_voice_dnd.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_voice_offline.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_voice_offline_white.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_send_voice_online.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_star_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_star_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_verified_fingerprint.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_verified_user_black_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_verified_user_white_18dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_voicemail_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_volume_up_black_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_warning_white_24dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/ic_warning_white_48dp.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_received.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png delete mode 100644 src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png rename src/main/res/drawable/{account_image_border.xml => background_account_profile_picture.xml} (66%) create mode 100644 src/main/res/drawable/background_list_item.xml create mode 100644 src/main/res/drawable/background_message_bubble.xml rename src/main/res/drawable/{grey.xml => background_message_separator.xml} (51%) create mode 100644 src/main/res/drawable/background_no_results.xml create mode 100644 src/main/res/drawable/background_search.xml create mode 100644 src/main/res/drawable/background_selectable_list_item.xml create mode 100644 src/main/res/drawable/background_selected_item_conversation.xml rename src/main/res/drawable/{background.xml => background_splash_screen.xml} (79%) create mode 100644 src/main/res/drawable/background_surface_container.xml delete mode 100644 src/main/res/drawable/es_slidingpane_shadow.xml delete mode 100644 src/main/res/drawable/greybackground.xml create mode 100644 src/main/res/drawable/ic_adb_48dp.xml rename src/main/res/drawable/{ic_scroll_to_end_black.xml => ic_add_24dp.xml} (51%) create mode 100644 src/main/res/drawable/ic_announcement_24dp.xml create mode 100644 src/main/res/drawable/ic_archive_24dp.xml create mode 100644 src/main/res/drawable/ic_archive_48dp.xml create mode 100644 src/main/res/drawable/ic_attach_file_24dp.xml create mode 100644 src/main/res/drawable/ic_autorenew_24dp.xml create mode 100644 src/main/res/drawable/ic_backup_48dp.xml create mode 100644 src/main/res/drawable/ic_bluetooth_audio_24dp.xml create mode 100644 src/main/res/drawable/ic_book_48dp.xml create mode 100644 src/main/res/drawable/ic_call_24dp.xml delete mode 100644 src/main/res/drawable/ic_call_black54_24dp.xml create mode 100644 src/main/res/drawable/ic_call_end_24dp.xml create mode 100644 src/main/res/drawable/ic_call_made_24dp.xml create mode 100644 src/main/res/drawable/ic_call_missed_24db.xml create mode 100644 src/main/res/drawable/ic_call_missed_24dp.xml create mode 100644 src/main/res/drawable/ic_call_missed_outgoing_24dp.xml delete mode 100644 src/main/res/drawable/ic_call_missed_white_24db.xml create mode 100644 src/main/res/drawable/ic_call_received_24dp.xml delete mode 100644 src/main/res/drawable/ic_call_white70_24dp.xml create mode 100644 src/main/res/drawable/ic_camera_alt_24dp.xml create mode 100644 src/main/res/drawable/ic_cancel_24dp.xml create mode 100644 src/main/res/drawable/ic_cancel_96dp.xml create mode 100644 src/main/res/drawable/ic_chat_24dp.xml create mode 100644 src/main/res/drawable/ic_check_24dp.xml create mode 100644 src/main/res/drawable/ic_clear_24dp.xml create mode 100644 src/main/res/drawable/ic_cloud_download_24dp.xml create mode 100644 src/main/res/drawable/ic_code_48dp.xml create mode 100644 src/main/res/drawable/ic_content_copy_24dp.xml create mode 100644 src/main/res/drawable/ic_delete_24dp.xml create mode 100644 src/main/res/drawable/ic_description_24dp.xml create mode 100644 src/main/res/drawable/ic_description_48dp.xml create mode 100644 src/main/res/drawable/ic_directions_24dp.xml delete mode 100644 src/main/res/drawable/ic_directions_black_24dp.xml create mode 100644 src/main/res/drawable/ic_done_24dp.xml create mode 100644 src/main/res/drawable/ic_download_24dp.xml create mode 100644 src/main/res/drawable/ic_edit_24dp.xml create mode 100644 src/main/res/drawable/ic_email_48dp.xml create mode 100644 src/main/res/drawable/ic_error_24dp.xml create mode 100644 src/main/res/drawable/ic_event_48dp.xml create mode 100644 src/main/res/drawable/ic_flip_camera_android_24dp.xml create mode 100644 src/main/res/drawable/ic_forum_24dp.xml create mode 100644 src/main/res/drawable/ic_gps_fixed_24dp.xml delete mode 100644 src/main/res/drawable/ic_gps_fixed_black_24dp.xml create mode 100644 src/main/res/drawable/ic_gps_not_fixed_24dp.xml delete mode 100644 src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml create mode 100644 src/main/res/drawable/ic_group_24dp.xml create mode 100644 src/main/res/drawable/ic_headphones_48dp.xml create mode 100644 src/main/res/drawable/ic_headset_mic_24dp.xml create mode 100644 src/main/res/drawable/ic_help_24dp.xml create mode 100644 src/main/res/drawable/ic_help_center_48dp.xml create mode 100644 src/main/res/drawable/ic_hourglass_top_24dp.xml create mode 100644 src/main/res/drawable/ic_image_24dp.xml create mode 100644 src/main/res/drawable/ic_image_48dp.xml create mode 100644 src/main/res/drawable/ic_keyboard_double_arrow_down_24dp.xml create mode 100644 src/main/res/drawable/ic_link_24dp.xml create mode 100644 src/main/res/drawable/ic_link_off_24dp.xml create mode 100644 src/main/res/drawable/ic_location_pin_24dp.xml create mode 100644 src/main/res/drawable/ic_location_pin_48dp.xml create mode 100644 src/main/res/drawable/ic_lock_24dp.xml create mode 100644 src/main/res/drawable/ic_lock_open_outline_24dp.xml create mode 100644 src/main/res/drawable/ic_login_24dp.xml create mode 100644 src/main/res/drawable/ic_logout_24dp.xml delete mode 100644 src/main/res/drawable/ic_logout_white_24dp.xml create mode 100644 src/main/res/drawable/ic_mark_chat_read_24dp.xml create mode 100644 src/main/res/drawable/ic_mic_24dp.xml create mode 100644 src/main/res/drawable/ic_mic_48dp.xml create mode 100644 src/main/res/drawable/ic_mic_off_24dp.xml create mode 100644 src/main/res/drawable/ic_movie_48dp.xml create mode 100644 src/main/res/drawable/ic_navigate_next_24dp.xml rename src/main/res/drawable/{ic_baseline_check_24.xml => ic_new_releases_24dp.xml} (52%) create mode 100644 src/main/res/drawable/ic_notifications_24dp.xml create mode 100644 src/main/res/drawable/ic_notifications_none_24dp.xml create mode 100644 src/main/res/drawable/ic_notifications_off_24dp.xml create mode 100644 src/main/res/drawable/ic_notifications_paused_24dp.xml create mode 100644 src/main/res/drawable/ic_pause_24dp.xml create mode 100644 src/main/res/drawable/ic_person_24dp.xml create mode 100644 src/main/res/drawable/ic_person_48dp.xml create mode 100644 src/main/res/drawable/ic_person_add_24dp.xml create mode 100644 src/main/res/drawable/ic_phone_in_talk_24dp.xml delete mode 100644 src/main/res/drawable/ic_place_black_24dp.xml create mode 100644 src/main/res/drawable/ic_play_arrow_24dp.xml create mode 100644 src/main/res/drawable/ic_play_circle_24dp.xml create mode 100644 src/main/res/drawable/ic_play_lesson_48dp.xml delete mode 100644 src/main/res/drawable/ic_play_lesson_black_24.xml delete mode 100644 src/main/res/drawable/ic_play_lesson_white_48dp.xml create mode 100644 src/main/res/drawable/ic_public_24dp.xml create mode 100644 src/main/res/drawable/ic_qr_code_24dp.xml delete mode 100644 src/main/res/drawable/ic_qr_code_black_24dp.xml create mode 100644 src/main/res/drawable/ic_qr_code_scanner_24dp.xml delete mode 100644 src/main/res/drawable/ic_qr_code_white_24dp.xml create mode 100644 src/main/res/drawable/ic_refresh_24dp.xml create mode 100644 src/main/res/drawable/ic_replay_24dp.xml create mode 100644 src/main/res/drawable/ic_reply_24dp.xml create mode 100644 src/main/res/drawable/ic_save_24dp.xml delete mode 100644 src/main/res/drawable/ic_scroll_to_end_white.xml create mode 100644 src/main/res/drawable/ic_search_128dp.xml create mode 100644 src/main/res/drawable/ic_search_24dp.xml create mode 100644 src/main/res/drawable/ic_send_24dp.xml create mode 100644 src/main/res/drawable/ic_settings_24dp.xml create mode 100644 src/main/res/drawable/ic_share_24dp.xml create mode 100644 src/main/res/drawable/ic_star_24dp.xml create mode 100644 src/main/res/drawable/ic_tour_48dp.xml create mode 100644 src/main/res/drawable/ic_travel_explore_24dp.xml create mode 100644 src/main/res/drawable/ic_unarchive_24dp.xml create mode 100644 src/main/res/drawable/ic_verified_user_24dp.xml create mode 100644 src/main/res/drawable/ic_videocam_24dp.xml delete mode 100644 src/main/res/drawable/ic_videocam_black54_24dp.xml create mode 100644 src/main/res/drawable/ic_videocam_off_24dp.xml delete mode 100644 src/main/res/drawable/ic_videocam_white70_24dp.xml create mode 100644 src/main/res/drawable/ic_voicemail_24dp.xml create mode 100644 src/main/res/drawable/ic_volume_off_24dp.xml create mode 100644 src/main/res/drawable/ic_volume_up_24dp.xml create mode 100644 src/main/res/drawable/ic_warning_24dp.xml delete mode 100644 src/main/res/drawable/list_item_background_dark.xml delete mode 100644 src/main/res/drawable/list_item_background_light.xml delete mode 100644 src/main/res/drawable/no_results_background_dark.xml delete mode 100644 src/main/res/drawable/no_results_background_light.xml delete mode 100644 src/main/res/drawable/no_results_primary_background_dark.xml delete mode 100644 src/main/res/drawable/no_results_primary_background_light.xml delete mode 100644 src/main/res/drawable/search_background_dark.xml delete mode 100644 src/main/res/drawable/search_background_light.xml delete mode 100644 src/main/res/drawable/visibility_toggle_drawable.xml delete mode 100644 src/main/res/drawable/white_cursor.xml delete mode 100644 src/main/res/layout/create_conference_dialog.xml create mode 100644 src/main/res/layout/dialog_create_conference.xml rename src/main/res/layout/{create_public_channel_dialog.xml => dialog_create_public_channel.xml} (60%) create mode 100644 src/main/res/layout/dialog_enter_jid.xml delete mode 100644 src/main/res/layout/enter_jid_dialog.xml delete mode 100644 src/main/res/layout/form_boolean.xml delete mode 100644 src/main/res/layout/form_text.xml rename src/main/res/layout/{account_row.xml => item_account.xml} (76%) create mode 100644 src/main/res/layout/item_autocomplete.xml rename src/main/res/layout/{search_result_item.xml => item_channel_discovery.xml} (69%) rename src/main/res/layout/{contact.xml => item_contact.xml} (61%) rename src/main/res/layout/{conversation_list_row.xml => item_conversation.xml} (59%) rename src/main/res/layout/{media.xml => item_media.xml} (69%) rename src/main/res/layout/{media_preview.xml => item_media_preview.xml} (74%) create mode 100644 src/main/res/layout/item_message_content.xml create mode 100644 src/main/res/layout/item_message_date_bubble.xml create mode 100644 src/main/res/layout/item_message_received.xml create mode 100644 src/main/res/layout/item_message_rtp_session.xml create mode 100644 src/main/res/layout/item_message_sent.xml create mode 100644 src/main/res/layout/item_message_status.xml rename src/main/res/layout/{user_preview.xml => item_user_preview.xml} (71%) delete mode 100644 src/main/res/layout/message_content.xml delete mode 100644 src/main/res/layout/message_date_bubble.xml delete mode 100644 src/main/res/layout/message_received.xml delete mode 100644 src/main/res/layout/message_rtp_session.xml delete mode 100644 src/main/res/layout/message_sent.xml delete mode 100644 src/main/res/layout/message_status.xml delete mode 100644 src/main/res/layout/presence_template.xml delete mode 100644 src/main/res/layout/simple_list_item.xml delete mode 100644 src/main/res/layout/toolbar.xml delete mode 100644 src/main/res/values-night/colors.xml create mode 100644 src/main/res/values-night/themes.xml create mode 100644 src/main/res/values/colors-md.xml delete mode 100644 src/main/res/values/colors.xml delete mode 100644 src/main/res/values/styles.xml rename src/quicksy/res/menu/{choose_country.xml => item_choose_country.xml} (88%) create mode 100644 src/quicksy/res/values/colors-themed.xml diff --git a/build.gradle b/build.gradle index 908b80c17..2540a7836 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'me.drakeet.support:toastcompat:1.1.0' - implementation "com.leinardi.android:speed-dial:3.2.0" + implementation "com.leinardi.android:speed-dial:3.3.0" implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index 87c925fe6..5b111101e 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -5,6 +5,7 @@ share()); if (bundle != null && bundle.containsKey("invite")) { this.easyOnboardingInvite = bundle.getParcelable("invite"); @@ -65,11 +69,11 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn } private void share() { - final String shareText = getString( - R.string.easy_invite_share_text, - easyOnboardingInvite.getDomain(), - easyOnboardingInvite.getShareableLink() - ); + final String shareText = + getString( + R.string.easy_invite_share_text, + easyOnboardingInvite.getDomain(), + easyOnboardingInvite.getShareableLink()); final Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); @@ -95,16 +99,47 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn private void showInvite(final EasyOnboardingInvite invite) { this.binding.inProgress.setVisibility(View.GONE); this.binding.invite.setVisibility(View.VISIBLE); - this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain())); + this.binding.tapToShare.setText( + getString(R.string.tap_share_button_send_invite, invite.getDomain())); final Point size = new Point(); getWindowManager().getDefaultDisplay().getSize(size); final int width = Math.min(size.x, size.y); - final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableLink(), width); + final boolean nightMode = + (this.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + final int black; + final int white; + if (nightMode) { + black = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurface, + "No surface color configured"); + white = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceInverse, + "No inverse surface color configured"); + } else { + black = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceInverse, + "No inverse surface color configured"); + white = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurface, + "No surface color configured"); + } + final Bitmap bitmap = + BarcodeProvider.create2dBarcodeBitmap( + invite.getShareableLink(), width, black, white); binding.qrCode.setImageBitmap(bitmap); } @Override - public void onSaveInstanceState(Bundle bundle) { + public void onSaveInstanceState(@NonNull Bundle bundle) { super.onSaveInstanceState(bundle); if (easyOnboardingInvite != null) { bundle.putParcelable("invite", easyOnboardingInvite); @@ -141,11 +176,12 @@ public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOn @Override public void inviteRequestFailed(final String message) { - runOnUiThread(() -> { - if (!Strings.isNullOrEmpty(message)) { - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - } - finish(); - }); + runOnUiThread( + () -> { + if (!Strings.isNullOrEmpty(message)) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show(); + } + finish(); + }); } } diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java index ed998677b..1f1040c09 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -18,6 +18,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import java.io.IOException; @@ -31,7 +32,6 @@ import eu.siacs.conversations.services.ImportBackupService; import eu.siacs.conversations.ui.adapter.BackupFileAdapter; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.ThemeHelper; public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { @@ -46,22 +46,15 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo @Override protected void onCreate(final Bundle savedInstanceState) { - this.mTheme = ThemeHelper.find(this); - setTheme(this.mTheme); super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(binding.toolbar); setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false)); this.backupFileAdapter = new BackupFileAdapter(); this.binding.list.setAdapter(this.backupFileAdapter); this.backupFileAdapter.setOnItemClickedListener(this); } - - @Override - protected void onResume(){ - super.onResume(); - SettingsUtils.applyScreenshotPreventionSetting(this); - } @Override public boolean onCreateOptionsMenu(final Menu menu) { @@ -80,12 +73,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo @Override public void onStart() { super.onStart(); - final int theme = ThemeHelper.find(this); - if (this.mTheme != theme) { - recreate(); - } else { - bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); - } + bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); final Intent intent = getIntent(); if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) { Uri uri = intent.getData(); @@ -146,7 +134,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri()); enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString())); - AlertDialog.Builder builder = new AlertDialog.Builder(this); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setView(enterPasswordBinding.getRoot()); builder.setTitle(R.string.enter_password); builder.setNegativeButton(R.string.cancel, (dialog, which) -> { @@ -186,6 +174,7 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); configureActionBar(getSupportActionBar(), !loadingState); this.mLoadingState = loadingState; invalidateOptionsMenu(); diff --git a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java index 38761befd..5a301f769 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/MagicCreateActivity.java @@ -10,45 +10,32 @@ import android.widget.Toast; import androidx.databinding.DataBindingUtil; -import java.security.SecureRandom; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.MagicCreateBinding; +import eu.siacs.conversations.databinding.ActivityMagicCreateBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.InstallReferrerUtils; import eu.siacs.conversations.xmpp.Jid; +import java.security.SecureRandom; + public class MagicCreateActivity extends XmppActivity implements TextWatcher { public static final String EXTRA_DOMAIN = "domain"; public static final String EXTRA_PRE_AUTH = "pre_auth"; public static final String EXTRA_USERNAME = "username"; - private MagicCreateBinding binding; + private ActivityMagicCreateBinding binding; private String domain; private String username; private String preAuth; @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override - void onBackendConnected() { - - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } + void onBackendConnected() {} @Override protected void onCreate(final Bundle savedInstanceState) { @@ -60,7 +47,8 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.magic_create); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(this.binding.toolbar); configureActionBar(getSupportActionBar(), this.domain == null); if (username != null && domain != null) { @@ -72,51 +60,64 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher { } else if (domain != null) { binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain)); } - binding.createAccount.setOnClickListener(v -> { - try { - final String username = binding.username.getText().toString(); - final Jid jid; - final boolean fixedUsername; - if (this.domain != null && this.username != null) { - fixedUsername = true; - jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain); - } else if (this.domain != null) { - fixedUsername = false; - jid = Jid.ofLocalAndDomainEscaped(username, this.domain); - } else { - fixedUsername = false; - jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); - } - if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) { - binding.username.setError(getString(R.string.invalid_username)); - binding.username.requestFocus(); - } else { - binding.username.setError(null); - Account account = xmppConnectionService.findAccountByJid(jid); - if (account == null) { - account = new Account(jid, CryptoHelper.createPassword(new SecureRandom())); - account.setOption(Account.OPTION_REGISTER, true); - account.setOption(Account.OPTION_DISABLED, true); - account.setOption(Account.OPTION_MAGIC_CREATE, true); - account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); - if (this.preAuth != null) { - account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + binding.createAccount.setOnClickListener( + v -> { + try { + final String username = binding.username.getText().toString(); + final Jid jid; + final boolean fixedUsername; + if (this.domain != null && this.username != null) { + fixedUsername = true; + jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain); + } else if (this.domain != null) { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, this.domain); + } else { + fixedUsername = false; + jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); } - xmppConnectionService.createAccount(account); + if (!jid.getEscapedLocal().equals(jid.getLocal()) + || (this.username == null && username.length() < 3)) { + binding.usernameLayout.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); + } else { + binding.usernameLayout.setError(null); + Account account = xmppConnectionService.findAccountByJid(jid); + if (account == null) { + account = + new Account( + jid, + CryptoHelper.createPassword(new SecureRandom())); + account.setOption(Account.OPTION_REGISTER, true); + account.setOption(Account.OPTION_DISABLED, true); + account.setOption(Account.OPTION_MAGIC_CREATE, true); + account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); + if (this.preAuth != null) { + account.setKey( + Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); + } + xmppConnectionService.createAccount(account); + } + Intent intent = + new Intent(MagicCreateActivity.this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("init", true); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Toast.makeText( + MagicCreateActivity.this, + R.string.secure_password_generated, + Toast.LENGTH_SHORT) + .show(); + StartConversationActivity.addInviteUri(intent, getIntent()); + startActivity(intent); + } + } catch (final IllegalArgumentException e) { + binding.usernameLayout.setError(getString(R.string.invalid_username)); + binding.username.requestFocus(); } - Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toString()); - intent.putExtra("init", true); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show(); - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - } - } catch (IllegalArgumentException e) { - binding.username.setError(getString(R.string.invalid_username)); - binding.username.requestFocus(); - } - }); + }); binding.username.addTextChangedListener(this); } @@ -127,14 +128,10 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher { } @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } + public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(final Editable s) { @@ -153,8 +150,10 @@ public class MagicCreateActivity extends XmppActivity implements TextWatcher { } else { jid = Jid.ofLocalAndDomainEscaped(username, this.domain); } - binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString())); - } catch (IllegalArgumentException e) { + binding.fullJid.setText( + getString(R.string.your_full_jid_will_be, jid.toEscapedString())); + binding.usernameLayout.setError(null); + } catch (final IllegalArgumentException e) { binding.fullJid.setVisibility(View.INVISIBLE); } } diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 4446acefe..1a04210dd 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -1,10 +1,14 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; + import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Bundle; import android.security.KeyChain; import android.security.KeyChainAliasCallback; +import android.util.Log; import android.util.Pair; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -12,23 +16,17 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.ListView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; +import androidx.databinding.DataBindingUtil; -import org.openintents.openpgp.util.OpenPgpApi; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import com.google.common.base.Strings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityManageAccountsBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -37,10 +35,17 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; +import org.openintents.openpgp.util.OpenPgpApi; -public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ManageAccountActivity extends XmppActivity + implements OnAccountUpdate, + KeyChainAliasCallback, + XmppConnectionService.OnAccountCreated, + AccountAdapter.OnTglAccountState { private final String STATE_SELECTED_ACCOUNT = "selected_account"; @@ -50,7 +55,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda protected Jid selectedAccountJid = null; protected final List accountList = new ArrayList<>(); - protected ListView accountListView; protected AccountAdapter mAccountAdapter; protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false); @@ -67,7 +71,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda accountList.clear(); accountList.addAll(xmppConnectionService.getAccounts()); } - ActionBar actionBar = getSupportActionBar(); + final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setHomeButtonEnabled(this.accountList.size() > 0); actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0); @@ -81,8 +85,11 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda super.onCreate(savedInstanceState); - setContentView(R.layout.activity_manage_accounts); - setSupportActionBar(findViewById(R.id.toolbar)); + ActivityManageAccountsBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_manage_accounts); + + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); + setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar()); if (savedInstanceState != null) { String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT); @@ -95,26 +102,19 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } } - accountListView = findViewById(R.id.account_list); this.mAccountAdapter = new AccountAdapter(this, accountList); - accountListView.setAdapter(this.mAccountAdapter); - accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); - registerForContextMenu(accountListView); + binding.accountList.setAdapter(this.mAccountAdapter); + binding.accountList.setOnItemClickListener( + (arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); + registerForContextMenu(binding.accountList); } - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (selectedAccount != null) { - savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); + savedInstanceState.putString( + STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); } super.onSaveInstanceState(savedInstanceState); } @@ -122,8 +122,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - ManageAccountActivity.this.getMenuInflater().inflate( - R.menu.manageaccounts_context, menu); + ManageAccountActivity.this.getMenuInflater().inflate(R.menu.manageaccounts_context, menu); AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; this.selectedAccount = accountList.get(acmi.position); if (this.selectedAccount.isEnabled()) { @@ -144,9 +143,10 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } refreshUiReal(); if (this.mPostponedActivityResult != null) { - this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); + this.onActivityResult( + mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); } - if (Config.X509_VERIFICATION && this.accountList.size() == 0) { + if (Config.X509_VERIFICATION && this.accountList.isEmpty()) { if (mInvokedAddAccount.compareAndSet(false, true)) { addAccountFromKey(); } @@ -233,9 +233,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda return super.onOptionsItemSelected(item); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) { if (allGranted(grantResults)) { @@ -258,13 +258,14 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public boolean onNavigateUp() { if (xmppConnectionService.getConversations().size() == 0) { - Intent contactsIntent = new Intent(this, - StartConversationActivity.class); + Intent contactsIntent = new Intent(this, StartConversationActivity.class); contactsIntent.setFlags( // if activity exists in stack, pop the stack and go back to it - Intent.FLAG_ACTIVITY_CLEAR_TOP | + Intent.FLAG_ACTIVITY_CLEAR_TOP + | // otherwise, make a new task for it - Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_NEW_TASK + | // don't use the new activity animation; finish // animation runs instead Intent.FLAG_ACTIVITY_NO_ANIMATION); @@ -286,16 +287,17 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } private void addAccountFromKey() { + Log.d(Config.LOGTAG, "add account from key"); try { KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } catch (final ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG) + .show(); } } private void publishAvatar(Account account) { - Intent intent = new Intent(getApplicationContext(), - PublishProfilePictureActivity.class); + Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); startActivity(intent); } @@ -377,7 +379,6 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda } } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -385,7 +386,8 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda if (xmppConnectionServiceBound) { if (requestCode == REQUEST_CHOOSE_PGP_ID) { if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { - selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID)); + selectedAccount.setPgpSignId( + data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID)); announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); } else { choosePgpSignId(selectedAccount); @@ -402,9 +404,17 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void alias(final String alias) { - if (alias != null) { - xmppConnectionService.createAccountFromKey(alias, this); + if (Strings.isNullOrEmpty(alias)) { + runOnUiThread( + () -> + Toast.makeText( + this, + R.string.no_certificate_selected, + Toast.LENGTH_LONG) + .show()); + return; } + xmppConnectionService.createAccountFromKey(alias, this); } @Override @@ -417,6 +427,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void informUser(final int r) { - runOnUiThread(() -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show()); + runOnUiThread( + () -> Toast.makeText(ManageAccountActivity.this, r, Toast.LENGTH_LONG).show()); } } diff --git a/src/conversations/java/eu/siacs/conversations/ui/PickServerActivity.java b/src/conversations/java/eu/siacs/conversations/ui/PickServerActivity.java index 06320d33d..cbdd21b36 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/PickServerActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/PickServerActivity.java @@ -26,15 +26,6 @@ public class PickServerActivity extends XmppActivity { } - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { @@ -53,7 +44,8 @@ public class PickServerActivity extends XmppActivity { } @Override - public void onNewIntent(Intent intent) { + public void onNewIntent(final Intent intent) { + super.onNewIntent(intent); if (intent != null) { setIntent(intent); } @@ -66,6 +58,7 @@ public class PickServerActivity extends XmppActivity { } super.onCreate(savedInstanceState); ActivityPickServerBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_pick_server); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar()); binding.useCim.setOnClickListener(v -> { @@ -81,7 +74,7 @@ public class PickServerActivity extends XmppActivity { if (accounts.size() == 1) { intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); intent.putExtra("init", true); - } else if (accounts.size() >= 1) { + } else if (!accounts.isEmpty()) { intent = new Intent(this, ManageAccountActivity.class); } addInviteUri(intent); diff --git a/src/conversations/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java index 762dfbb42..66ed355c4 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java @@ -56,15 +56,6 @@ public class ShareViaAccountActivity extends XmppActivity { }); } - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - @Override void onBackendConnected() { final int numAccounts = xmppConnectionService.getAccounts().size(); diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java index d61c64a9c..24528fc16 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -34,7 +34,10 @@ import eu.siacs.conversations.xmpp.Jid; import static eu.siacs.conversations.utils.PermissionUtils.allGranted; import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; -public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { +import com.google.common.base.Strings; + +public class WelcomeActivity extends XmppActivity + implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { private static final int REQUEST_IMPORT_BACKUP = 0x63fb; @@ -66,7 +69,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi final Intent intent; if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); - } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { + } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) + && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); } else { @@ -81,22 +85,14 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override - void onBackendConnected() { - - } + void onBackendConnected() {} @Override public void onStart() { super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } new InstallReferrerUtils(this); } @@ -119,42 +115,44 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } super.onCreate(savedInstanceState); - ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome); + ActivityWelcomeBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_welcome); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar(), false); - binding.registerNewAccount.setOnClickListener(v -> { - final Intent intent = new Intent(this, PickServerActivity.class); - addInviteUri(intent); - startActivity(intent); - }); - binding.useExisting.setOnClickListener(v -> { - final List accounts = xmppConnectionService.getAccounts(); - Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class); - intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false); - if (accounts.size() == 1) { - intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); - intent.putExtra("init", true); - } else if (accounts.size() >= 1) { - intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class); - } - addInviteUri(intent); - startActivity(intent); - }); - + setTitle(null); + binding.registerNewAccount.setOnClickListener( + v -> { + final Intent intent = new Intent(this, PickServerActivity.class); + addInviteUri(intent); + startActivity(intent); + }); + binding.useExisting.setOnClickListener( + v -> { + final List accounts = xmppConnectionService.getAccounts(); + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false); + if (accounts.size() == 1) { + intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); + intent.putExtra("init", true); + } else if (!accounts.isEmpty()) { + intent = new Intent(this, ManageAccountActivity.class); + } + addInviteUri(intent); + startActivity(intent); + }); } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.welcome_menu, menu); final MenuItem scan = menu.findItem(R.id.action_scan_qr_code); scan.setVisible(Compatibility.hasFeatureCamera(this)); return super.onCreateOptionsMenu(menu); } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_import_backup: if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { @@ -174,16 +172,25 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi private void addAccountFromKey() { try { KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show(); + } catch (final ActivityNotFoundException e) { + Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG) + .show(); } } @Override public void alias(final String alias) { - if (alias != null) { - xmppConnectionService.createAccountFromKey(alias, this); + if (Strings.isNullOrEmpty(alias)) { + runOnUiThread( + () -> + Toast.makeText( + this, + R.string.no_certificate_selected, + Toast.LENGTH_LONG) + .show()); + return; } + xmppConnectionService.createAccountFromKey(alias, this); } @Override @@ -201,7 +208,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { @@ -211,7 +219,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi startActivity(new Intent(this, ImportBackupActivity.class)); break; } - } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + } else if (Arrays.asList(permissions) + .contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); } } @@ -232,5 +241,4 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString()); } } - } diff --git a/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java index 9857dcd8a..9f32352ee 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java +++ b/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.AccountRowBinding; +import eu.siacs.conversations.databinding.ItemAccountBinding; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.ImportBackupService; import eu.siacs.conversations.utils.BackupFileHeader; @@ -39,7 +39,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter(imageView); DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics(); this.size = ((int) (48 * metrics.density)); @@ -146,8 +146,7 @@ public class BackupFileAdapter extends RecyclerView.AdapterECHuegCV^l*HW)V~E;3QPf!hnW>r91l1D=W_Vb z5W(y4G{MkBcZZ3?)a0y#>li&PcZualU6`^s>)_+eKZ~whaF{%!&NbjKcL*=z|M*AY f|L&Jxcvj00UZF6vf&ID(&>ak(u6{1-oD!M+fBM5T$P+WqDqf*6fiTYQEJx&}*G=J0NWgzCp99W#r?rXGJn%$B z>pbv5M4LPi)c^)CmvK+P2;p9U(bwDqF#0w}14hXl2^jq{M*&8eIRY@6nOgy)9dip{ zR4~^9Mul?~kR&SJ4&V>og8^)aXvzZ*M6}8SC%n+wGm8XBh&2&JQp zm*xkIOkLACniS#=UAm>$D(CP);GQF^cC>4buxhJZg0$ntc##tO*11-DT24+#+IO9Q zzh?2&)W~TY&&!&|UA_h3|*nY@Hy z!WqLL29IYFnhYw%JtC||eNJ5rlJ2TI8%z59KCw$4?_AOhW%Vv;zGLZD#LD^KGR7mI zN$5#PlhBj}`ioYXUN8>qHNBu2*lTjZYQ2le(&dd@(R)}5*hEA_A|AJJnx0;fcBVtl imEBd(5#+G2och=Q?sSz#RHy+1n8DN4&t;ucLK6T>dYn`M diff --git a/src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png b/src/conversations/res/drawable-xxxhdpi/ic_unarchive_white_24dp.png deleted file mode 100644 index a789520baa7f4b65fd73ae78a57ec50b9d645e3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 503 zcmV9JZFUK03OixFK?s74p2SYCAYR}p1?>_^(85!gQmq9eh^_{-&k=SI_HVTg zh?@Ooznyspp8wN-!>=89()iz$2!_W(zjkExyoapP1JK?6aWn z7F@3dm~h4roiV8cct+cOVyBz&mbRxg01s(5V4Wa(uWz#a0m;&wUU1(`Z22Ye?}7v+GT zWU46#Bx{5K0}L?000Rs#zyP5!14IyLtN{IfOJf9xXotoI5b?(}CV+^)Ok)9v_{TH` zfQbK0*#RQ1m@)%IY%pa7h}dw-2oO;)Wdn#PoH7AKbeOULM0A`o07Pst4Gj>n7+Ap}a$4Sn!<>CMU002ovPDHLkV1mEb(=Px3 diff --git a/src/conversations/res/layout/activity_easy_invite.xml b/src/conversations/res/layout/activity_easy_invite.xml index 8bbf11c03..6f1c8fc57 100644 --- a/src/conversations/res/layout/activity_easy_invite.xml +++ b/src/conversations/res/layout/activity_easy_invite.xml @@ -1,17 +1,23 @@ - + - + + + + + + android:textAppearance="?textAppearanceBodyMedium" /> + android:textAppearance="?textAppearanceBodyMedium" />