diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b70c498adf..d3321ca127 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -683,6 +683,12 @@ + + Reply Signal Message Unsecured SMS - Pending Signal messages - You have pending Signal messages, tap to open and retrieve + You may have new messages + Open Signal to check for recent notifications. %1$s %2$s Contact diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 143d05571d..5266d98935 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule; +import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.jobmanager.DependencyInjector; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; @@ -135,6 +136,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc Log.i(TAG, "App is now visible."); executePendingContactSync(); KeyCachingService.onAppForegrounded(this); + MessageNotifier.cancelMessagesPending(this); } @Override @@ -334,7 +336,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc private void initializePendingMessages() { if (TextSecurePreferences.getNeedsMessagePull(this)) { Log.i(TAG, "Scheduling a message fetch."); - ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob(this)); + if (Build.VERSION.SDK_INT >= 26) { + FcmJobService.schedule(this); + } else { + ApplicationContext.getInstance(this).getJobManager().add(new PushNotificationReceiveJob(this)); + } TextSecurePreferences.setNeedsMessagePull(this, false); } } diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index ffa43b236b..cfa41d2eed 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.CreateProfileActivity; import org.thoughtcrime.securesms.DeviceListFragment; import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; +import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.gcm.FcmService; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.AttachmentUploadJob; @@ -108,7 +109,8 @@ import dagger.Provides; StickerPackDownloadJob.class, MultiDeviceStickerPackOperationJob.class, MultiDeviceStickerPackSyncJob.class, - LinkPreviewRepository.class}) + LinkPreviewRepository.class, + FcmJobService.class}) public class SignalCommunicationModule { private static final String TAG = SignalCommunicationModule.class.getSimpleName(); diff --git a/src/org/thoughtcrime/securesms/gcm/FcmJobService.java b/src/org/thoughtcrime/securesms/gcm/FcmJobService.java new file mode 100644 index 0000000000..c0154f77d1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/gcm/FcmJobService.java @@ -0,0 +1,77 @@ +package org.thoughtcrime.securesms.gcm; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; + +import java.io.IOException; + +import javax.inject.Inject; + +/** + * Pulls down messages. Used when we fail to pull down messages in {@link FcmService}. + */ +@RequiresApi(26) +public class FcmJobService extends JobService implements InjectableType { + + private static final String TAG = FcmJobService.class.getSimpleName(); + + private static final int ID = 1337; + + @Inject SignalServiceMessageReceiver messageReceiver; + + @RequiresApi(26) + public static void schedule(@NonNull Context context) { + JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(ID, new ComponentName(context, FcmJobService.class)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setBackoffCriteria(0, JobInfo.BACKOFF_POLICY_LINEAR) + .setPersisted(true); + + ServiceUtil.getJobScheduler(context).schedule(jobInfoBuilder.build()); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.d(TAG, "onStartJob()"); + ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this); + + if (ApplicationContext.getInstance(getApplicationContext()).isAppVisible()) { + Log.i(TAG, "App is foregrounded. No need to run."); + return false; + } + + SignalExecutors.UNBOUNDED.execute(() -> { + try { + new PushNotificationReceiveJob(getApplicationContext()).pullAndProcessMessages(messageReceiver, TAG, System.currentTimeMillis()); + jobFinished(params, false); + } catch (IOException e) { + Log.w(TAG, "Failed to pull. Notifying and scheduling a retry.", e); + MessageNotifier.notifyMessagesPending(getApplicationContext()); + jobFinished(params, true); + } + }); + + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + Log.d(TAG, "onStopJob()"); + return TextSecurePreferences.getNeedsMessagePull(getApplicationContext()); + } +} diff --git a/src/org/thoughtcrime/securesms/gcm/FcmService.java b/src/org/thoughtcrime/securesms/gcm/FcmService.java index 12a1f2f891..6f0fd1f5e3 100644 --- a/src/org/thoughtcrime/securesms/gcm/FcmService.java +++ b/src/org/thoughtcrime/securesms/gcm/FcmService.java @@ -1,33 +1,26 @@ package org.thoughtcrime.securesms.gcm; import android.content.Context; +import android.os.Build; import android.os.PowerManager; -import androidx.annotation.NonNull; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.util.PowerManagerCompat; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.WakeLockUtil; -import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; -import org.whispersystems.signalservice.internal.util.Util; import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -35,8 +28,8 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy private static final String TAG = FcmService.class.getSimpleName(); - private static final Executor MESSAGE_EXECUTOR = SignalExecutors.newCachedSingleThreadExecutor("FcmMessageProcessing"); - private static final String WAKE_LOCK_TAG = "FcmMessageProcessing"; + private static final String WAKE_LOCK_TAG = "FcmMessageProcessing"; + private static final long SOCKET_TIMEOUT = TimeUnit.SECONDS.toMillis(10); @Inject SignalServiceMessageReceiver messageReceiver; @@ -79,69 +72,27 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy boolean doze = PowerManagerCompat.isDeviceIdleMode(powerManager); boolean network = new NetworkConstraint.Factory(ApplicationContext.getInstance(context)).create().isMet(); - final Object foregroundLock = new Object(); - final AtomicBoolean foregroundRunning = new AtomicBoolean(false); - final AtomicBoolean taskCompleted = new AtomicBoolean(false); - final CountDownLatch latch = new CountDownLatch(1); - if (doze || !network) { - Log.i(TAG, "Starting a foreground task because we may be operating in a constrained environment. Doze: " + doze + " Network: " + network); - showForegroundNotification(context); - foregroundRunning.set(true); - latch.countDown(); - } - - MESSAGE_EXECUTOR.execute(() -> { - 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 { - latch.countDown(); - } - taskCompleted.set(true); - } - - decrementActiveGcmCount(); - Log.i(TAG, "Processing complete."); - } - }); - - if (!foregroundRunning.get()) { - new Thread("FcmForegroundServiceTimer") { - @Override - public void run() { - Util.sleep(7000); - synchronized (foregroundLock) { - if (!taskCompleted.get() && !foregroundRunning.getAndSet(true)) { - Log.i(TAG, "Starting a foreground task because the job is running long."); - showForegroundNotification(context); - latch.countDown(); - } - } - } - }.start(); + Log.w(TAG, "We may be operating in a constrained environment. Doze: " + doze + " Network: " + network); } try { - latch.await(); - } catch (InterruptedException e) { - Log.w(TAG, "Latch was interrupted.", e); + messageReceiver.setSoTimeoutMillis(SOCKET_TIMEOUT); + new PushNotificationReceiveJob(context).pullAndProcessMessages(messageReceiver, TAG, startTime); + } catch (IOException e) { + if (Build.VERSION.SDK_INT >= 26) { + Log.i(TAG, "Failed to retrieve the envelope. Scheduling on the system JobScheduler (API " + Build.VERSION.SDK_INT + ").", e); + FcmJobService.schedule(context); + } else { + Log.i(TAG, "Failed to retrieve the envelope. Scheduling on JobManager (API " + Build.VERSION.SDK_INT + ").", e); + ApplicationContext.getInstance(context) + .getJobManager() + .add(new PushNotificationReceiveJob(context)); + } } - } - private void showForegroundNotification(@NonNull Context context) { - GenericForegroundService.startForegroundTask(context, - context.getString(R.string.GcmBroadcastReceiver_retrieving_a_message), - NotificationChannels.OTHER, - R.drawable.ic_signal_downloading); + decrementActiveGcmCount(); + Log.i(TAG, "Processing complete."); } private static synchronized boolean incrementActiveGcmCount() { diff --git a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java index f421b5552b..757d596972 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushNotificationReceiveJob.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -61,6 +62,7 @@ public class PushNotificationReceiveJob extends PushReceivedJob implements Injec Log.i(tag, "Successfully processed an envelope." + timeSuffix(startTime)); }); TextSecurePreferences.setNeedsMessagePull(context, false); + MessageNotifier.cancelMessagesPending(context); } } @Override diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index f563d4cb1e..9825afd0f6 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -38,6 +38,7 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import android.text.TextUtils; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.ContactUtil; @@ -120,7 +121,7 @@ public class MessageNotifier { } public static void notifyMessagesPending(Context context) { - if (!TextSecurePreferences.isNotificationsEnabled(context)) { + if (!TextSecurePreferences.isNotificationsEnabled(context) || ApplicationContext.getInstance(context).isAppVisible()) { return; } @@ -128,6 +129,10 @@ public class MessageNotifier { ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build()); } + public static void cancelMessagesPending(Context context) { + ServiceUtil.getNotificationManager(context).cancel(PENDING_MESSAGES_ID); + } + public static void cancelDelayedNotifications() { executor.cancel(); } diff --git a/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java index eca8a36913..fb07b7e99c 100644 --- a/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java @@ -23,14 +23,16 @@ public class PendingMessageNotificationBuilder extends AbstractNotificationBuild setColor(context.getResources().getColor(R.color.textsecure_primary)); setCategory(NotificationCompat.CATEGORY_MESSAGE); - setContentTitle(context.getString(R.string.MessageNotifier_pending_signal_messages)); - setContentText(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages)); - setTicker(context.getString(R.string.MessageNotifier_you_have_pending_signal_messages)); + setContentTitle(context.getString(R.string.MessageNotifier_you_may_have_new_messages)); + setContentText(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications)); + setTicker(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications)); setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); setAutoCancel(true); setAlarms(null, RecipientDatabase.VibrateState.DEFAULT); + setOnlyAlertOnce(true); + if (!NotificationChannels.supported()) { setPriority(TextSecurePreferences.getNotificationPriority(context)); } diff --git a/src/org/thoughtcrime/securesms/util/ServiceUtil.java b/src/org/thoughtcrime/securesms/util/ServiceUtil.java index dd74ad1d33..a6cd2d1920 100644 --- a/src/org/thoughtcrime/securesms/util/ServiceUtil.java +++ b/src/org/thoughtcrime/securesms/util/ServiceUtil.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util; import android.app.Activity; import android.app.AlarmManager; import android.app.NotificationManager; +import android.app.job.JobScheduler; import android.content.Context; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -54,6 +55,11 @@ public class ServiceUtil { return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); } + @RequiresApi(26) + public static JobScheduler getJobScheduler(Context context) { + return (JobScheduler) context.getSystemService(JobScheduler.class); + } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) public static @Nullable SubscriptionManager getSubscriptionManager(@NonNull Context context) { return (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);