diff --git a/res/values/strings.xml b/res/values/strings.xml index a0b805b1dd..d9e13ca529 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -311,6 +311,9 @@ Now you can share a profile photo and name with friends on Signal Signal profiles are here + + Retrieving a message... + Permanent Signal communication failure! Signal was unable to register with Google Play Services. Signal messages and calls have been disabled, please try re-registering in Settings > Advanced. diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 7561771a55..68a515a827 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.jobmanager.dependencies.DependencyInjector; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.GcmRefreshJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; +import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.Log; @@ -50,7 +51,6 @@ import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.voiceengine.WebRtcAudioManager; @@ -91,6 +91,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc @Override public void onCreate() { super.onCreate(); + Log.i(TAG, "onCreate()"); initializeRandomNumberFix(); initializeLogging(); initializeCrashHandling(); @@ -102,6 +103,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc initializePeriodicTasks(); initializeCircumvention(); initializeWebRtc(); + initializePendingMessages(); NotificationChannels.create(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); } @@ -259,4 +261,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc ApplicationContext.getInstance(this).getJobManager().add(new MultiDeviceContactUpdateJob(this, true)); } } + + private void initializePendingMessages() { + if (TextSecurePreferences.getNeedsMessagePull(this)) { + Log.i(TAG, "Scheduling a message fetch."); + ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob()); + TextSecurePreferences.setNeedsMessagePull(this, false); + } + } } diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index a4faa115e9..d6dcbac99e 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.dependencies; import android.content.Context; + +import org.thoughtcrime.securesms.gcm.GcmBroadcastReceiver; import org.thoughtcrime.securesms.logging.Log; import org.greenrobot.eventbus.EventBus; @@ -80,7 +82,8 @@ import dagger.Provides; MultiDeviceProfileKeyUpdateJob.class, SendReadReceiptJob.class, MultiDeviceReadReceiptUpdateJob.class, - AppProtectionPreferenceFragment.class}) + AppProtectionPreferenceFragment.class, + GcmBroadcastReceiver.class}) public class SignalCommunicationModule { private static final String TAG = SignalCommunicationModule.class.getSimpleName(); diff --git a/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java b/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java index ef17a35c7e..1e51f0a317 100644 --- a/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java +++ b/src/org/thoughtcrime/securesms/gcm/GcmBroadcastReceiver.java @@ -2,8 +2,13 @@ package org.thoughtcrime.securesms.gcm; import android.content.Context; import android.content.Intent; +import android.os.PowerManager; import android.support.v4.content.WakefulBroadcastReceiver; import android.text.TextUtils; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.jobmanager.requirements.NetworkRequirement; import org.thoughtcrime.securesms.logging.Log; import com.google.android.gms.gcm.GoogleCloudMessaging; @@ -11,14 +16,27 @@ import com.google.android.gms.gcm.GoogleCloudMessaging; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.jobs.PushContentReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; +import org.thoughtcrime.securesms.service.GenericForegroundService; +import org.thoughtcrime.securesms.util.PowerManagerCompat; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.internal.util.Util; -public class GcmBroadcastReceiver extends WakefulBroadcastReceiver { +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.inject.Inject; + +public class GcmBroadcastReceiver extends WakefulBroadcastReceiver implements InjectableType { private static final String TAG = GcmBroadcastReceiver.class.getSimpleName(); + @Inject SignalServiceMessageReceiver messageReceiver; + @Override public void onReceive(Context context, Intent intent) { + ApplicationContext.getInstance(context).injectDependencies(this); + GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context); String messageType = gcm.getMessageType(intent); @@ -44,8 +62,63 @@ public class GcmBroadcastReceiver extends WakefulBroadcastReceiver { } private void handleReceivedNotification(Context context) { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new PushNotificationReceiveJob(context)); + TextSecurePreferences.setNeedsMessagePull(context, true); + + long startTime = System.currentTimeMillis(); + PendingResult callback = goAsync(); + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + boolean doze = PowerManagerCompat.isDeviceIdleMode(powerManager); + boolean network = new NetworkRequirement(context).isPresent(); + + final Object foregroundLock = new Object(); + final AtomicBoolean foregroundRunning = new AtomicBoolean(false); + final AtomicBoolean taskCompleted = new AtomicBoolean(false); + + if (doze || !network) { + Log.i(TAG, "Starting a foreground task because we may be operating in a constrained environment. Doze: " + doze + " Network: " + network); + GenericForegroundService.startForegroundTask(context, context.getString(R.string.GcmBroadcastReceiver_retrieving_a_message)); + foregroundRunning.set(true); + callback.finish(); + } + + new Thread("GcmMessageProcessing") { + @Override + public void run() { + try { + new PushNotificationReceiveJob(context).pullAndProcessMessages(messageReceiver, TAG, startTime); + } catch (IOException e) { + Log.i(TAG, "Failed to retrieve the envelope. Scheduling on JobManager.", e); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new PushNotificationReceiveJob(context)); + } finally { + synchronized (foregroundLock) { + if (foregroundRunning.getAndSet(false)) { + GenericForegroundService.stopForegroundTask(context); + } else { + callback.finish(); + } + taskCompleted.set(true); + } + Log.i(TAG, "Processing complete."); + } + } + }.start(); + + if (!foregroundRunning.get()) { + new Thread("GcmForegroundServiceTimer") { + @Override + public void run() { + Util.sleep(5000); + synchronized (foregroundLock) { + if (!taskCompleted.get() && !foregroundRunning.getAndSet(true)) { + Log.i(TAG, "Starting a foreground task because the job is running long."); + GenericForegroundService.startForegroundTask(context, context.getString(R.string.GcmBroadcastReceiver_retrieving_a_message)); + callback.finish(); + } + } + } + }.start(); + } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java index 4787d74c60..5e99f976d4 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java @@ -54,7 +54,7 @@ public class PushContentReceiveJob extends PushReceivedJob { String sessionKey = TextSecurePreferences.getSignalingKey(context); SignalServiceEnvelope envelope = new SignalServiceEnvelope(data, sessionKey); - handle(envelope); + processEnvelope(envelope); } catch (IOException | InvalidVersionException e) { Log.w(TAG, e); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 9487c8cd8a..11d4606bd0 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -119,6 +119,10 @@ public class PushDecryptJob extends ContextJob { super(null, null); } + public PushDecryptJob(Context context) { + this(context, -1); + } + public PushDecryptJob(Context context, long pushMessageId) { this(context, pushMessageId, -1); } @@ -146,32 +150,20 @@ public class PushDecryptJob extends ContextJob { @Override public void onRun() throws NoSuchMessageException { - if (!IdentityKeyUtil.hasIdentityKey(context)) { - Log.w(TAG, "Skipping job, waiting for migration..."); - return; + synchronized (PushReceivedJob.RECEIVE_LOCK) { + if (needsMigration()) { + Log.w(TAG, "Skipping, waiting for migration..."); + postMigrationNotification(); + return; + } + + PushDatabase database = DatabaseFactory.getPushDatabase(context); + SignalServiceEnvelope envelope = database.get(messageId); + Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent(); + + handleMessage(envelope, optionalSmsMessageId); + database.delete(messageId); } - - if (TextSecurePreferences.getNeedsSqlCipherMigration(context)) { - Log.w(TAG, "Skipping job, waiting for sqlcipher migration..."); - NotificationManagerCompat.from(context).notify(494949, - new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context)) - .setSmallIcon(R.drawable.icon_notification) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message)) - .setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages)) - .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0)) - .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) - .build()); - return; - } - - PushDatabase database = DatabaseFactory.getPushDatabase(context); - SignalServiceEnvelope envelope = database.get(messageId); - Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent(); - - handleMessage(envelope, optionalSmsMessageId); - database.delete(messageId); } @Override @@ -184,7 +176,38 @@ public class PushDecryptJob extends ContextJob { } - private void handleMessage(SignalServiceEnvelope envelope, Optional smsMessageId) { + public void processMessage(@NonNull SignalServiceEnvelope envelope) { + synchronized (PushReceivedJob.RECEIVE_LOCK) { + if (needsMigration()) { + Log.w(TAG, "Skipping and storing envelope, waiting for migration..."); + DatabaseFactory.getPushDatabase(context).insert(envelope); + postMigrationNotification(); + return; + } + + handleMessage(envelope, Optional.absent()); + } + } + + private boolean needsMigration() { + return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context); + } + + private void postMigrationNotification() { + NotificationManagerCompat.from(context).notify(494949, + new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context)) + .setSmallIcon(R.drawable.icon_notification) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message)) + .setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages)) + .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0)) + .setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE) + .build()); + + } + + private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional smsMessageId) { try { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); diff --git a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java index f552ec6dc4..da7014550b 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java @@ -4,10 +4,13 @@ import android.content.Context; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.jobmanager.SafeData; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -51,14 +54,19 @@ public class PushNotificationReceiveJob extends PushReceivedJob implements Injec @Override public void onRun() throws IOException { - receiver.retrieveMessages(new SignalServiceMessageReceiver.MessageReceivedCallback() { - @Override - public void onMessage(SignalServiceEnvelope envelope) { - handle(envelope); - } - }); + pullAndProcessMessages(receiver, TAG, System.currentTimeMillis()); } + public void pullAndProcessMessages(SignalServiceMessageReceiver receiver, String tag, long startTime) throws IOException { + synchronized (PushReceivedJob.RECEIVE_LOCK) { + receiver.retrieveMessages(envelope -> { + Log.i(tag, "Retrieved an envelope." + timeSuffix(startTime)); + processEnvelope(envelope); + Log.i(tag, "Successfully processed an envelope." + timeSuffix(startTime)); + }); + TextSecurePreferences.setNeedsMessagePull(context, false); + } + } @Override public boolean onShouldRetry(Exception e) { Log.w(TAG, e); @@ -70,4 +78,8 @@ public class PushNotificationReceiveJob extends PushReceivedJob implements Injec Log.w(TAG, "***** Failed to download pending message!"); // MessageNotifier.notifyMessagesPending(getContext()); } + + private static String timeSuffix(long startTime) { + return " (" + (System.currentTimeMillis() - startTime) + " ms elapsed)"; + } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java index 924f93f12f..3bb7366780 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java @@ -8,7 +8,6 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; @@ -18,33 +17,34 @@ public abstract class PushReceivedJob extends ContextJob { private static final String TAG = PushReceivedJob.class.getSimpleName(); + public static final Object RECEIVE_LOCK = new Object(); + protected PushReceivedJob(Context context, JobParameters parameters) { super(context, parameters); } - public void handle(SignalServiceEnvelope envelope) { - Address source = Address.fromExternal(context, envelope.getSource()); - Recipient recipient = Recipient.from(context, source, false); + public void processEnvelope(@NonNull SignalServiceEnvelope envelope) { + synchronized (RECEIVE_LOCK) { + Address source = Address.fromExternal(context, envelope.getSource()); + Recipient recipient = Recipient.from(context, source, false); - if (!isActiveNumber(recipient)) { - DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED); - ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(context, recipient, false)); - } + if (!isActiveNumber(recipient)) { + DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED); + ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(context, recipient, false)); + } - if (envelope.isReceipt()) { - handleReceipt(envelope); - } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage()) { - handleMessage(envelope); - } else { - Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); + if (envelope.isReceipt()) { + handleReceipt(envelope); + } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage()) { + handleMessage(envelope); + } else { + Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); + } } } private void handleMessage(SignalServiceEnvelope envelope) { - long messageId = DatabaseFactory.getPushDatabase(context).insert(envelope); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new PushDecryptJob(context, messageId)); + new PushDecryptJob(context).processMessage(envelope); } private void handleReceipt(SignalServiceEnvelope envelope) { @@ -56,6 +56,4 @@ public abstract class PushReceivedJob extends ContextJob { private boolean isActiveNumber(@NonNull Recipient recipient) { return recipient.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED; } - - } diff --git a/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java b/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java index 8968f863b9..4b5735deee 100644 --- a/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java +++ b/src/org/thoughtcrime/securesms/service/MessageRetrievalService.java @@ -6,6 +6,7 @@ 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; @@ -211,10 +212,7 @@ public class MessageRetrievalService extends Service implements InjectableType, localPipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES, envelope -> { Log.i(TAG, "Retrieved envelope! " + envelope.getSource()); - - PushContentReceiveJob receiveJob = new PushContentReceiveJob(MessageRetrievalService.this); - receiveJob.handle(envelope); - + new PushContentReceiveJob(getApplicationContext()).processEnvelope(envelope); decrementPushReceived(); }); } catch (TimeoutException e) { diff --git a/src/org/thoughtcrime/securesms/util/PowerManagerCompat.java b/src/org/thoughtcrime/securesms/util/PowerManagerCompat.java new file mode 100644 index 0000000000..f4b7f60868 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/PowerManagerCompat.java @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms.util; + +import android.os.Build; +import android.os.PowerManager; +import android.support.annotation.NonNull; + +public class PowerManagerCompat { + + public static boolean isDeviceIdleMode(@NonNull PowerManager powerManager) { + if (Build.VERSION.SDK_INT >= 23) { + return powerManager.isDeviceIdleMode(); + } + return false; + } +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index fadb89d724..7e1dbf9bbc 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -162,6 +162,8 @@ public class TextSecurePreferences { private static final String NOTIFICATION_CHANNEL_VERSION = "pref_notification_channel_version"; private static final String NOTIFICATION_MESSAGES_CHANNEL_VERSION = "pref_notification_messages_channel_version"; + private static final String NEEDS_MESSAGE_PULL = "pref_needs_message_pull"; + public static boolean isScreenLockEnabled(@NonNull Context context) { return getBooleanPreference(context, SCREEN_LOCK, false); } @@ -983,6 +985,14 @@ public class TextSecurePreferences { setIntegerPrefrence(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version); } + public static boolean getNeedsMessagePull(Context context) { + return getBooleanPreference(context, NEEDS_MESSAGE_PULL, false); + } + + public static void setNeedsMessagePull(Context context, boolean needsMessagePull) { + setBooleanPreference(context, NEEDS_MESSAGE_PULL, needsMessagePull); + } + public static void setBooleanPreference(Context context, String key, boolean value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); }