X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/f094cd6806aae68fba9ebd0cf29eaf4eabf042d2..004293362eed2871e2645c5388e7a8a62a754f73:/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index d2ffaaab..2ea96591 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -135,6 +135,11 @@ public class ManagerImpl implements Manager { private final Context context; private boolean hasCaughtUpWithOldMessages = false; + private boolean ignoreAttachments = false; + + private Thread receiveThread; + private final Set messageHandlers = new HashSet<>(); + private boolean isReceivingSynchronous; ManagerImpl( SignalAccount account, @@ -824,10 +829,10 @@ public class ManagerImpl implements Manager { return registeredUsers; } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + private void retryFailedReceivedMessages(ReceiveMessageHandler handler) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + var actions = retryFailedReceivedMessage(handler, cachedMessage); if (actions != null) { queuedActions.addAll(actions); } @@ -836,7 +841,7 @@ public class ManagerImpl implements Manager { } private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage + final ReceiveMessageHandler handler, final CachedMessage cachedMessage ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { @@ -872,14 +877,118 @@ public class ManagerImpl implements Manager { } @Override - public void receiveMessages( - long timeout, - TimeUnit unit, - boolean returnOnTimeout, - boolean ignoreAttachments, - ReceiveMessageHandler handler + public void addReceiveHandler(final ReceiveMessageHandler handler) { + if (isReceivingSynchronous) { + throw new IllegalStateException("Already receiving message synchronously."); + } + synchronized (messageHandlers) { + messageHandlers.add(handler); + + startReceiveThreadIfRequired(); + } + } + + private void startReceiveThreadIfRequired() { + if (receiveThread != null) { + return; + } + receiveThread = new Thread(() -> { + while (!Thread.interrupted()) { + try { + receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, decryptedContent, e) -> { + synchronized (messageHandlers) { + for (ReceiveMessageHandler h : messageHandlers) { + try { + h.handleMessage(envelope, decryptedContent, e); + } catch (Exception ex) { + logger.warn("Message handler failed, ignoring", ex); + } + } + } + }); + break; + } catch (IOException e) { + logger.warn("Receiving messages failed, retrying", e); + } + } + hasCaughtUpWithOldMessages = false; + synchronized (messageHandlers) { + receiveThread = null; + + // Check if in the meantime another handler has been registered + if (!messageHandlers.isEmpty()) { + startReceiveThreadIfRequired(); + } + } + }); + + receiveThread.start(); + } + + @Override + public void removeReceiveHandler(final ReceiveMessageHandler handler) { + final Thread thread; + synchronized (messageHandlers) { + thread = receiveThread; + receiveThread = null; + messageHandlers.remove(handler); + if (!messageHandlers.isEmpty() || isReceivingSynchronous) { + return; + } + } + + stopReceiveThread(thread); + } + + private void stopReceiveThread(final Thread thread) { + thread.interrupt(); + try { + thread.join(); + } catch (InterruptedException ignored) { + } + } + + @Override + public boolean isReceiving() { + if (isReceivingSynchronous) { + return true; + } + synchronized (messageHandlers) { + return messageHandlers.size() > 0; + } + } + + @Override + public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException { + receiveMessages(timeout, unit, true, handler); + } + + @Override + public void receiveMessages(ReceiveMessageHandler handler) throws IOException { + receiveMessages(1L, TimeUnit.HOURS, false, handler); + } + + private void receiveMessages( + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler + ) throws IOException { + if (isReceiving()) { + throw new IllegalStateException("Already receiving message."); + } + isReceivingSynchronous = true; + receiveThread = Thread.currentThread(); + try { + receiveMessagesInternal(timeout, unit, returnOnTimeout, handler); + } finally { + receiveThread = null; + hasCaughtUpWithOldMessages = false; + isReceivingSynchronous = false; + } + } + + private void receiveMessagesInternal( + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { - retryFailedReceivedMessages(handler, ignoreAttachments); + retryFailedReceivedMessages(handler); Set queuedActions = new HashSet<>(); @@ -887,6 +996,8 @@ public class ManagerImpl implements Manager { signalWebSocket.connect(); hasCaughtUpWithOldMessages = false; + var backOffCounter = 0; + final var MAX_BACKOFF_COUNTER = 9; while (!Thread.interrupted()) { SignalServiceEnvelope envelope; @@ -901,6 +1012,8 @@ public class ManagerImpl implements Manager { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); + backOffCounter = 0; + if (result.isPresent()) { envelope = result.get(); logger.debug("New message received from server"); @@ -924,11 +1037,24 @@ public class ManagerImpl implements Manager { } else { throw e; } - } catch (WebSocketUnavailableException e) { - logger.debug("Pipe unexpectedly unavailable, connecting"); - signalWebSocket.connect(); - continue; + } catch (IOException e) { + logger.debug("Pipe unexpectedly unavailable: {}", e.getMessage()); + if (e instanceof WebSocketUnavailableException || "Connection closed!".equals(e.getMessage())) { + final var sleepMilliseconds = 100 * (long) Math.pow(2, backOffCounter); + backOffCounter = Math.min(backOffCounter + 1, MAX_BACKOFF_COUNTER); + logger.warn("Connection closed unexpectedly, reconnecting in {} ms", sleepMilliseconds); + try { + Thread.sleep(sleepMilliseconds); + } catch (InterruptedException interruptedException) { + return; + } + hasCaughtUpWithOldMessages = false; + signalWebSocket.connect(); + continue; + } + throw e; } catch (TimeoutException e) { + backOffCounter = 0; if (returnOnTimeout) return; continue; } @@ -939,9 +1065,11 @@ public class ManagerImpl implements Manager { if (hasCaughtUpWithOldMessages) { handleQueuedActions(queuedActions); + queuedActions.clear(); } if (cachedMessage[0] != null) { if (exception instanceof UntrustedIdentityException) { + logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); final var recipientId = resolveRecipient(address); if (!envelope.hasSourceUuid()) { @@ -958,6 +1086,12 @@ public class ManagerImpl implements Manager { } } handleQueuedActions(queuedActions); + queuedActions.clear(); + } + + @Override + public void setIgnoreAttachments(final boolean ignoreAttachments) { + this.ignoreAttachments = ignoreAttachments; } @Override @@ -966,6 +1100,7 @@ public class ManagerImpl implements Manager { } private void handleQueuedActions(final Collection queuedActions) { + logger.debug("Handling message actions"); var interrupted = false; for (var action : queuedActions) { try { @@ -1060,11 +1195,12 @@ public class ManagerImpl implements Manager { } final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()); return new Identity(address, identityInfo.getIdentityKey(), identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), - identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), - identityInfo.getIdentityKey()).getSerialized(), + scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } @@ -1216,6 +1352,15 @@ public class ManagerImpl implements Manager { } private void close(boolean closeAccount) throws IOException { + Thread thread; + synchronized (messageHandlers) { + messageHandlers.clear(); + thread = receiveThread; + receiveThread = null; + } + if (thread != null) { + stopReceiveThread(thread); + } executor.shutdown(); dependencies.getSignalWebSocket().disconnect();