diff --git a/build.gradle b/build.gradle index 3a0441356..06aa31ae5 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,10 @@ dependencies { implementation "androidx.security:security-crypto:1.0.0" + + implementation 'org.slf4j:slf4j-api:1.7.36' + implementation 'com.github.tony19:logback-android:2.0.1' + testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.9' diff --git a/proguard-rules.pro b/proguard-rules.pro index 7e4d7d31d..b9bab8865 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -13,6 +13,10 @@ -keep class org.openintents.openpgp.* -keep class org.webrtc.** { *; } +# Logger +-keep class org.slf4j.** {*;} +-keep class ch.qos.** {*;} + -dontwarn javax.mail.internet.MimeMessage -dontwarn javax.mail.internet.MimeBodyPart -dontwarn javax.mail.internet.SharedInputStream diff --git a/src/main/assets/logback.xml b/src/main/assets/logback.xml new file mode 100644 index 000000000..0c5ac873f --- /dev/null +++ b/src/main/assets/logback.xml @@ -0,0 +1,16 @@ + + + + conversations + + + %logger{12}: %msg + + + + + + + diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 2791ff827..66566e12e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -79,4 +79,5 @@ public final class Namespace { "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; public static final String JABBER_CLIENT = "jabber:client"; + public static final String FORWARD = "urn:xmpp:forward:0"; } diff --git a/src/main/java/im/conversations/android/database/dao/PresenceDao.java b/src/main/java/im/conversations/android/database/dao/PresenceDao.java index 1ba5a7836..9e406792c 100644 --- a/src/main/java/im/conversations/android/database/dao/PresenceDao.java +++ b/src/main/java/im/conversations/android/database/dao/PresenceDao.java @@ -3,8 +3,9 @@ package im.conversations.android.database.dao; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; -import androidx.room.Upsert; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.database.entity.PresenceEntity; import im.conversations.android.database.model.Account; @@ -26,7 +27,7 @@ public abstract class PresenceDao { + " resource=:resource") abstract void deletePresence(long account, Jid address, String resource); - @Upsert + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract void insert(PresenceEntity entity); public void set( diff --git a/src/main/java/im/conversations/android/database/model/PresenceType.java b/src/main/java/im/conversations/android/database/model/PresenceType.java index 943c96f55..7407f2314 100644 --- a/src/main/java/im/conversations/android/database/model/PresenceType.java +++ b/src/main/java/im/conversations/android/database/model/PresenceType.java @@ -12,6 +12,6 @@ public enum PresenceType { if (typeAttribute == null) { return null; } - return of(typeAttribute.toUpperCase(Locale.ROOT)); + return valueOf(typeAttribute.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/im/conversations/android/xml/TagWriter.java b/src/main/java/im/conversations/android/xml/TagWriter.java index 221805eb7..6e660ed23 100644 --- a/src/main/java/im/conversations/android/xml/TagWriter.java +++ b/src/main/java/im/conversations/android/xml/TagWriter.java @@ -1,7 +1,5 @@ package im.conversations.android.xml; -import android.util.Log; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Tag; import im.conversations.android.xmpp.model.StreamElement; @@ -11,9 +9,13 @@ import java.io.OutputStreamWriter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TagWriter { + private static final Logger LOGGER = LoggerFactory.getLogger(TagWriter.class); + private OutputStreamWriter outputStream; private boolean finished = false; private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue<>(); @@ -83,7 +85,7 @@ public class TagWriter { public void writeStanzaAsync(StreamElement stanza) { if (finished) { - Log.d(Config.LOGTAG, "attempting to write stanza to finished TagWriter"); + LOGGER.info("attempting to write stanza to finished TagWriter"); } else { if (!asyncStanzaWriter.isAlive()) { try { diff --git a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java b/src/main/java/im/conversations/android/xmpp/ConnectionPool.java index cf218e6ab..b7cbbe385 100644 --- a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java +++ b/src/main/java/im/conversations/android/xmpp/ConnectionPool.java @@ -4,7 +4,6 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.os.SystemClock; -import android.util.Log; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -28,9 +27,13 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ConnectionPool { + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class); + private static volatile ConnectionPool INSTANCE; private final Context context; @@ -137,7 +140,7 @@ public class ConnectionPool { if (connection.getStatus() == ConnectionState.ONLINE) { synchronized (lowPingTimeoutMode) { if (lowPingTimeoutMode.remove(account.address)) { - Log.d(Config.LOGTAG, account.address + ": leaving low ping timeout mode"); + LOGGER.debug("{}: leaving low ping timeout mode", account.address); } } ConversationsDatabase.getInstance(context) @@ -154,11 +157,9 @@ public class ConnectionPool { // resetSendingToWaiting(account); if (isInLowPingTimeoutMode(account)) { - Log.d( - Config.LOGTAG, - account.address - + ": went into offline state during low ping mode." - + " reconnecting now"); + LOGGER.debug( + "{}: went into offline state during low ping mode. reconnecting now", + account.address); reconnectAccount(connection); } else { final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; @@ -173,24 +174,20 @@ public class ConnectionPool { final int next = connection.getTimeToNextAttempt(); final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); if (next <= 0) { - Log.d( - Config.LOGTAG, - account.address - + ": error connecting account. reconnecting now." - + " lowPingTimeout=" - + lowPingTimeoutMode); + LOGGER.debug( + "{}: error connecting account. reconnecting now. lowPingTimeout={}", + account.address, + lowPingTimeoutMode); reconnectAccount(connection); } else { final int attempt = connection.getAttempt() + 1; - Log.d( - Config.LOGTAG, - account.address - + ": error connecting account. try again in " - + next - + "s for the " - + attempt - + " time. lowPingTimeout=" - + lowPingTimeoutMode); + LOGGER.debug( + "{}: error connecting account. try again in {}s for the {} time." + + " lowPingTimeout={}", + account.address, + next, + attempt, + lowPingTimeoutMode); scheduleWakeUpCall(next); } } @@ -246,9 +243,7 @@ public class ConnectionPool { final Account account = xmppConnection.getAccount(); final boolean lowTimeout = isInLowPingTimeoutMode(account); xmppConnection.sendPing(); - Log.d( - Config.LOGTAG, - account.address + " send ping (lowTimeout=" + lowTimeout + ")"); + LOGGER.debug("{}: send ping (lowTimeout={})", account.address, lowTimeout); scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT); } } @@ -277,7 +272,7 @@ public class ConnectionPool { (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); if (lastSent > lastReceived) { if (pingTimeoutIn < 0) { - Log.d(Config.LOGTAG, account.address + ": ping timeout"); + LOGGER.debug("{}: ping timeout", account.address); this.reconnectAccount(connection); } else { this.scheduleWakeUpCall(Ints.saturatedCast(pingTimeoutIn / 1000)); @@ -287,18 +282,14 @@ public class ConnectionPool { if (isAccountPushed) { pingNow = true; if (lowPingTimeoutMode.add(account.address)) { - Log.d( - Config.LOGTAG, - account.address + ": entering low ping timeout mode"); + LOGGER.debug("{}: entering low ping timeout mode", account.address); } } else if (msToNextPing <= 0) { pingNow = true; } else { this.scheduleWakeUpCall(Ints.saturatedCast(msToNextPing / 1000)); if (lowPingTimeoutMode.remove(account.address)) { - Log.d( - Config.LOGTAG, - account.address + ": leaving low ping timeout mode"); + LOGGER.debug("{}: leaving low ping timeout mode", account.address); } } } @@ -310,13 +301,10 @@ public class ConnectionPool { (SystemClock.elapsedRealtime() - connection.getLastConnect()) / 1000; long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; if (timeout < 0) { - Log.d( - Config.LOGTAG, - account.address - + ": time out during connect reconnecting" - + " (secondsSinceLast=" - + secondsSinceLastConnect - + ")"); + LOGGER.debug( + "{}: time out during connect reconnecting (secondsSinceLast={})", + account.address, + secondsSinceLastConnect); connection.resetAttemptCount(false); reconnectAccount(connection); } diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index 7c884eb44..cc056ee2b 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -2259,9 +2259,15 @@ public class XmppConnection implements Runnable { public boolean fromAccount(final Stanza stanza) { final Jid from = stanza.getFrom(); + // TODO null is valid too?! return from != null && from.asBareJid().equals(connectionAddress.asBareJid()); } + public boolean toAccount(final Stanza stanza) { + final Jid to = stanza.getTo(); + return to == null || to.asBareJid().equals(connectionAddress.asBareJid()); + } + public boolean supportsClientStateIndication() { return this.streamFeatures != null && this.streamFeatures.clientStateIndication(); } diff --git a/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java b/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java index 4fc354245..60c0a2349 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java @@ -1,18 +1,26 @@ package im.conversations.android.xmpp.manager; import android.content.Context; -import android.util.Log; -import eu.siacs.conversations.Config; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.carbons.Enable; +import im.conversations.android.xmpp.model.carbons.Received; +import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.stanza.IQ; +import im.conversations.android.xmpp.processor.MessageProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class CarbonsManager extends AbstractManager { + private static final Logger LOGGER = LoggerFactory.getLogger(CarbonsManager.class); + + private final MessageProcessor messageProcessor; + private boolean enabled = false; public CarbonsManager(Context context, XmppConnection connection) { super(context, connection); + this.messageProcessor = new MessageProcessor(context, connection, false); } public void enable() { @@ -22,14 +30,11 @@ public class CarbonsManager extends AbstractManager { iq, result -> { if (result.getType() == IQ.Type.RESULT) { - Log.d( - Config.LOGTAG, - getAccount().address + ": successfully enabled carbons"); + LOGGER.info("{}: successfully enabled carbons", getAccount().address); this.enabled = true; } else { - Log.d( - Config.LOGTAG, - getAccount().address + ": could not enable carbons " + result); + LOGGER.warn( + "{}: could not enable carbons {}", getAccount().address, result); } }); } @@ -41,4 +46,30 @@ public class CarbonsManager extends AbstractManager { public boolean isEnabled() { return this.enabled; } + + public void handleReceived(final Received received) { + final var forwarded = received.getForwarded(); + final var message = forwarded == null ? null : forwarded.getMessage(); + if (message == null) { + LOGGER.warn("Received carbon copy did not contain forwarded message"); + } else if (connection.toAccount(message)) { + // all received, forwarded messages must be addressed to us + this.messageProcessor.accept(message); + } else { + LOGGER.warn("Received carbon copy had invalid `to` attribute {}", message.getTo()); + } + } + + public void handleSent(final Sent sent) { + final var forwarded = sent.getForwarded(); + final var message = forwarded == null ? null : forwarded.getMessage(); + if (message == null) { + LOGGER.warn("Sent carbon copy did not contain forwarded message"); + } else if (connection.fromAccount(message)) { + // all sent, forwarded messages must be addressed from us + this.messageProcessor.accept(message); + } else { + LOGGER.warn("Sent carbon copy had invalid `from` attribute {}", message.getFrom()); + } + } } diff --git a/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java b/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java index ebe4f64a4..300c8e317 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java @@ -1,18 +1,20 @@ package im.conversations.android.xmpp.manager; import android.content.Context; -import android.util.Log; import com.google.common.base.Strings; import com.google.common.collect.Collections2; -import eu.siacs.conversations.Config; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.roster.Item; import im.conversations.android.xmpp.model.roster.Query; import im.conversations.android.xmpp.model.stanza.IQ; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RosterManager extends AbstractManager { + private static final Logger LOGGER = LoggerFactory.getLogger(RosterManager.class); + public RosterManager(final Context context, final XmppConnection connection) { super(context, connection); } @@ -31,9 +33,9 @@ public class RosterManager extends AbstractManager { final Query rosterQuery = new Query(); iqPacket.addChild(rosterQuery); if (Strings.isNullOrEmpty(rosterVersion)) { - Log.d(Config.LOGTAG, account.address + ": fetching roster"); + LOGGER.info("{}: fetching roster", account.address); } else { - Log.d(Config.LOGTAG, account.address + ": fetching roster version " + rosterVersion); + LOGGER.info("{}: fetching roster version {}", account.address, rosterVersion); rosterQuery.setVersion(rosterVersion); } connection.sendIqPacket(iqPacket, this::handleFetchResult); @@ -52,7 +54,7 @@ public class RosterManager extends AbstractManager { final var database = getDatabase(); final var version = query.getVersion(); final var items = query.getExtensions(Item.class); - // In a roster result (Section 2.1.4), the client MUST ignore values of the c'subscription' + // In a roster result (Section 2.1.4), the client MUST ignore values of the 'subscription' // attribute other than "none", "to", "from", or "both". final var validItems = Collections2.filter( diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java new file mode 100644 index 000000000..507869a60 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Received extends Extension { + + public Received() { + super(Received.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java new file mode 100644 index 000000000..0201c53c6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Sent extends Extension { + + public Sent() { + super(Sent.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java new file mode 100644 index 000000000..80a646a41 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.forward; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.stanza.Message; + +@XmlElement(namespace = Namespace.FORWARD) +public class Forwarded extends Extension { + + public Forwarded() { + super(Forwarded.class); + } + + public Message getMessage() { + return this.getExtension(Message.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java index 8edd96d19..4c867f81d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java @@ -32,7 +32,7 @@ public class Item extends Extension { public Subscription getSubscription() { final String value = this.getAttribute("subscription"); try { - return value == null ? null : Subscription.valueOf(value.toLowerCase(Locale.ROOT)); + return value == null ? null : Subscription.valueOf(value.toUpperCase(Locale.ROOT)); } catch (final IllegalArgumentException e) { return null; } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java index 5340833ae..8ae877fb2 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -8,4 +8,8 @@ public class Message extends Stanza { public Message() { super(Message.class); } + + public String getBody() { + return this.findChildContent("body"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 80147fffd..a1728c09c 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -9,6 +9,7 @@ import im.conversations.android.xmpp.manager.BlockingManager; import im.conversations.android.xmpp.manager.BookmarkManager; import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.manager.RosterManager; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.function.Consumer; public class BindProcessor extends XmppConnection.Delegate implements Consumer { @@ -46,6 +47,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer { +public class MessageProcessor extends XmppConnection.Delegate implements Consumer { - public MessageProcessor(final Context context, final XmppConnection connection) {} + private static final Logger LOGGER = LoggerFactory.getLogger(MessageProcessor.class); + + private final boolean isRoot; + + public MessageProcessor(final Context context, final XmppConnection connection) { + this(context, connection, true); + } + + public MessageProcessor( + final Context context, final XmppConnection connection, final boolean isRoot) { + super(context, connection); + this.isRoot = isRoot; + } @Override - public void accept(final Message messagePacket) {} + public void accept(final Message message) { + + if (isRoot && connection.fromServer(message) && message.hasExtension(Received.class)) { + getManager(CarbonsManager.class).handleReceived(message.getExtension(Received.class)); + } + + if (isRoot && connection.fromServer(message) && message.hasExtension(Sent.class)) { + getManager(CarbonsManager.class).handleSent(message.getExtension(Sent.class)); + } + + final String body = message.getBody(); + if (!Strings.isNullOrEmpty(body)) { + LOGGER.info("'{}' from {}", body, message.getFrom()); + } + } }