Perform decryptions inline.

This commit is contained in:
Greyson Parrelli
2023-03-06 13:40:44 -05:00
committed by Alex Hart
parent e222f96310
commit 1b2cb2637f
18 changed files with 614 additions and 615 deletions

View File

@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.payments.Payments;
@@ -97,7 +96,6 @@ public class ApplicationDependencies {
private static volatile SignalServiceMessageSender messageSender;
private static volatile SignalServiceMessageReceiver messageReceiver;
private static volatile IncomingMessageObserver incomingMessageObserver;
private static volatile IncomingMessageProcessor incomingMessageProcessor;
private static volatile BackgroundMessageRetriever backgroundMessageRetriever;
private static volatile LiveRecipientCache recipientCache;
private static volatile JobManager jobManager;
@@ -274,18 +272,6 @@ public class ApplicationDependencies {
return provider.provideSignalServiceNetworkAccess();
}
public static @NonNull IncomingMessageProcessor getIncomingMessageProcessor() {
if (incomingMessageProcessor == null) {
synchronized (LOCK) {
if (incomingMessageProcessor == null) {
incomingMessageProcessor = provider.provideIncomingMessageProcessor();
}
}
}
return incomingMessageProcessor;
}
public static @NonNull BackgroundMessageRetriever getBackgroundMessageRetriever() {
if (backgroundMessageRetriever == null) {
synchronized (LOCK) {
@@ -693,7 +679,6 @@ public class ApplicationDependencies {
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration);
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration);
@NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess();
@NonNull IncomingMessageProcessor provideIncomingMessageProcessor();
@NonNull BackgroundMessageRetriever provideBackgroundMessageRetriever();
@NonNull LiveRecipientCache provideRecipientCache();
@NonNull JobManager provideJobManager();

View File

@@ -47,7 +47,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
import org.thoughtcrime.securesms.net.SignalWebSocketHealthMonitor;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
@@ -157,11 +156,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new SignalServiceNetworkAccess(context);
}
@Override
public @NonNull IncomingMessageProcessor provideIncomingMessageProcessor() {
return new IncomingMessageProcessor(context);
}
@Override
public @NonNull BackgroundMessageRetriever provideBackgroundMessageRetriever() {
return new BackgroundMessageRetriever();

View File

@@ -21,7 +21,7 @@ public final class DecryptionsDrainedConstraint implements Constraint {
@Override
public boolean isMet() {
return ApplicationDependencies.getIncomingMessageObserver().isDecryptionDrained();
return ApplicationDependencies.getIncomingMessageObserver().getDecryptionDrained();
}
@Override

View File

@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.migrations.BlobStorageLocationMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
import org.thoughtcrime.securesms.migrations.ClearGlideCacheMigrationJob;
import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob;
import org.thoughtcrime.securesms.migrations.DecryptionsDrainedMigrationJob;
import org.thoughtcrime.securesms.migrations.DeleteDeprecatedLogsMigrationJob;
import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob;
import org.thoughtcrime.securesms.migrations.EmojiDownloadMigrationJob;
@@ -215,6 +216,7 @@ public final class JobManagerFactories {
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory());
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
put(DecryptionsDrainedMigrationJob.KEY, new DecryptionsDrainedMigrationJob.Factory());
put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory());
put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory());
put(EmojiDownloadMigrationJob.KEY, new EmojiDownloadMigrationJob.Factory());

View File

@@ -9,9 +9,8 @@ import org.thoughtcrime.securesms.jobmanager.Job;
/**
* A job that has the same queue as {@link PushDecryptMessageJob} that we enqueue so we can notify
* the {@link org.thoughtcrime.securesms.messages.IncomingMessageObserver} when decryptions have
* finished. This lets us know not just when the websocket is drained, but when all the decryptions
* for the messages we pulled down from the websocket have been finished.
* the {@link org.thoughtcrime.securesms.messages.IncomingMessageObserver} when the decryption job
* queue is empty.
*/
public class PushDecryptDrainedJob extends BaseJob {

View File

@@ -47,6 +47,7 @@ class PushDecryptMessageJob private constructor(
private const val KEY_ENVELOPE = "envelope"
}
@Deprecated("No more jobs of this type should be enqueued. Decryptions now happen as things come off of the websocket.")
@JvmOverloads
constructor(envelope: SignalServiceEnvelope, smsMessageId: Long = -1) : this(
Parameters.Builder()

View File

@@ -1,388 +0,0 @@
package org.thoughtcrime.securesms.messages;
import android.app.Application;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil;
import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob;
import org.thoughtcrime.securesms.jobs.UnableToStartException;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor.Processor;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* The application-level manager of our websocket connection.
* <p>
* This class is responsible for opening/closing the websocket based on the app's state and observing new inbound messages received on the websocket.
*/
public class IncomingMessageObserver {
private static final String TAG = Log.tag(IncomingMessageObserver.class);
public static final int FOREGROUND_ID = 313399;
private static final long REQUEST_TIMEOUT_MINUTES = 1;
private static final long OLD_REQUEST_WINDOW_MS = TimeUnit.MINUTES.toMillis(5);
private static final long MAX_BACKGROUND_TIME = TimeUnit.MINUTES.toMillis(5);
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
private final Application context;
private final List<Runnable> decryptionDrainedListeners;
private final BroadcastReceiver connectionReceiver;
private final Map<String, Long> keepAliveTokens;
private boolean appVisible;
private long lastInteractionTime;
private volatile boolean networkDrained;
private volatile boolean decryptionDrained;
private volatile boolean terminated;
public IncomingMessageObserver(@NonNull Application context) {
if (INSTANCE_COUNT.incrementAndGet() != 1) {
throw new AssertionError("Multiple observers!");
}
this.context = context;
this.decryptionDrainedListeners = new CopyOnWriteArrayList<>();
this.keepAliveTokens = new HashMap<>();
this.lastInteractionTime = System.currentTimeMillis();
new MessageRetrievalThread().start();
if (!SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced()) {
try {
ForegroundServiceUtil.startWhenCapable(context, new Intent(context, ForegroundService.class));
} catch (UnableToStartException e) {
Log.w(TAG, "Unable to start foreground service for websocket!", e);
}
}
ApplicationDependencies.getAppForegroundObserver().addListener(new AppForegroundObserver.Listener() {
@Override
public void onForeground() {
onAppForegrounded();
}
@Override
public void onBackground() {
onAppBackgrounded();
}
});
connectionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
synchronized (IncomingMessageObserver.this) {
if (!NetworkConstraint.isMet(context)) {
Log.w(TAG, "Lost network connection. Shutting down our websocket connections and resetting the drained state.");
networkDrained = false;
decryptionDrained = false;
disconnect();
}
IncomingMessageObserver.this.notifyAll();
}
}
};
context.registerReceiver(connectionReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
public synchronized void notifyRegistrationChanged() {
notifyAll();
}
public synchronized void addDecryptionDrainedListener(@NonNull Runnable listener) {
decryptionDrainedListeners.add(listener);
if (decryptionDrained) {
listener.run();
}
}
public synchronized void removeDecryptionDrainedListener(@NonNull Runnable listener) {
decryptionDrainedListeners.remove(listener);
}
public boolean isDecryptionDrained() {
return decryptionDrained;
}
/**
* @return True if the websocket is active, otherwise false.
*/
public boolean isActive() {
return isConnectionNecessary();
}
public void notifyDecryptionsDrained() {
List<Runnable> listenersToTrigger = new ArrayList<>(decryptionDrainedListeners.size());
synchronized (this) {
if (networkDrained && !decryptionDrained) {
Log.i(TAG, "Decryptions newly drained.");
decryptionDrained = true;
listenersToTrigger.addAll(decryptionDrainedListeners);
}
}
for (Runnable listener : listenersToTrigger) {
listener.run();
}
}
private synchronized void onAppForegrounded() {
appVisible = true;
context.startService(new Intent(context, BackgroundService.class));
notifyAll();
}
private synchronized void onAppBackgrounded() {
appVisible = false;
lastInteractionTime = System.currentTimeMillis();
notifyAll();
}
private synchronized boolean isConnectionNecessary() {
boolean registered = SignalStore.account().isRegistered();
boolean fcmEnabled = SignalStore.account().isFcmEnabled();
boolean hasNetwork = NetworkConstraint.isMet(context);
boolean hasProxy = SignalStore.proxy().isProxyEnabled();
boolean forceWebsocket = SignalStore.internalValues().isWebsocketModeForced();
long oldRequest = System.currentTimeMillis() - OLD_REQUEST_WINDOW_MS;
long timeIdle = appVisible ? 0 : System.currentTimeMillis() - lastInteractionTime;
boolean removedRequests = keepAliveTokens.entrySet().removeIf(e -> e.getValue() < oldRequest);
if (removedRequests) {
Log.d(TAG, "Removed old keep web socket open requests.");
}
String lastInteractionString = appVisible ? "N/A" : timeIdle + " ms (" + (timeIdle < MAX_BACKGROUND_TIME ? "within limit" : "over limit") + ")";
boolean conclusion = registered &&
(appVisible || timeIdle < MAX_BACKGROUND_TIME || !fcmEnabled || Util.hasItems(keepAliveTokens)) &&
hasNetwork;
String needsConnectionString = conclusion ? "Needs Connection" : "Does Not Need Connection";
Log.d(TAG, String.format(Locale.US, "[" + needsConnectionString + "] Network: %s, Foreground: %s, Time Since Last Interaction: %s, FCM: %s, Stay open requests: [%s], Registered: %s, Proxy: %s, Force websocket: %s",
hasNetwork, appVisible, lastInteractionString, fcmEnabled, Util.join(keepAliveTokens.entrySet(), ","), registered, hasProxy, forceWebsocket));
return conclusion;
}
private synchronized void waitForConnectionNecessary() {
try {
while (!isConnectionNecessary()) wait();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public void terminateAsync() {
INSTANCE_COUNT.decrementAndGet();
context.unregisterReceiver(connectionReceiver);
SignalExecutors.BOUNDED.execute(() -> {
Log.w(TAG, "Beginning termination.");
terminated = true;
disconnect();
});
}
private void disconnect() {
ApplicationDependencies.getSignalWebSocket().disconnect();
}
public synchronized void registerKeepAliveToken(String key) {
keepAliveTokens.put(key, System.currentTimeMillis());
lastInteractionTime = System.currentTimeMillis();
notifyAll();
}
public synchronized void removeKeepAliveToken(String key) {
keepAliveTokens.remove(key);
lastInteractionTime = System.currentTimeMillis();
notifyAll();
}
private class MessageRetrievalThread extends Thread implements Thread.UncaughtExceptionHandler {
MessageRetrievalThread() {
super("MessageRetrievalService");
Log.i(TAG, "Initializing! (" + this.hashCode() + ")");
setUncaughtExceptionHandler(this);
}
@Override
public void run() {
int attempts = 0;
while (!terminated) {
Log.i(TAG, "Waiting for websocket state change....");
if (attempts > 1) {
long backoff = BackoffUtil.exponentialBackoff(attempts, TimeUnit.SECONDS.toMillis(30));
Log.w(TAG, "Too many failed connection attempts, attempts: " + attempts + " backing off: " + backoff);
ThreadUtil.sleep(backoff);
}
waitForConnectionNecessary();
Log.i(TAG, "Making websocket connection....");
SignalWebSocket signalWebSocket = ApplicationDependencies.getSignalWebSocket();
Disposable webSocketDisposable = signalWebSocket.getWebSocketState().subscribe(state -> {
Log.d(TAG, "WebSocket State: " + state);
// Any state change at all means that we are not drained
networkDrained = false;
decryptionDrained = false;
});
signalWebSocket.connect();
try {
while (isConnectionNecessary()) {
try {
Log.d(TAG, "Reading message...");
Optional<SignalServiceEnvelope> result = signalWebSocket.readOrEmpty(TimeUnit.MINUTES.toMillis(REQUEST_TIMEOUT_MINUTES), envelope -> {
Log.i(TAG, "Retrieved envelope! " + envelope.getTimestamp());
try (Processor processor = ApplicationDependencies.getIncomingMessageProcessor().acquire()) {
processor.processEnvelope(envelope);
}
});
attempts = 0;
if (!result.isPresent() && !networkDrained) {
Log.i(TAG, "Network was newly-drained. Enqueuing a job to listen for decryption draining.");
networkDrained = true;
ApplicationDependencies.getJobManager().add(new PushDecryptDrainedJob());
} else if (!result.isPresent()) {
Log.w(TAG, "Got tombstone, but we thought the network was already drained!");
}
} catch (WebSocketUnavailableException e) {
Log.i(TAG, "Pipe unexpectedly unavailable, connecting");
signalWebSocket.connect();
} catch (TimeoutException e) {
Log.w(TAG, "Application level read timeout...");
attempts = 0;
}
}
if (!appVisible) {
BackgroundService.stop(context);
}
} catch (Throwable e) {
attempts++;
Log.w(TAG, e);
} finally {
Log.w(TAG, "Shutting down pipe...");
disconnect();
webSocketDisposable.dispose();
}
Log.i(TAG, "Looping...");
}
Log.w(TAG, "Terminated! (" + this.hashCode() + ")");
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.w(TAG, "*** Uncaught exception!");
Log.w(TAG, e);
}
}
public static class ForegroundService extends Service {
@Override
public @Nullable IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), NotificationChannels.getInstance().BACKGROUND);
builder.setContentTitle(getApplicationContext().getString(R.string.MessageRetrievalService_signal));
builder.setContentText(getApplicationContext().getString(R.string.MessageRetrievalService_background_connection_enabled));
builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setWhen(0);
builder.setSmallIcon(R.drawable.ic_signal_background_connection);
startForeground(FOREGROUND_ID, builder.build());
return Service.START_STICKY;
}
}
/**
* A service that exists just to encourage the system to keep our process alive a little longer.
*/
public static class BackgroundService extends Service {
public static void start(Context context) {
try {
context.startService(new Intent(context, BackgroundService.class));
} catch (Exception e) {
Log.w(TAG, "Failed to start background service.", e);
}
}
public static void stop(Context context) {
context.stopService(new Intent(context, BackgroundService.class));
}
@Override
public @Nullable IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "Background service started.");
return START_STICKY;
}
@Override
public void onDestroy() {
Log.d(TAG, "Background service destroyed.");
}
}
}

View File

@@ -0,0 +1,509 @@
package org.thoughtcrime.securesms.messages
import android.annotation.SuppressLint
import android.app.Application
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.IBinder
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobmanager.JobTracker.JobListener
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startWhenCapable
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* The application-level manager of our websocket connection.
*
*
* This class is responsible for opening/closing the websocket based on the app's state and observing new inbound messages received on the websocket.
*/
class IncomingMessageObserver(private val context: Application) {
companion object {
private val TAG = Log.tag(IncomingMessageObserver::class.java)
private val WEBSOCKET_READ_TIMEOUT = TimeUnit.MINUTES.toMillis(1)
private val KEEP_ALIVE_TOKEN_MAX_AGE = TimeUnit.MINUTES.toMillis(5)
private val MAX_BACKGROUND_TIME = TimeUnit.MINUTES.toMillis(5)
private val INSTANCE_COUNT = AtomicInteger(0)
const val FOREGROUND_ID = 313399
}
private val decryptionDrainedListeners: MutableList<Runnable> = CopyOnWriteArrayList()
private val keepAliveTokens: MutableMap<String, Long> = mutableMapOf()
private val connectionReceiver: BroadcastReceiver
private val lock: ReentrantLock = ReentrantLock()
private val condition: Condition = lock.newCondition()
private var appVisible = false
private var lastInteractionTime: Long = System.currentTimeMillis()
@Volatile
private var terminated = false
@Volatile
var decryptionDrained = false
private set
init {
if (INSTANCE_COUNT.incrementAndGet() != 1) {
throw AssertionError("Multiple observers!")
}
MessageRetrievalThread().start()
if (!SignalStore.account().fcmEnabled || SignalStore.internalValues().isWebsocketModeForced) {
try {
startWhenCapable(context, Intent(context, ForegroundService::class.java))
} catch (e: UnableToStartException) {
Log.w(TAG, "Unable to start foreground service for websocket!", e)
}
}
ApplicationDependencies.getAppForegroundObserver().addListener(object : AppForegroundObserver.Listener {
override fun onForeground() {
onAppForegrounded()
}
override fun onBackground() {
onAppBackgrounded()
}
})
connectionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
lock.withLock {
if (!NetworkConstraint.isMet(context)) {
Log.w(TAG, "Lost network connection. Shutting down our websocket connections and resetting the drained state.")
decryptionDrained = false
disconnect()
}
condition.signalAll()
}
}
}
context.registerReceiver(connectionReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
fun notifyRegistrationChanged() {
lock.withLock {
condition.signalAll()
}
}
fun addDecryptionDrainedListener(listener: Runnable) {
decryptionDrainedListeners.add(listener)
if (decryptionDrained) {
listener.run()
}
}
fun removeDecryptionDrainedListener(listener: Runnable) {
decryptionDrainedListeners.remove(listener)
}
fun notifyDecryptionsDrained() {
if (ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE)) {
Log.i(TAG, "Queue was empty when notified. Signaling change.")
lock.withLock {
condition.signalAll()
}
} else {
Log.i(TAG, "Queue still had items when notified. Registering listener to signal change.")
ApplicationDependencies.getJobManager().addListener(
{ it.parameters.queue == PushDecryptMessageJob.QUEUE },
DecryptionDrainedQueueListener()
)
}
}
private fun onAppForegrounded() {
lock.withLock {
appVisible = true
context.startService(Intent(context, BackgroundService::class.java))
condition.signalAll()
}
}
private fun onAppBackgrounded() {
lock.withLock {
appVisible = false
lastInteractionTime = System.currentTimeMillis()
condition.signalAll()
}
}
private fun isConnectionNecessary(): Boolean {
lock.withLock {
val registered = SignalStore.account().isRegistered
val fcmEnabled = SignalStore.account().fcmEnabled
val hasNetwork = NetworkConstraint.isMet(context)
val hasProxy = SignalStore.proxy().isProxyEnabled
val forceWebsocket = SignalStore.internalValues().isWebsocketModeForced
val keepAliveCutoffTime = System.currentTimeMillis() - KEEP_ALIVE_TOKEN_MAX_AGE
val timeIdle = if (appVisible) 0 else System.currentTimeMillis() - lastInteractionTime
val removedRequests = keepAliveTokens.entries.removeIf { (_, createTime) -> createTime < keepAliveCutoffTime }
val decryptQueueEmpty = ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE)
if (removedRequests) {
Log.d(TAG, "Removed old keep web socket open requests.")
}
val lastInteractionString = if (appVisible) "N/A" else timeIdle.toString() + " ms (" + (if (timeIdle < MAX_BACKGROUND_TIME) "within limit" else "over limit") + ")"
val conclusion = registered &&
(appVisible || timeIdle < MAX_BACKGROUND_TIME || !fcmEnabled || Util.hasItems(keepAliveTokens)) &&
hasNetwork &&
decryptQueueEmpty
val needsConnectionString = if (conclusion) "Needs Connection" else "Does Not Need Connection"
Log.d(TAG, "[$needsConnectionString] Network: $hasNetwork, Foreground: $appVisible, Time Since Last Interaction: $lastInteractionString, FCM: $fcmEnabled, Stay open requests: [${keepAliveTokens.entries}], Registered: $registered, Proxy: $hasProxy, Force websocket: $forceWebsocket, Decrypt Queue Empty: $decryptQueueEmpty")
return conclusion
}
}
private fun waitForConnectionNecessary() {
lock.withLock {
try {
while (!isConnectionNecessary()) {
condition.await()
}
} catch (e: InterruptedException) {
throw AssertionError(e)
}
}
}
fun terminateAsync() {
INSTANCE_COUNT.decrementAndGet()
context.unregisterReceiver(connectionReceiver)
SignalExecutors.BOUNDED.execute {
Log.w(TAG, "Beginning termination.")
terminated = true
disconnect()
}
}
private fun disconnect() {
ApplicationDependencies.getSignalWebSocket().disconnect()
}
fun registerKeepAliveToken(key: String) {
lock.withLock {
keepAliveTokens[key] = System.currentTimeMillis()
lastInteractionTime = System.currentTimeMillis()
condition.signalAll()
}
}
fun removeKeepAliveToken(key: String) {
lock.withLock {
keepAliveTokens.remove(key)
lastInteractionTime = System.currentTimeMillis()
condition.signalAll()
}
}
@VisibleForTesting
fun processEnvelope(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
when (envelope.type.number) {
SignalServiceProtos.Envelope.Type.RECEIPT_VALUE -> processReceipt(envelope)
SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE,
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE,
SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE -> processMessage(envelope, serverDeliveredTimestamp)
else -> Log.w(TAG, "Received envelope of unknown type: " + envelope.type)
}
}
private fun processMessage(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
val result = MessageDecryptor.decrypt(context, envelope, serverDeliveredTimestamp)
when (result) {
is MessageDecryptor.Result.Success -> {
ApplicationDependencies.getJobManager().add(
PushProcessMessageJob(
result.toMessageState(),
result.toSignalServiceContent(),
null,
-1,
result.envelope.timestamp
)
)
}
is MessageDecryptor.Result.Error -> {
ApplicationDependencies.getJobManager().add(
PushProcessMessageJob(
result.toMessageState(),
null,
result.errorMetadata.toExceptionMetadata(),
-1,
result.envelope.timestamp
)
)
}
is MessageDecryptor.Result.Ignore -> {
// No action needed
}
else -> {
throw AssertionError("Unexpected result! ${result.javaClass.simpleName}")
}
}
result.followUpOperations.forEach { it.run() }
}
private fun processReceipt(envelope: SignalServiceProtos.Envelope) {
if (!UuidUtil.isUuid(envelope.sourceUuid)) {
Log.w(TAG, "Invalid envelope source UUID!")
return
}
val senderId = RecipientId.from(ServiceId.parseOrThrow(envelope.sourceUuid))
Log.i(TAG, "Received server receipt. Sender: $senderId, Device: ${envelope.sourceDevice}, Timestamp: ${envelope.timestamp}")
SignalDatabase.messages.incrementDeliveryReceiptCount(MessageTable.SyncMessageId(senderId, envelope.timestamp), System.currentTimeMillis())
SignalDatabase.messageLog.deleteEntryForRecipient(envelope.timestamp, senderId, envelope.sourceDevice)
}
private fun MessageDecryptor.Result.toMessageState(): MessageContentProcessor.MessageState {
return when (this) {
is MessageDecryptor.Result.DecryptionError -> MessageContentProcessor.MessageState.DECRYPTION_ERROR
is MessageDecryptor.Result.Ignore -> MessageContentProcessor.MessageState.NOOP
is MessageDecryptor.Result.InvalidVersion -> MessageContentProcessor.MessageState.INVALID_VERSION
is MessageDecryptor.Result.LegacyMessage -> MessageContentProcessor.MessageState.LEGACY_MESSAGE
is MessageDecryptor.Result.Success -> MessageContentProcessor.MessageState.DECRYPTED_OK
is MessageDecryptor.Result.UnsupportedDataMessage -> MessageContentProcessor.MessageState.UNSUPPORTED_DATA_MESSAGE
}
}
private fun MessageDecryptor.Result.Success.toSignalServiceContent(): SignalServiceContent {
val localAddress = SignalServiceAddress(this.metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
val metadata = SignalServiceMetadata(
SignalServiceAddress(this.metadata.sourceServiceId, Optional.ofNullable(this.metadata.sourceE164)),
this.metadata.sourceDeviceId,
this.envelope.timestamp,
this.envelope.serverTimestamp,
this.serverDeliveredTimestamp,
this.metadata.sealedSender,
this.envelope.serverGuid,
Optional.ofNullable(this.metadata.groupId),
this.metadata.destinationServiceId.toString()
)
val contentProto = SignalServiceContentProto.newBuilder()
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(metadata))
.setContent(content)
.build()
return SignalServiceContent.createFromProto(contentProto)!!
}
private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): MessageContentProcessor.ExceptionMetadata {
return MessageContentProcessor.ExceptionMetadata(
this.sender,
this.senderDevice,
this.groupId
)
}
private inner class MessageRetrievalThread : Thread("MessageRetrievalService"), Thread.UncaughtExceptionHandler {
init {
Log.i(TAG, "Initializing! (" + this.hashCode() + ")")
uncaughtExceptionHandler = this
}
override fun run() {
var attempts = 0
while (!terminated) {
Log.i(TAG, "Waiting for websocket state change....")
if (attempts > 1) {
val backoff = BackoffUtil.exponentialBackoff(attempts, TimeUnit.SECONDS.toMillis(30))
Log.w(TAG, "Too many failed connection attempts, attempts: $attempts backing off: $backoff")
ThreadUtil.sleep(backoff)
}
waitForConnectionNecessary()
Log.i(TAG, "Making websocket connection....")
val signalWebSocket = ApplicationDependencies.getSignalWebSocket()
val webSocketDisposable = signalWebSocket.webSocketState.subscribe { state: WebSocketConnectionState ->
Log.d(TAG, "WebSocket State: $state")
// Any state change at all means that we are not drained
decryptionDrained = false
}
signalWebSocket.connect()
try {
while (isConnectionNecessary()) {
try {
Log.d(TAG, "Reading message...")
val hasMore = signalWebSocket.readMessage(WEBSOCKET_READ_TIMEOUT) { envelope, serverDeliveredTimestamp ->
Log.i(TAG, "Retrieved envelope! " + envelope.timestamp)
processEnvelope(envelope, serverDeliveredTimestamp)
true
}
attempts = 0
if (!hasMore && !decryptionDrained) {
Log.i(TAG, "Decryptions newly-drained.")
decryptionDrained = true
for (listener in decryptionDrainedListeners.toList()) {
listener.run()
}
} else if (!hasMore) {
Log.w(TAG, "Got tombstone, but we thought the network was already drained!")
}
} catch (e: WebSocketUnavailableException) {
Log.i(TAG, "Pipe unexpectedly unavailable, connecting")
signalWebSocket.connect()
} catch (e: TimeoutException) {
Log.w(TAG, "Application level read timeout...")
attempts = 0
}
}
if (!appVisible) {
BackgroundService.stop(context)
}
} catch (e: Throwable) {
attempts++
Log.w(TAG, e)
} finally {
Log.w(TAG, "Shutting down pipe...")
disconnect()
webSocketDisposable.dispose()
}
Log.i(TAG, "Looping...")
}
Log.w(TAG, "Terminated! (" + this.hashCode() + ")")
}
override fun uncaughtException(t: Thread, e: Throwable) {
Log.w(TAG, "Uncaught exception in message thread!", e)
}
}
private inner class DecryptionDrainedQueueListener : JobListener {
@SuppressLint("WrongThread")
override fun onStateChanged(job: Job, jobState: JobTracker.JobState) {
if (jobState.isComplete) {
if (ApplicationDependencies.getJobManager().isQueueEmpty(PushDecryptMessageJob.QUEUE)) {
Log.i(TAG, "Queue is now empty. Signaling change.")
lock.withLock {
condition.signalAll()
}
ApplicationDependencies.getJobManager().removeListener(this)
} else {
Log.i(TAG, "Item finished in queue, but it's still not empty. Waiting to signal change.")
}
}
}
}
class ForegroundService : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val notification = NotificationCompat.Builder(applicationContext, NotificationChannels.getInstance().BACKGROUND)
.setContentTitle(applicationContext.getString(R.string.MessageRetrievalService_signal))
.setContentText(applicationContext.getString(R.string.MessageRetrievalService_background_connection_enabled))
.setPriority(NotificationCompat.PRIORITY_MIN)
.setWhen(0)
.setSmallIcon(R.drawable.ic_signal_background_connection)
.build()
startForeground(FOREGROUND_ID, notification)
return START_STICKY
}
}
/**
* A service that exists just to encourage the system to keep our process alive a little longer.
*/
class BackgroundService : Service() {
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "Background service started.")
return START_STICKY
}
override fun onDestroy() {
Log.d(TAG, "Background service destroyed.")
}
companion object {
fun start(context: Context) {
try {
context.startService(Intent(context, BackgroundService::class.java))
} catch (e: Exception) {
Log.w(TAG, "Failed to start background service.", e)
}
}
fun stop(context: Context) {
context.stopService(Intent(context, BackgroundService::class.java))
}
}
}
}

View File

@@ -1,112 +0,0 @@
package org.thoughtcrime.securesms.messages;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import java.io.Closeable;
import java.util.concurrent.locks.ReentrantLock;
/**
* The central entry point for all envelopes that have been retrieved. Envelopes must be processed
* here to guarantee proper ordering.
*/
public class IncomingMessageProcessor {
private static final String TAG = Log.tag(IncomingMessageProcessor.class);
private final Application context;
private final ReentrantLock lock;
public IncomingMessageProcessor(@NonNull Application context) {
this.context = context;
this.lock = new ReentrantLock();
}
/**
* @return An instance of a Processor that will allow you to process messages in a thread safe
* way. Must be closed.
*/
public Processor acquire() {
lock.lock();
return new Processor(context);
}
private void release() {
lock.unlock();
}
public class Processor implements Closeable {
private final Context context;
private final JobManager jobManager;
private Processor(@NonNull Context context) {
this.context = context;
this.jobManager = ApplicationDependencies.getJobManager();
}
/**
* @return The id of the {@link PushDecryptMessageJob} that was scheduled to process the message, if
* one was created. Otherwise null.
*/
public @Nullable String processEnvelope(@NonNull SignalServiceEnvelope envelope) {
if (envelope.hasSourceUuid()) {
Recipient.externalPush(envelope.getSourceAddress());
}
if (envelope.isReceipt()) {
processReceipt(envelope);
return null;
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isPlaintextContent()) {
return processMessage(envelope);
} else {
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
return null;
}
}
private @Nullable String processMessage(@NonNull SignalServiceEnvelope envelope) {
return processMessageDeferred(envelope);
}
private @Nullable String processMessageDeferred(@NonNull SignalServiceEnvelope envelope) {
Job job = new PushDecryptMessageJob(envelope);
jobManager.add(job);
return job.getId();
}
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
Recipient sender = Recipient.externalPush(envelope.getSourceAddress());
Log.i(TAG, "Received server receipt. Sender: " + sender.getId() + ", Device: " + envelope.getSourceDevice() + ", Timestamp: " + envelope.getTimestamp());
SignalDatabase.messages().incrementDeliveryReceiptCount(new SyncMessageId(sender.getId(), envelope.getTimestamp()), System.currentTimeMillis());
SignalDatabase.messageLog().deleteEntryForRecipient(envelope.getTimestamp(), sender.getId(), envelope.getSourceDevice());
}
@Override
public void close() {
release();
}
}
}

View File

@@ -122,9 +122,10 @@ public class ApplicationMigrations {
static final int GLIDE_CACHE_CLEAR = 77;
static final int SYSTEM_NAME_RESYNC = 78;
static final int RECOVERY_PASSWORD_SYNC = 79;
static final int DECRYPTIONS_DRAINED = 80;
}
public static final int CURRENT_VERSION = 79;
public static final int CURRENT_VERSION = 80;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@@ -542,6 +543,10 @@ public class ApplicationMigrations {
jobs.put(Version.RECOVERY_PASSWORD_SYNC, new AttributesMigrationJob());
}
if (lastSeenVersion < Version.DECRYPTIONS_DRAINED) {
jobs.put(Version.DECRYPTIONS_DRAINED, new DecryptionsDrainedMigrationJob());
}
return jobs;
}

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.migrations
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob
/**
* Kicks off a job to notify the [org.thoughtcrime.securesms.messages.IncomingMessageObserver] when the decryption queue is empty.
*/
internal class DecryptionsDrainedMigrationJob(
parameters: Parameters = Parameters.Builder().build()
) : MigrationJob(parameters) {
companion object {
val TAG = Log.tag(DecryptionsDrainedMigrationJob::class.java)
const val KEY = "DecryptionsDrainedMigrationJob"
}
override fun getFactoryKey(): String = KEY
override fun isUiBlocking(): Boolean = false
override fun performMigration() {
ApplicationDependencies.getJobManager().add(PushDecryptDrainedJob())
}
override fun shouldRetry(e: Exception): Boolean = false
class Factory : Job.Factory<DecryptionsDrainedMigrationJob> {
override fun create(parameters: Parameters, data: Data): DecryptionsDrainedMigrationJob {
return DecryptionsDrainedMigrationJob(parameters)
}
}
}