diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 15cab4f9f..0297bbd1e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -430,7 +430,7 @@ - + { - KeyCachingService.registerPassphraseActivityStarted(this); - - if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStarted(this); - else ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob(this)); - }); + if (networkAccess.isCensored(this)) { + ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob(this)); + } } @Override protected void onPause() { Log.i(TAG, "onPause()"); super.onPause(); - isVisible = false; - - // Android P has a bug in foreground timings where starting a service in onPause() can still crash - Util.postToMain(() -> { - KeyCachingService.registerPassphraseActivityStopped(this); - if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStopped(this); - }); } @Override @@ -95,8 +81,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA @Override public void onMasterSecretCleared() { Log.i(TAG, "onMasterSecretCleared()"); - if (isVisible) routeApplicationState(true); - else finish(); + if (ApplicationContext.getInstance(this).isAppVisible()) routeApplicationState(true); + else finish(); } protected T initFragment(@IdRes int target, diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java index 9ce0d8547..ef44371e1 100644 --- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.service.MessageRetrievalService; import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -67,7 +66,6 @@ public class WebRtcCallActivity extends Activity { public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION"; private WebRtcCallScreen callScreen; - private SignalServiceNetworkAccess networkAccess; @Override public void onCreate(Bundle savedInstanceState) { @@ -89,12 +87,6 @@ public class WebRtcCallActivity extends Activity { public void onResume() { Log.i(TAG, "onResume()"); super.onResume(); - - // Android P has a bug in foreground timings where starting a service in onResume() can still crash - Util.postToMain(() -> { - if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStarted(this); - }); - initializeScreenshotSecurity(); EventBus.getDefault().register(this); } @@ -116,11 +108,6 @@ public class WebRtcCallActivity extends Activity { Log.i(TAG, "onPause"); super.onPause(); EventBus.getDefault().unregister(this); - - // Android P has a bug in foreground timings where starting a service in onPause() can still crash - Util.postToMain(() -> { - if (!networkAccess.isCensored(this)) MessageRetrievalService.registerActivityStopped(this); - }); } @Override @@ -152,8 +139,6 @@ public class WebRtcCallActivity extends Activity { callScreen.setCameraFlipButtonListener(new CameraFlipButtonListener()); callScreen.setSpeakerButtonListener(new SpeakerButtonListener()); callScreen.setBluetoothButtonListener(new BluetoothButtonListener()); - - networkAccess = new SignalServiceNetworkAccess(this); } private void handleSetMuteAudio(boolean enabled) { diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index d6dcbac99..704560409 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -38,7 +38,7 @@ import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; -import org.thoughtcrime.securesms.service.MessageRetrievalService; +import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; @@ -61,7 +61,7 @@ import dagger.Provides; PushMediaSendJob.class, AttachmentDownloadJob.class, RefreshPreKeysJob.class, - MessageRetrievalService.class, + IncomingMessageObserver.class, PushNotificationReceiveJob.class, MultiDeviceContactUpdateJob.class, MultiDeviceGroupUpdateJob.class, @@ -118,10 +118,10 @@ public class SignalCommunicationModule { new DynamicCredentialsProvider(context), new SignalProtocolStoreImpl(context), BuildConfig.USER_AGENT, - Optional.fromNullable(MessageRetrievalService.getPipe()), + Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.of(new SecurityEventListener(context))); } else { - this.messageSender.setMessagePipe(MessageRetrievalService.getPipe()); + this.messageSender.setMessagePipe(IncomingMessageObserver.getPipe()); } return this.messageSender; @@ -142,6 +142,11 @@ public class SignalCommunicationModule { return this.messageReceiver; } + @Provides + synchronized SignalServiceNetworkAccess provideSignalServiceNetworkAccess() { + return networkAccess; + } + private static class DynamicCredentialsProvider implements CredentialsProvider { private final Context context; diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index baf554299..37a878986 100644 --- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.service.MessageRetrievalService; +import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.Util; @@ -107,7 +107,7 @@ public class RetrieveProfileJob extends ContextJob implements InjectableType { } private SignalServiceProfile retrieveProfile(@NonNull String number) throws IOException { - SignalServiceMessagePipe pipe = MessageRetrievalService.getPipe(); + SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe(); if (pipe != null) { try { diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 7316e011a..e6cc3f0b1 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -51,8 +51,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.service.MessageRetrievalService; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -164,7 +164,7 @@ public class MessageNotifier { if (notification.getId() != SUMMARY_NOTIFICATION_ID && notification.getId() != CallNotificationBuilder.WEBRTC_NOTIFICATION && notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && - notification.getId() != MessageRetrievalService.FOREGROUND_ID && + notification.getId() != IncomingMessageObserver.FOREGROUND_ID && notification.getId() != PENDING_MESSAGES_ID) { for (NotificationItem item : notificationState.getNotifications()) { diff --git a/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java b/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java new file mode 100644 index 000000000..1e1147904 --- /dev/null +++ b/src/org/thoughtcrime/securesms/service/IncomingMessageObserver.java @@ -0,0 +1,202 @@ +package org.thoughtcrime.securesms.service; + +import android.app.Service; +import android.arch.lifecycle.DefaultLifecycleObserver; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.ProcessLifecycleOwner; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; + +import org.thoughtcrime.securesms.logging.Log; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; +import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirementProvider; +import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; +import org.thoughtcrime.securesms.jobs.PushContentReceiveJob; +import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.InvalidVersionException; +import org.whispersystems.signalservice.api.SignalServiceMessagePipe; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +public class IncomingMessageObserver implements InjectableType, RequirementListener { + + private static final String TAG = IncomingMessageObserver.class.getSimpleName(); + + public static final int FOREGROUND_ID = 313399; + private static final long REQUEST_TIMEOUT_MINUTES = 1; + + private static SignalServiceMessagePipe pipe = null; + + private final Context context; + private final NetworkRequirement networkRequirement; + + private boolean appVisible; + + @Inject SignalServiceMessageReceiver receiver; + @Inject SignalServiceNetworkAccess networkAccess; + + public IncomingMessageObserver(@NonNull Context context) { + ApplicationContext.getInstance(context).injectDependencies(this); + + this.context = context; + this.networkRequirement = new NetworkRequirement(context); + + new NetworkRequirementProvider(context).setListener(this); + new MessageRetrievalThread().start(); + + if (TextSecurePreferences.isGcmDisabled(context)) { + ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class)); + } + + ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() { + @Override + public void onStart(@NonNull LifecycleOwner owner) { + onAppForegrounded(); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + onAppBackgrounded(); + } + }); + } + + @Override + public void onRequirementStatusChanged() { + synchronized (this) { + notifyAll(); + } + } + + private synchronized void onAppForegrounded() { + appVisible = true; + notifyAll(); + } + + private synchronized void onAppBackgrounded() { + appVisible = false; + notifyAll(); + } + + private synchronized boolean isConnectionNecessary() { + boolean isGcmDisabled = TextSecurePreferences.isGcmDisabled(context); + + Log.d(TAG, String.format("Network requirement: %s, app visible: %s, gcm disabled: %b", + networkRequirement.isPresent(), appVisible, isGcmDisabled)); + + return TextSecurePreferences.isPushRegistered(context) && + TextSecurePreferences.isWebsocketRegistered(context) && + (appVisible || isGcmDisabled) && + networkRequirement.isPresent() && + !networkAccess.isCensored(context); + } + + private synchronized void waitForConnectionNecessary() { + try { + while (!isConnectionNecessary()) wait(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + private void shutdown(SignalServiceMessagePipe pipe) { + try { + pipe.shutdown(); + } catch (Throwable t) { + Log.w(TAG, t); + } + } + + public static @Nullable SignalServiceMessagePipe getPipe() { + return pipe; + } + + private class MessageRetrievalThread extends Thread implements Thread.UncaughtExceptionHandler { + + MessageRetrievalThread() { + super("MessageRetrievalService"); + setUncaughtExceptionHandler(this); + } + + @Override + public void run() { + while (true) { + Log.i(TAG, "Waiting for websocket state change...."); + waitForConnectionNecessary(); + + Log.i(TAG, "Making websocket connection...."); + pipe = receiver.createMessagePipe(); + + SignalServiceMessagePipe localPipe = pipe; + + try { + while (isConnectionNecessary()) { + try { + Log.i(TAG, "Reading message..."); + localPipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES, + envelope -> { + Log.i(TAG, "Retrieved envelope! " + envelope.getSource()); + new PushContentReceiveJob(context).processEnvelope(envelope); + }); + } catch (TimeoutException e) { + Log.w(TAG, "Application level read timeout..."); + } catch (InvalidVersionException e) { + Log.w(TAG, e); + } + } + } catch (Throwable e) { + Log.w(TAG, e); + } finally { + Log.w(TAG, "Shutting down pipe..."); + shutdown(localPipe); + } + + Log.i(TAG, "Looping..."); + } + } + + @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); + builder.setSmallIcon(R.drawable.ic_signal_grey_24dp); + startForeground(FOREGROUND_ID, builder.build()); + + return Service.START_STICKY; + } + } +} diff --git a/src/org/thoughtcrime/securesms/service/KeyCachingService.java b/src/org/thoughtcrime/securesms/service/KeyCachingService.java index c4134e029..152cbe7f3 100644 --- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -29,6 +29,7 @@ import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import org.thoughtcrime.securesms.logging.Log; @@ -68,14 +69,10 @@ public class KeyCachingService extends Service { private static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT"; public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY"; public static final String DISABLE_ACTION = "org.thoughtcrime.securesms.service.action.DISABLE"; - public static final String ACTIVITY_START_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_START_EVENT"; - public static final String ACTIVITY_STOP_EVENT = "org.thoughtcrime.securesms.service.action.ACTIVITY_STOP_EVENT"; public static final String LOCALE_CHANGE_EVENT = "org.thoughtcrime.securesms.service.action.LOCALE_CHANGE_EVENT"; private DynamicLanguage dynamicLanguage = new DynamicLanguage(); - private PendingIntent pending; - private int activitiesRunning = 0; private final IBinder binder = new KeySetBinder(); private static MasterSecret masterSecret; @@ -98,6 +95,14 @@ public class KeyCachingService extends Service { return masterSecret; } + public static void onAppForegrounded(@NonNull Context context) { + ServiceUtil.getAlarmManager(context).cancel(buildExpirationPendingIntent(context)); + } + + public static void onAppBackgrounded(@NonNull Context context) { + startTimeoutIfAppropriate(context); + } + @SuppressLint("StaticFieldLeak") public void setMasterSecret(final MasterSecret masterSecret) { synchronized (KeyCachingService.class) { @@ -105,7 +110,7 @@ public class KeyCachingService extends Service { foregroundService(); broadcastNewSecret(); - startTimeoutIfAppropriate(); + startTimeoutIfAppropriate(this); new AsyncTask() { @Override @@ -127,8 +132,6 @@ public class KeyCachingService extends Service { if (intent.getAction() != null) { switch (intent.getAction()) { case CLEAR_KEY_ACTION: handleClearKey(); break; - case ACTIVITY_START_EVENT: handleActivityStarted(); break; - case ACTIVITY_STOP_EVENT: handleActivityStopped(); break; case PASSPHRASE_EXPIRED_EVENT: handleClearKey(); break; case DISABLE_ACTION: handleDisableService(); break; case LOCALE_CHANGE_EVENT: handleLocaleChanged(); break; @@ -143,8 +146,6 @@ public class KeyCachingService extends Service { public void onCreate() { Log.i(TAG, "onCreate()"); super.onCreate(); - this.pending = PendingIntent.getService(this, 0, new Intent(PASSPHRASE_EXPIRED_EVENT, null, - this, KeyCachingService.class), 0); if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) { try { @@ -174,21 +175,6 @@ public class KeyCachingService extends Service { startActivity(intent); } - private void handleActivityStarted() { - Log.d(TAG, "Incrementing activity count..."); - - AlarmManager alarmManager = ServiceUtil.getAlarmManager(this); - alarmManager.cancel(pending); - activitiesRunning++; - } - - private void handleActivityStopped() { - Log.d(TAG, "Decrementing activity count..."); - - activitiesRunning--; - startTimeoutIfAppropriate(); - } - @SuppressLint("StaticFieldLeak") private void handleClearKey() { Log.i(TAG, "handleClearKey()"); @@ -233,27 +219,29 @@ public class KeyCachingService extends Service { foregroundService(); } - private void startTimeoutIfAppropriate() { - boolean timeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(this); - long screenTimeout = TextSecurePreferences.getScreenLockTimeout(this); + private static void startTimeoutIfAppropriate(@NonNull Context context) { + boolean timeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(context); + long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context); - if ((activitiesRunning == 0) && (KeyCachingService.masterSecret != null) && - (timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(this)) || - (screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(this))) + if ((KeyCachingService.masterSecret != null) && + (timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context)) || + (screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context))) { - long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(this); - long screenLockTimeoutSeconds = TextSecurePreferences.getScreenLockTimeout(this); + long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context); + long screenLockTimeoutSeconds = TextSecurePreferences.getScreenLockTimeout(context); long timeoutMillis; - if (!TextSecurePreferences.isPasswordDisabled(this)) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes); - else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds); + if (!TextSecurePreferences.isPasswordDisabled(context)) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes); + else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds); Log.i(TAG, "Starting timeout: " + timeoutMillis); - AlarmManager alarmManager = ServiceUtil.getAlarmManager(this); - alarmManager.cancel(pending); - alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, pending); + AlarmManager alarmManager = ServiceUtil.getAlarmManager(context); + PendingIntent expirationIntent = buildExpirationPendingIntent(context); + + alarmManager.cancel(expirationIntent); + alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, expirationIntent); } } @@ -338,6 +326,11 @@ public class KeyCachingService extends Service { return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); } + private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) { + Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class); + return PendingIntent.getService(context, 0, expirationIntent, 0); + } + @Override public IBinder onBind(Intent arg0) { return binder; @@ -348,16 +341,4 @@ public class KeyCachingService extends Service { return KeyCachingService.this; } } - - public static void registerPassphraseActivityStarted(Context activity) { - Intent intent = new Intent(activity, KeyCachingService.class); - intent.setAction(KeyCachingService.ACTIVITY_START_EVENT); - activity.startService(intent); - } - - public static void registerPassphraseActivityStopped(Context activity) { - Intent intent = new Intent(activity, KeyCachingService.class); - intent.setAction(KeyCachingService.ACTIVITY_STOP_EVENT); - activity.startService(intent); - } } diff --git a/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java b/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java deleted file mode 100644 index 4b5735dee..000000000 --- a/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java +++ /dev/null @@ -1,247 +0,0 @@ -package org.thoughtcrime.securesms.service; - -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; - -import org.thoughtcrime.securesms.logging.Log; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.gcm.GcmBroadcastReceiver; -import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; -import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirementProvider; -import org.thoughtcrime.securesms.jobmanager.requirements.RequirementListener; -import org.thoughtcrime.securesms.jobs.PushContentReceiveJob; -import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.signalservice.api.SignalServiceMessagePipe; -import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; - -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.inject.Inject; - -public class MessageRetrievalService extends Service implements InjectableType, RequirementListener { - - private static final String TAG = MessageRetrievalService.class.getSimpleName(); - - public static final String ACTION_ACTIVITY_STARTED = "ACTIVITY_STARTED"; - public static final String ACTION_ACTIVITY_FINISHED = "ACTIVITY_FINISHED"; - public static final String ACTION_PUSH_RECEIVED = "PUSH_RECEIVED"; - public static final String ACTION_INITIALIZE = "INITIALIZE"; - public static final int FOREGROUND_ID = 313399; - - private static final long REQUEST_TIMEOUT_MINUTES = 1; - - private NetworkRequirement networkRequirement; - private NetworkRequirementProvider networkRequirementProvider; - - @Inject - public SignalServiceMessageReceiver receiver; - - private int activeActivities = 0; - private List pushPending = new LinkedList<>(); - private MessageRetrievalThread retrievalThread = null; - - public static SignalServiceMessagePipe pipe = null; - - @Override - public void onCreate() { - super.onCreate(); - ApplicationContext.getInstance(this).injectDependencies(this); - - networkRequirement = new NetworkRequirement(this); - networkRequirementProvider = new NetworkRequirementProvider(this); - - networkRequirementProvider.setListener(this); - - retrievalThread = new MessageRetrievalThread(); - retrievalThread.start(); - - setForegroundIfNecessary(); - } - - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) return START_STICKY; - - if (ACTION_ACTIVITY_STARTED.equals(intent.getAction())) incrementActive(); - else if (ACTION_ACTIVITY_FINISHED.equals(intent.getAction())) decrementActive(); - else if (ACTION_PUSH_RECEIVED.equals(intent.getAction())) incrementPushReceived(intent); - - return START_STICKY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (retrievalThread != null) { - retrievalThread.stopThread(); - } - - sendBroadcast(new Intent("org.thoughtcrime.securesms.RESTART")); - } - - @Override - public void onRequirementStatusChanged() { - synchronized (this) { - notifyAll(); - } - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private void setForegroundIfNecessary() { - if (TextSecurePreferences.isGcmDisabled(this)) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationChannels.OTHER); - builder.setContentTitle(getString(R.string.MessageRetrievalService_signal)); - builder.setContentText(getString(R.string.MessageRetrievalService_background_connection_enabled)); - builder.setPriority(NotificationCompat.PRIORITY_MIN); - builder.setWhen(0); - builder.setSmallIcon(R.drawable.ic_signal_grey_24dp); - startForeground(FOREGROUND_ID, builder.build()); - } - } - - private synchronized void incrementActive() { - activeActivities++; - Log.d(TAG, "Active Count: " + activeActivities); - notifyAll(); - } - - private synchronized void decrementActive() { - activeActivities--; - Log.d(TAG, "Active Count: " + activeActivities); - notifyAll(); - } - - private synchronized void incrementPushReceived(Intent intent) { - pushPending.add(intent); - notifyAll(); - } - - private synchronized void decrementPushReceived() { - if (!pushPending.isEmpty()) { - Intent intent = pushPending.remove(0); - GcmBroadcastReceiver.completeWakefulIntent(intent); - notifyAll(); - } - } - - private synchronized boolean isConnectionNecessary() { - boolean isGcmDisabled = TextSecurePreferences.isGcmDisabled(this); - - Log.d(TAG, String.format("Network requirement: %s, active activities: %s, push pending: %s, gcm disabled: %b", - networkRequirement.isPresent(), activeActivities, pushPending.size(), isGcmDisabled)); - - return TextSecurePreferences.isPushRegistered(this) && - TextSecurePreferences.isWebsocketRegistered(this) && - (activeActivities > 0 || !pushPending.isEmpty() || isGcmDisabled) && - networkRequirement.isPresent(); - } - - private synchronized void waitForConnectionNecessary() { - try { - while (!isConnectionNecessary()) wait(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - } - - private void shutdown(SignalServiceMessagePipe pipe) { - try { - pipe.shutdown(); - } catch (Throwable t) { - Log.w(TAG, t); - } - } - - public static void registerActivityStarted(Context activity) { - Intent intent = new Intent(activity, MessageRetrievalService.class); - intent.setAction(MessageRetrievalService.ACTION_ACTIVITY_STARTED); - activity.startService(intent); - } - - public static void registerActivityStopped(Context activity) { - Intent intent = new Intent(activity, MessageRetrievalService.class); - intent.setAction(MessageRetrievalService.ACTION_ACTIVITY_FINISHED); - activity.startService(intent); - } - - public static @Nullable SignalServiceMessagePipe getPipe() { - return pipe; - } - - private class MessageRetrievalThread extends Thread implements Thread.UncaughtExceptionHandler { - - private AtomicBoolean stopThread = new AtomicBoolean(false); - - MessageRetrievalThread() { - super("MessageRetrievalService"); - setUncaughtExceptionHandler(this); - } - - @Override - public void run() { - while (!stopThread.get()) { - Log.i(TAG, "Waiting for websocket state change...."); - waitForConnectionNecessary(); - - Log.i(TAG, "Making websocket connection...."); - pipe = receiver.createMessagePipe(); - - SignalServiceMessagePipe localPipe = pipe; - - try { - while (isConnectionNecessary() && !stopThread.get()) { - try { - Log.i(TAG, "Reading message..."); - localPipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES, - envelope -> { - Log.i(TAG, "Retrieved envelope! " + envelope.getSource()); - new PushContentReceiveJob(getApplicationContext()).processEnvelope(envelope); - decrementPushReceived(); - }); - } catch (TimeoutException e) { - Log.w(TAG, "Application level read timeout..."); - } catch (InvalidVersionException e) { - Log.w(TAG, e); - } - } - } catch (Throwable e) { - Log.w(TAG, e); - } finally { - Log.w(TAG, "Shutting down pipe..."); - shutdown(localPipe); - } - - Log.i(TAG, "Looping..."); - } - - Log.i(TAG, "Exiting..."); - } - - private void stopThread() { - stopThread.set(true); - } - - @Override - public void uncaughtException(Thread t, Throwable e) { - Log.w(TAG, "*** Uncaught exception!"); - Log.w(TAG, e); - } - } -} diff --git a/src/org/thoughtcrime/securesms/service/PersistentConnectionBootListener.java b/src/org/thoughtcrime/securesms/service/PersistentConnectionBootListener.java index 4040696ec..2a2a4a71c 100644 --- a/src/org/thoughtcrime/securesms/service/PersistentConnectionBootListener.java +++ b/src/org/thoughtcrime/securesms/service/PersistentConnectionBootListener.java @@ -4,21 +4,19 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.util.Log; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class PersistentConnectionBootListener extends BroadcastReceiver { + + private static final String TAG = PersistentConnectionBootListener.class.getSimpleName(); + @Override public void onReceive(Context context, Intent intent) { if (intent != null && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - if (TextSecurePreferences.isGcmDisabled(context)) { - Intent serviceIntent = new Intent(context, MessageRetrievalService.class); - serviceIntent.setAction(MessageRetrievalService.ACTION_INITIALIZE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(serviceIntent); - else context.startService(serviceIntent); - } + Log.i(TAG, "Received boot event. Application should be started, allowing non-GCM devices to start a foreground service."); } } }