2020-05-20 13:06:08 +00:00
|
|
|
package org.thoughtcrime.securesms.messages;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2020-12-20 19:45:51 +00:00
|
|
|
import android.app.Application;
|
2018-10-15 20:27:21 +00:00
|
|
|
import android.app.Service;
|
2020-07-21 14:38:42 +00:00
|
|
|
import android.content.BroadcastReceiver;
|
2018-10-15 20:27:21 +00:00
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
2020-07-21 14:38:42 +00:00
|
|
|
import android.content.IntentFilter;
|
|
|
|
import android.net.ConnectivityManager;
|
2018-10-15 20:27:21 +00:00
|
|
|
import android.os.IBinder;
|
2020-12-04 23:31:58 +00:00
|
|
|
|
2019-06-05 19:47:14 +00:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.core.app.NotificationCompat;
|
|
|
|
import androidx.core.content.ContextCompat;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2021-02-03 00:56:50 +00:00
|
|
|
import org.signal.core.util.concurrent.SignalExecutors;
|
2020-12-04 23:31:58 +00:00
|
|
|
import org.signal.core.util.logging.Log;
|
|
|
|
import org.thoughtcrime.securesms.R;
|
2019-07-15 15:12:26 +00:00
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
2019-03-28 15:56:35 +00:00
|
|
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
2020-12-08 21:51:03 +00:00
|
|
|
import org.thoughtcrime.securesms.jobs.PushDecryptDrainedJob;
|
2021-02-02 21:42:47 +00:00
|
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
2020-12-04 23:31:58 +00:00
|
|
|
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor.Processor;
|
2018-10-15 20:27:21 +00:00
|
|
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
|
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
2021-02-08 20:37:45 +00:00
|
|
|
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
2018-10-15 20:27:21 +00:00
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
2020-07-21 14:38:42 +00:00
|
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
2021-07-09 14:01:24 +00:00
|
|
|
import org.whispersystems.signalservice.api.SignalWebSocket;
|
2020-07-21 14:38:42 +00:00
|
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
2021-07-09 14:01:24 +00:00
|
|
|
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
import java.util.ArrayList;
|
2020-07-21 14:38:42 +00:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
2018-10-15 20:27:21 +00:00
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import java.util.concurrent.TimeoutException;
|
2021-02-10 17:12:40 +00:00
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2020-07-21 14:38:42 +00:00
|
|
|
public class IncomingMessageObserver {
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2021-03-29 22:37:22 +00:00
|
|
|
private static final String TAG = Log.tag(IncomingMessageObserver.class);
|
2018-10-15 20:27:21 +00:00
|
|
|
|
|
|
|
public static final int FOREGROUND_ID = 313399;
|
|
|
|
private static final long REQUEST_TIMEOUT_MINUTES = 1;
|
|
|
|
|
2021-02-10 17:12:40 +00:00
|
|
|
private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
|
|
|
|
|
2020-12-20 19:45:51 +00:00
|
|
|
private final Application context;
|
2020-07-21 14:38:42 +00:00
|
|
|
private final SignalServiceNetworkAccess networkAccess;
|
2020-12-08 21:51:03 +00:00
|
|
|
private final List<Runnable> decryptionDrainedListeners;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
|
|
|
private boolean appVisible;
|
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
private volatile boolean networkDrained;
|
|
|
|
private volatile boolean decryptionDrained;
|
2021-02-02 21:42:47 +00:00
|
|
|
private volatile boolean terminated;
|
2018-10-15 20:27:21 +00:00
|
|
|
|
2020-12-20 19:45:51 +00:00
|
|
|
public IncomingMessageObserver(@NonNull Application context) {
|
2021-02-10 17:12:40 +00:00
|
|
|
if (INSTANCE_COUNT.incrementAndGet() != 1) {
|
|
|
|
throw new AssertionError("Multiple observers!");
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
this.context = context;
|
|
|
|
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
|
|
|
this.decryptionDrainedListeners = new CopyOnWriteArrayList<>();
|
2018-10-15 20:27:21 +00:00
|
|
|
|
|
|
|
new MessageRetrievalThread().start();
|
|
|
|
|
2019-01-24 11:04:28 +00:00
|
|
|
if (TextSecurePreferences.isFcmDisabled(context)) {
|
2018-10-15 20:27:21 +00:00
|
|
|
ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class));
|
|
|
|
}
|
|
|
|
|
2021-02-08 20:37:45 +00:00
|
|
|
ApplicationDependencies.getAppForegroundObserver().addListener(new AppForegroundObserver.Listener() {
|
2018-10-15 20:27:21 +00:00
|
|
|
@Override
|
2021-02-08 20:37:45 +00:00
|
|
|
public void onForeground() {
|
2018-10-15 20:27:21 +00:00
|
|
|
onAppForegrounded();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-02-08 20:37:45 +00:00
|
|
|
public void onBackground() {
|
2018-10-15 20:27:21 +00:00
|
|
|
onAppBackgrounded();
|
|
|
|
}
|
|
|
|
});
|
2020-05-20 13:06:08 +00:00
|
|
|
|
2020-07-21 14:38:42 +00:00
|
|
|
context.registerReceiver(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.");
|
2020-12-08 21:51:03 +00:00
|
|
|
networkDrained = false;
|
|
|
|
decryptionDrained = false;
|
2021-07-09 14:01:24 +00:00
|
|
|
shutdown();
|
2020-07-21 14:38:42 +00:00
|
|
|
}
|
|
|
|
IncomingMessageObserver.this.notifyAll();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
2018-10-15 20:27:21 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 15:07:34 +00:00
|
|
|
public synchronized void notifyRegistrationChanged() {
|
|
|
|
notifyAll();
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
public synchronized void addDecryptionDrainedListener(@NonNull Runnable listener) {
|
|
|
|
decryptionDrainedListeners.add(listener);
|
|
|
|
if (decryptionDrained) {
|
2020-07-21 14:38:42 +00:00
|
|
|
listener.run();
|
2018-10-15 20:27:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
public boolean isDecryptionDrained() {
|
2021-02-13 03:42:01 +00:00
|
|
|
return decryptionDrained || networkAccess.isCensored(context);
|
2020-12-08 21:51:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
2020-07-21 14:38:42 +00:00
|
|
|
}
|
|
|
|
|
2018-10-15 20:27:21 +00:00
|
|
|
private synchronized void onAppForegrounded() {
|
|
|
|
appVisible = true;
|
|
|
|
notifyAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized void onAppBackgrounded() {
|
|
|
|
appVisible = false;
|
|
|
|
notifyAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized boolean isConnectionNecessary() {
|
2020-07-21 14:38:42 +00:00
|
|
|
boolean registered = TextSecurePreferences.isPushRegistered(context);
|
|
|
|
boolean websocketRegistered = TextSecurePreferences.isWebsocketRegistered(context);
|
|
|
|
boolean isGcmDisabled = TextSecurePreferences.isFcmDisabled(context);
|
|
|
|
boolean hasNetwork = NetworkConstraint.isMet(context);
|
2021-02-02 21:42:47 +00:00
|
|
|
boolean hasProxy = SignalStore.proxy().isProxyEnabled();
|
2020-07-21 14:38:42 +00:00
|
|
|
|
2021-02-02 21:42:47 +00:00
|
|
|
Log.d(TAG, String.format("Network: %s, Foreground: %s, FCM: %s, Censored: %s, Registered: %s, Websocket Registered: %s, Proxy: %s",
|
|
|
|
hasNetwork, appVisible, !isGcmDisabled, networkAccess.isCensored(context), registered, websocketRegistered, hasProxy));
|
2020-07-21 14:38:42 +00:00
|
|
|
|
|
|
|
return registered &&
|
|
|
|
websocketRegistered &&
|
|
|
|
(appVisible || isGcmDisabled) &&
|
|
|
|
hasNetwork &&
|
2018-10-15 20:27:21 +00:00
|
|
|
!networkAccess.isCensored(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized void waitForConnectionNecessary() {
|
|
|
|
try {
|
|
|
|
while (!isConnectionNecessary()) wait();
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
throw new AssertionError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-03 00:56:50 +00:00
|
|
|
public void terminateAsync() {
|
2021-02-10 17:12:40 +00:00
|
|
|
INSTANCE_COUNT.decrementAndGet();
|
|
|
|
|
2021-02-03 00:56:50 +00:00
|
|
|
SignalExecutors.BOUNDED.execute(() -> {
|
|
|
|
Log.w(TAG, "Beginning termination.");
|
|
|
|
terminated = true;
|
2021-07-09 14:01:24 +00:00
|
|
|
shutdown();
|
2021-02-03 00:56:50 +00:00
|
|
|
});
|
2021-02-02 21:42:47 +00:00
|
|
|
}
|
|
|
|
|
2021-07-09 14:01:24 +00:00
|
|
|
private void shutdown() {
|
|
|
|
ApplicationDependencies.getSignalWebSocket().disconnect();
|
2018-05-22 09:13:10 +00:00
|
|
|
}
|
|
|
|
|
2018-10-15 20:27:21 +00:00
|
|
|
private class MessageRetrievalThread extends Thread implements Thread.UncaughtExceptionHandler {
|
|
|
|
|
|
|
|
MessageRetrievalThread() {
|
|
|
|
super("MessageRetrievalService");
|
2021-02-02 21:42:47 +00:00
|
|
|
Log.i(TAG, "Initializing! (" + this.hashCode() + ")");
|
2018-10-15 20:27:21 +00:00
|
|
|
setUncaughtExceptionHandler(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() {
|
2021-02-02 21:42:47 +00:00
|
|
|
while (!terminated) {
|
2018-10-15 20:27:21 +00:00
|
|
|
Log.i(TAG, "Waiting for websocket state change....");
|
|
|
|
waitForConnectionNecessary();
|
|
|
|
|
|
|
|
Log.i(TAG, "Making websocket connection....");
|
2021-07-09 14:01:24 +00:00
|
|
|
SignalWebSocket signalWebSocket = ApplicationDependencies.getSignalWebSocket();
|
|
|
|
signalWebSocket.connect();
|
2018-10-15 20:27:21 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
while (isConnectionNecessary()) {
|
|
|
|
try {
|
2020-07-21 14:38:42 +00:00
|
|
|
Log.d(TAG, "Reading message...");
|
2021-07-09 14:01:24 +00:00
|
|
|
Optional<SignalServiceEnvelope> result = signalWebSocket.readOrEmpty(TimeUnit.MINUTES.toMillis(REQUEST_TIMEOUT_MINUTES), envelope -> {
|
2020-07-21 14:38:42 +00:00
|
|
|
Log.i(TAG, "Retrieved envelope! " + envelope.getTimestamp());
|
|
|
|
try (Processor processor = ApplicationDependencies.getIncomingMessageProcessor().acquire()) {
|
|
|
|
processor.processEnvelope(envelope);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-12-08 21:51:03 +00:00
|
|
|
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());
|
2020-07-21 14:38:42 +00:00
|
|
|
}
|
2021-07-09 14:01:24 +00:00
|
|
|
} catch (WebSocketUnavailableException e) {
|
|
|
|
Log.i(TAG, "Pipe unexpectedly unavailable, connecting");
|
|
|
|
signalWebSocket.connect();
|
2018-10-15 20:27:21 +00:00
|
|
|
} catch (TimeoutException e) {
|
|
|
|
Log.w(TAG, "Application level read timeout...");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Throwable e) {
|
|
|
|
Log.w(TAG, e);
|
|
|
|
} finally {
|
|
|
|
Log.w(TAG, "Shutting down pipe...");
|
2021-07-09 14:01:24 +00:00
|
|
|
shutdown();
|
2018-10-15 20:27:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Log.i(TAG, "Looping...");
|
|
|
|
}
|
2021-02-02 21:42:47 +00:00
|
|
|
|
|
|
|
Log.w(TAG, "Terminated! (" + this.hashCode() + ")");
|
2018-10-15 20:27:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@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.OTHER);
|
|
|
|
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);
|
2018-10-28 06:32:28 +00:00
|
|
|
builder.setSmallIcon(R.drawable.ic_signal_background_connection);
|
2018-10-15 20:27:21 +00:00
|
|
|
startForeground(FOREGROUND_ID, builder.build());
|
|
|
|
|
|
|
|
return Service.START_STICKY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|