diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchService.java b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchService.java
index b54c1fa08a35fbe6333676b48f1c351a14ee1dfe..ea9bca75909458e9eb1baa185ab3605f1cb0b240 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchService.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchService.java
@@ -24,9 +24,12 @@ import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
 import org.thoughtcrime.securesms.messages.RestStrategy;
 import org.thoughtcrime.securesms.notifications.NotificationChannels;
 import org.thoughtcrime.securesms.notifications.NotificationIds;
+import org.thoughtcrime.securesms.service.GenericForegroundService;
+import org.thoughtcrime.securesms.service.NotificationController;
 import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
 
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * This service does the actual network fetch in response to an FCM message.
@@ -54,7 +57,8 @@ public class FcmFetchService extends Service {
 
   private static final SerialMonoLifoExecutor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.UNBOUNDED);
 
-  private final AtomicInteger activeCount = new AtomicInteger(0);
+  private final AtomicInteger                           activeCount          = new AtomicInteger(0);
+  private final AtomicReference<NotificationController> foregroundController = new AtomicReference<>();
 
   public static @NonNull Intent buildIntent(@NonNull Context context, boolean foreground) {
     Intent intent = new Intent(context, FcmFetchService.class);
@@ -73,16 +77,18 @@ public class FcmFetchService extends Service {
       Log.i(TAG, "Incrementing active count to " + count);
     }
 
-    if (intent.getBooleanExtra(KEY_FOREGROUND, false)) {
-      Log.i(TAG, "Launching in the foreground.");
-      startForeground(NotificationIds.FCM_FETCH, new NotificationCompat.Builder(this, NotificationChannels.OTHER)
-          .setSmallIcon(R.drawable.ic_notification)
-          .setContentTitle(getString(R.string.BackgroundMessageRetriever_checking_for_messages))
-          .setCategory(NotificationCompat.CATEGORY_SERVICE)
-          .setProgress(0, 0, true)
-          .setContentIntent(PendingIntent.getActivity(this, 0, MainActivity.clearTop(this), 0))
-          .setVibrate(new long[] { 0 })
-          .build());
+    synchronized (foregroundController) {
+      boolean useForeground = intent.getBooleanExtra(KEY_FOREGROUND, false);
+      boolean hasController = foregroundController.get() != null;
+      
+      if (useForeground && !hasController) {
+        Log.i(TAG, "Launching in the foreground.");
+        NotificationController controller = GenericForegroundService.startForegroundTask(this, getString(R.string.BackgroundMessageRetriever_checking_for_messages), NotificationChannels.OTHER);
+        controller.setIndeterminateProgress();
+        foregroundController.set(controller);
+      } else {
+        Log.i(TAG, "Launching in the background. (useForeground: " + useForeground + ", hasController: " + hasController + ")");
+      }
     }
 
     return START_NOT_STICKY;
@@ -104,6 +110,15 @@ public class FcmFetchService extends Service {
     if (activeCount.decrementAndGet() == 0) {
       Log.d(TAG, "No more active. Stopping.");
       stopSelf();
+
+      synchronized (foregroundController) {
+        NotificationController activeController = foregroundController.get();
+        if (activeController != null) {
+          Log.d(TAG, "Stopping foreground notification.");
+          activeController.close();
+          foregroundController.set(null);
+        }
+      }
     }
   }
 
diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
index fe57f237cec27e51aaa39eefa6a9dff4d65f7dc1..44a78f518708f469ba596c9eaefb28f8a963ba21 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
 import org.thoughtcrime.securesms.jobs.SubmitRateLimitPushChallengeJob;
 import org.thoughtcrime.securesms.keyvalue.SignalStore;
 import org.thoughtcrime.securesms.registration.PushChallengeRequest;
+import org.thoughtcrime.securesms.util.FeatureFlags;
 
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
@@ -79,8 +80,8 @@ public class FcmReceiveService extends FirebaseMessagingService {
   private static void handleReceivedNotification(Context context, @Nullable RemoteMessage remoteMessage) {
     try {
       long timeSinceLastRefresh = System.currentTimeMillis() - SignalStore.misc().getLastFcmForegroundServiceTime();
-      if (Build.VERSION.SDK_INT >= 31 && remoteMessage != null && remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH && timeSinceLastRefresh > FCM_FOREGROUND_INTERVAL) {
-        ContextCompat.startForegroundService(context, FcmFetchService.buildIntent(context, true));
+      if (FeatureFlags.useFcmForegroundService() && Build.VERSION.SDK_INT >= 31 && remoteMessage != null && remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH && timeSinceLastRefresh > FCM_FOREGROUND_INTERVAL) {
+        context.startService(FcmFetchService.buildIntent(context, true));
         SignalStore.misc().setLastFcmForegroundServiceTime(System.currentTimeMillis());
       } else {
         context.startService(FcmFetchService.buildIntent(context, false));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java
deleted file mode 100644
index 1341b760db0ad8564b2137fac6ec9064f8025a0a..0000000000000000000000000000000000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.thoughtcrime.securesms.jobs;
-
-import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.jobmanager.Job;
-
-public abstract class PushReceivedJob extends BaseJob {
-
-  private static final String TAG = Log.tag(PushReceivedJob.class);
-
-
-  protected PushReceivedJob(Job.Parameters parameters) {
-    super(parameters);
-  }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index 1912ceb1246c9268fed7d5bde474c092c9bc1906..83315ce06eb4e7d30713be24183aa8847eae6ae4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -96,6 +96,7 @@ public final class FeatureFlags {
   private static final String USE_AEC3                          = "android.calling.useAec3";
   private static final String PAYMENTS_COUNTRY_BLOCKLIST        = "android.payments.blocklist";
   private static final String PNP_CDS                           = "android.pnp.cds";
+  private static final String USE_FCM_FOREGROUND_SERVICE        = "android.useFcmForegroundService";
 
   /**
    * We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -143,7 +144,8 @@ public final class FeatureFlags {
       SOFTWARE_AEC_BLOCKLIST_MODELS,
       USE_HARDWARE_AEC_IF_OLD,
       USE_AEC3,
-      PAYMENTS_COUNTRY_BLOCKLIST
+      PAYMENTS_COUNTRY_BLOCKLIST,
+      USE_FCM_FOREGROUND_SERVICE
   );
 
   @VisibleForTesting
@@ -203,7 +205,8 @@ public final class FeatureFlags {
       SOFTWARE_AEC_BLOCKLIST_MODELS,
       USE_HARDWARE_AEC_IF_OLD,
       USE_AEC3,
-      PAYMENTS_COUNTRY_BLOCKLIST
+      PAYMENTS_COUNTRY_BLOCKLIST,
+      USE_FCM_FOREGROUND_SERVICE
   );
 
   /**
@@ -505,6 +508,10 @@ public final class FeatureFlags {
     return Environment.IS_STAGING && getBoolean(PNP_CDS, false);
   }
 
+  public static boolean useFcmForegroundService() {
+    return getBoolean(USE_FCM_FOREGROUND_SERVICE, false);
+  }
+
   /** Only for rendering debug info. */
   public static synchronized @NonNull Map<String, Object> getMemoryValues() {
     return new TreeMap<>(REMOTE_VALUES);